diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index 7a57a01899..22dfac4bb9 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -240,7 +240,7 @@ getRouteNonIdempotentWithRWTransaction( "/gdocs/:id", getIndividualGdoc ) -putRouteWithRWTransaction(apiRouter, "/gdocs/:id", createOrUpdateGdoc) +apiRouter.put("/gdocs/:id", createOrUpdateGdoc) deleteRouteWithRWTransaction(apiRouter, "/gdocs/:id", deleteGdoc) postRouteWithRWTransaction(apiRouter, "/gdocs/:gdocId/setTags", setGdocTags) diff --git a/adminSiteServer/apiRoutes/gdocs.ts b/adminSiteServer/apiRoutes/gdocs.ts index fbeb412e0d..818fe4143b 100644 --- a/adminSiteServer/apiRoutes/gdocs.ts +++ b/adminSiteServer/apiRoutes/gdocs.ts @@ -1,10 +1,10 @@ import { getCanonicalUrl } from "@ourworldindata/components" import { GdocsContentSource, - DbInsertUser, JsonError, GDOCS_BASE_URL, gdocUrlRegex, + OwidGdocPostInterface, PostsGdocsLinksTableName, PostsGdocsXImagesTableName, PostsGdocsTableName, @@ -84,67 +84,6 @@ export async function getIndividualGdoc( } } -/** - * Handles all four `GdocPublishingAction` cases - * - SavingDraft (no action) - * - Publishing (index and bake) - * - Updating (index and bake (potentially via lightning deploy)) - * - Unpublishing (remove from index and bake) - */ -async function indexAndBakeGdocIfNeccesary( - trx: db.KnexReadWriteTransaction, - user: Required, - prevGdoc: - | GdocPost - | GdocDataInsight - | GdocHomepage - | GdocAbout - | GdocAuthor, - nextGdoc: GdocPost | GdocDataInsight | GdocHomepage | GdocAbout | GdocAuthor -) { - const prevJson = prevGdoc.toJSON() - const nextJson = nextGdoc.toJSON() - const hasChanges = checkHasChanges(prevGdoc, nextGdoc) - const action = getPublishingAction(prevJson, nextJson) - const isGdocPost = checkIsGdocPostExcludingFragments(nextJson) - - await match(action) - .with(GdocPublishingAction.SavingDraft, lodash.noop) - .with(GdocPublishingAction.Publishing, async () => { - if (isGdocPost) { - await indexIndividualGdocPost( - nextJson, - trx, - // If the gdoc is being published for the first time, prevGdoc.slug will be undefined - // In that case, we pass nextJson.slug to see if it has any page views (i.e. from WP) - prevGdoc.slug || nextJson.slug - ) - } - await triggerStaticBuild(user, `${action} ${nextJson.slug}`) - }) - .with(GdocPublishingAction.Updating, async () => { - if (isGdocPost) { - await indexIndividualGdocPost(nextJson, trx, prevGdoc.slug) - } - if (checkIsLightningUpdate(prevJson, nextJson, hasChanges)) { - await enqueueLightningChange( - user, - `Lightning update ${nextJson.slug}`, - nextJson.slug - ) - } else { - await triggerStaticBuild(user, `${action} ${nextJson.slug}`) - } - }) - .with(GdocPublishingAction.Unpublishing, async () => { - if (isGdocPost) { - await removeIndividualGdocPostFromIndex(nextJson) - } - await triggerStaticBuild(user, `${action} ${nextJson.slug}`) - }) - .exhaustive() -} - /** * Only supports creating a new empty Gdoc or updating an existing one. Does not * support creating a new Gdoc from an existing one. Relevant updates will @@ -152,35 +91,100 @@ async function indexAndBakeGdocIfNeccesary( */ export async function createOrUpdateGdoc( req: Request, - res: e.Response>, - trx: db.KnexReadWriteTransaction + res: e.Response> ) { const { id } = req.params if (isEmpty(req.body)) { - return createOrLoadGdocById(trx, id) + return await db.knexReadWriteTransaction(async (trx) => { + return await createOrLoadGdocById(trx, id) + }) } - const prevGdoc = await getAndLoadGdocById(trx, id) - if (!prevGdoc) throw new JsonError(`No Google Doc with id ${id} found`) + const user = res.locals.user + let nextGdoc: + | GdocPost + | GdocDataInsight + | GdocHomepage + | GdocAbout + | GdocAuthor + | undefined + let build: "full" | "lightning" | undefined + let buildMessage = "" + + await db.knexReadWriteTransaction(async (trx) => { + const prevGdoc = await getAndLoadGdocById(trx, id) + if (!prevGdoc) throw new JsonError(`No Google Doc with id ${id} found`) - const nextGdoc = gdocFromJSON(req.body) - await nextGdoc.loadState(trx) + nextGdoc = gdocFromJSON(req.body) + await nextGdoc.loadState(trx) - await addImagesToContentGraph(trx, nextGdoc) + await addImagesToContentGraph(trx, nextGdoc) - await setLinksForGdoc( - trx, - nextGdoc.id, - nextGdoc.links, - nextGdoc.published - ? GdocLinkUpdateMode.DeleteAndInsert - : GdocLinkUpdateMode.DeleteOnly - ) + await setLinksForGdoc( + trx, + nextGdoc.id, + nextGdoc.links, + nextGdoc.published + ? GdocLinkUpdateMode.DeleteAndInsert + : GdocLinkUpdateMode.DeleteOnly + ) - await upsertGdoc(trx, nextGdoc) + await upsertGdoc(trx, nextGdoc) - await indexAndBakeGdocIfNeccesary(trx, res.locals.user, prevGdoc, nextGdoc) + const prevJson = prevGdoc.toJSON() + const nextJson = nextGdoc.toJSON() + const hasChanges = checkHasChanges(prevGdoc, nextGdoc) + const action = getPublishingAction(prevJson, nextJson) + const isGdocPost = checkIsGdocPostExcludingFragments(nextJson) + + await match(action) + .with(GdocPublishingAction.SavingDraft, lodash.noop) + .with(GdocPublishingAction.Publishing, async () => { + if (isGdocPost) { + await indexIndividualGdocPost( + nextJson as OwidGdocPostInterface, + trx, + // If the gdoc is being published for the first time, prevGdoc.slug will be undefined + // In that case, we pass nextJson.slug to see if it has any page views (i.e. from WP) + prevGdoc.slug || nextJson.slug + ) + } + build = "full" + buildMessage = `${action} ${nextJson.slug}` + }) + .with(GdocPublishingAction.Updating, async () => { + if (isGdocPost) { + await indexIndividualGdocPost(nextJson, trx, prevGdoc.slug) + } + if (checkIsLightningUpdate(prevJson, nextJson, hasChanges)) { + build = "lightning" + buildMessage = `Lightning update ${nextJson.slug}` + } else { + build = "full" + buildMessage = `${action} ${nextJson.slug}` + } + }) + .with(GdocPublishingAction.Unpublishing, async () => { + if (isGdocPost) { + await removeIndividualGdocPostFromIndex(nextJson) + } + build = "full" + buildMessage = `${action} ${nextJson.slug}` + }) + .exhaustive() + }) + + if (!nextGdoc) return + + // The build must be triggered after the transaction has been committed + // otherwise the deploy-queue process can get stale data from the DB. + // https://github.com/owid/owid-grapher/issues/3908 + if (build === "full") { + await triggerStaticBuild(user, buildMessage) + } else if (build === "lightning") { + await enqueueLightningChange(user, buildMessage, nextGdoc.slug) + } return nextGdoc }