Merged in feat/rework-contentstack (pull request #3493)

Feat(SW-3708): refactor contentstack fetching (removing all refs) and cache invalidation

* Remove all REFS

* Revalidate correct language

* PR fixes

* PR fixes

* Throw when errors from contentstack api


Approved-by: Joakim Jäderberg
This commit is contained in:
Linus Flood
2026-01-27 12:38:36 +00:00
parent a5e214f783
commit 5fc93472f4
193 changed files with 489 additions and 9018 deletions

View File

@@ -4,68 +4,29 @@ import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/i
import { ContentPageEnum } from "../../../types/contentPage"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import {
accordionRefsSchema,
accordionSchema,
} from "../schemas/blocks/accordion"
import {
cardGridRefsSchema,
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import {
contentRefsSchema as blockContentRefsSchema,
contentSchema as blockContentSchema,
} from "../schemas/blocks/content"
import {
dynamicContentRefsSchema,
dynamicContentSchema as blockDynamicContentSchema,
} from "../schemas/blocks/dynamicContent"
import { accordionSchema } from "../schemas/blocks/accordion"
import { cardsGridSchema } from "../schemas/blocks/cardsGrid"
import { contentSchema as blockContentSchema } from "../schemas/blocks/content"
import { dynamicContentSchema as blockDynamicContentSchema } from "../schemas/blocks/dynamicContent"
import { contentPageHotelListingSchema } from "../schemas/blocks/hotelListing"
import { jotformRefsSchema, jotformSchema } from "../schemas/blocks/jotform"
import {
shortcutsRefsSchema,
shortcutsSchema,
} from "../schemas/blocks/shortcuts"
import { jotformSchema } from "../schemas/blocks/jotform"
import { shortcutsSchema } from "../schemas/blocks/shortcuts"
import { tableSchema } from "../schemas/blocks/table"
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
import { videoBlockRefsSchema, videoBlockSchema } from "../schemas/blocks/video"
import {
videoCardRefsSchema,
videoCardSchema,
} from "../schemas/blocks/videoCard"
import {
dynamicContentRefsSchema as headerDynamicContentRefsSchema,
dynamicContentSchema as headerDynamicContentSchema,
} from "../schemas/headers/dynamicContent"
import {
linkAndTitleSchema,
linkConnectionRefs,
} from "../schemas/linkConnection"
import { textColsSchema } from "../schemas/blocks/textCols"
import { uspGridSchema } from "../schemas/blocks/uspGrid"
import { videoBlockSchema } from "../schemas/blocks/video"
import { videoCardSchema } from "../schemas/blocks/videoCard"
import { dynamicContentSchema as headerDynamicContentSchema } from "../schemas/headers/dynamicContent"
import { linkAndTitleSchema } from "../schemas/linkConnection"
import { internalOrExternalLinkSchema } from "../schemas/pageLinks"
import {
contentRefsSchema as sidebarContentRefsSchema,
contentSchema as sidebarContentSchema,
} from "../schemas/sidebar/content"
import { contentSchema as sidebarContentSchema } from "../schemas/sidebar/content"
import { dynamicContentSchema as sidebarDynamicContentSchema } from "../schemas/sidebar/dynamicContent"
import {
joinLoyaltyContactRefsSchema,
joinLoyaltyContactSchema,
} from "../schemas/sidebar/joinLoyaltyContact"
import {
quickLinksRefschema,
quickLinksSchema,
} from "../schemas/sidebar/quickLinks"
import {
scriptedCardRefschema,
scriptedCardsSchema,
} from "../schemas/sidebar/scriptedCard"
import {
teaserCardRefschema,
teaserCardsSchema,
} from "../schemas/sidebar/teaserCard"
import { joinLoyaltyContactSchema } from "../schemas/sidebar/joinLoyaltyContact"
import { quickLinksSchema } from "../schemas/sidebar/quickLinks"
import { scriptedCardsSchema } from "../schemas/sidebar/scriptedCard"
import { teaserCardsSchema } from "../schemas/sidebar/teaserCard"
import { systemSchema } from "../schemas/system"
import { transformedVideoSchema, videoRefSchema } from "../schemas/video"
import { transformedVideoSchema } from "../schemas/video"
// Block schemas
export const contentPageCards = z
@@ -250,137 +211,3 @@ export const contentPageSchema = z.object({
url: z.string(),
}),
})
/** REFS */
const contentPageCardsRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardGridRefsSchema)
const contentPageBlockContentRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Content),
})
.merge(blockContentRefsSchema)
const contentPageDynamicContentRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.DynamicContent),
})
.merge(dynamicContentRefsSchema)
const contentPageShortcutsRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Shortcuts),
})
.merge(shortcutsRefsSchema)
const contentPageTextColsRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.TextCols),
})
.merge(textColsRefsSchema)
const contentPageUspGridRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid),
})
.merge(uspGridRefsSchema)
const contentPageAccordionRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Accordion),
})
.merge(accordionRefsSchema)
const contentPageVideoCardRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.VideoCard),
})
.merge(videoCardRefsSchema)
const contentPageVideoRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Video),
})
.merge(videoBlockRefsSchema)
const contentPageJotformRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Jotform),
})
.merge(jotformRefsSchema)
const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [
contentPageAccordionRefs,
contentPageBlockContentRefs,
contentPageShortcutsRefs,
contentPageCardsRefs,
contentPageDynamicContentRefs,
contentPageTextColsRefs,
contentPageUspGridRefs,
contentPageVideoCardRefs,
contentPageJotformRefs,
contentPageVideoRefs,
])
const contentPageSidebarContentRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.Content),
})
.merge(sidebarContentRefsSchema)
const contentPageSidebarJoinLoyaltyContactRef = z
.object({
__typename: z.literal(
ContentPageEnum.ContentStack.sidebar.JoinLoyaltyContact
),
})
.merge(joinLoyaltyContactRefsSchema)
const contentPageSidebarScriptedCardRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.ScriptedCard),
})
.merge(scriptedCardRefschema)
const contentPageSidebarTeaserCardRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.TeaserCard),
})
.merge(teaserCardRefschema)
const contentPageSidebarQuickLinksRef = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.sidebar.QuickLinks),
})
.merge(quickLinksRefschema)
const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [
contentPageSidebarContentRef,
contentPageSidebarJoinLoyaltyContactRef,
contentPageSidebarScriptedCardRef,
contentPageSidebarTeaserCardRef,
contentPageSidebarQuickLinksRef,
])
const contentPageHeaderRefs = z.object({
navigation_links: z.array(linkConnectionRefs),
top_primary_button: linkConnectionRefs.nullable(),
dynamic_content: headerDynamicContentRefsSchema.nullish(),
})
export const contentPageRefsSchema = z.object({
content_page: z.object({
hero_video: videoRefSchema.nullish(),
header: contentPageHeaderRefs,
blocks: discriminatedUnionArray(
contentPageBlockRefsItem.options
).nullable(),
sidebar: discriminatedUnionArray(
contentPageSidebarRefsItem.options
).nullable(),
system: systemSchema,
}),
})

View File

@@ -8,8 +8,8 @@ import {
GetContentPageBlocksBatch2,
} from "../../../graphql/Query/ContentPage/ContentPage.graphql"
import { contentstackExtendedProcedureUID } from "../../../procedures"
import { generateTag } from "../../../utils/generateTag"
import { contentPageSchema } from "./output"
import { fetchContentPageRefs, generatePageTags } from "./utils"
import type { GetContentPageSchema } from "../../../types/contentPage"
import type { TrackingPageData } from "../../types"
@@ -18,13 +18,8 @@ export const contentPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
const contentPageRefs = await fetchContentPageRefs(lang, uid)
if (!contentPageRefs) {
return null
}
const tags = generatePageTags(contentPageRefs, lang)
// by fetching references when a child entry is published
const cacheKey = generateTag(lang, uid)
const getContentPageCounter = createCounter(
"trpc.contentstack.contentPage.get"
@@ -41,7 +36,7 @@ export const contentPageQueryRouter = router({
document: GetContentPage,
variables: { locale: lang, uid },
cacheOptions: {
key: `${tags.join(",")}:contentPage`,
key: `${cacheKey}:contentPage`,
ttl: "max",
},
},
@@ -50,7 +45,7 @@ export const contentPageQueryRouter = router({
document: GetContentPageBlocksBatch1,
variables: { locale: lang, uid },
cacheOptions: {
key: `${tags.join(",")}:contentPageBlocksBatch1`,
key: `${cacheKey}:contentPageBlocksBatch1`,
ttl: "max",
},
},
@@ -59,7 +54,7 @@ export const contentPageQueryRouter = router({
document: GetContentPageBlocksBatch2,
variables: { locale: lang, uid },
cacheOptions: {
key: `${tags.join(",")}:contentPageBlocksBatch2`,
key: `${cacheKey}:contentPageBlocksBatch2`,
ttl: "max",
},
},

View File

@@ -1,218 +0,0 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { notFoundError } from "../../../errors"
import { batchRequest } from "../../../graphql/batchRequest"
import {
GetContentPageBlocksRefs,
GetContentPageRefs,
} from "../../../graphql/Query/ContentPage/ContentPage.graphql"
import { ContentPageEnum } from "../../../types/contentPage"
import {
generateRefsResponseTag,
generateTag,
generateTagsFromAssetSystem,
generateTagsFromSystem,
} from "../../../utils/generateTag"
import { contentPageRefsSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type {
ContentPageRefs,
GetContentPageRefsSchema,
} from "../../../types/contentPage"
import type { AssetSystem, System } from "../schemas/system"
export async function fetchContentPageRefs(lang: Lang, uid: string) {
const getContentPageRefsCounter = createCounter(
"trpc.contentstack.contentPage.get.refs"
)
const metricsGetContentPageRefs = getContentPageRefsCounter.init({
lang,
uid,
})
metricsGetContentPageRefs.start()
const variables = { locale: lang, uid }
const res = await batchRequest<GetContentPageRefsSchema>([
{
document: GetContentPageRefs,
variables,
cacheOptions: {
key: generateRefsResponseTag(lang, uid),
ttl: "max",
},
},
{
document: GetContentPageBlocksRefs,
variables,
cacheOptions: {
key: generateTag(lang, uid + 1),
ttl: "max",
},
},
])
if (!res.data) {
metricsGetContentPageRefs.noDataError()
throw notFoundError({
message: "GetContentPageRefs/GetContentPageBlocksRefs returned no data",
errorDetails: variables,
})
}
const validatedData = contentPageRefsSchema.safeParse(res.data)
if (!validatedData.success) {
metricsGetContentPageRefs.validationError(validatedData.error)
return null
}
metricsGetContentPageRefs.success()
return validatedData.data
}
export function generatePageTags(
validatedData: ContentPageRefs,
lang: Lang
): string[] {
const { connections, assetConnections } = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTagsFromAssetSystem(assetConnections),
generateTag(lang, validatedData.content_page.system.uid),
].flat()
}
export function getConnections({ content_page }: ContentPageRefs) {
const connections: System["system"][] = [content_page.system]
const assetConnections: AssetSystem[] = []
if (content_page.hero_video?.sourceConnection.edges[0]) {
assetConnections.push(
content_page.hero_video.sourceConnection.edges[0].node
)
}
if (content_page.blocks) {
content_page.blocks.forEach((block) => {
const typeName = block.__typename
switch (typeName) {
case ContentPageEnum.ContentStack.blocks.Accordion:
if (block.accordion.length) {
connections.push(...block.accordion.filter((c) => !!c))
}
break
case ContentPageEnum.ContentStack.blocks.Content:
if (block?.content?.length) {
block.content.forEach((contentBlock) => {
if ("system" in contentBlock) {
assetConnections.push(contentBlock)
} else {
connections.push(contentBlock)
}
})
}
break
case ContentPageEnum.ContentStack.blocks.CardsGrid:
if (block.cards_grid.length) {
connections.push(...block.cards_grid)
}
break
case ContentPageEnum.ContentStack.blocks.DynamicContent:
if (block.dynamic_content.link) {
connections.push(block.dynamic_content.link)
}
break
case ContentPageEnum.ContentStack.blocks.Shortcuts:
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts.filter((c) => !!c))
}
break
case ContentPageEnum.ContentStack.blocks.TextCols:
if (block.text_cols.length) {
connections.push(...block.text_cols)
}
break
case ContentPageEnum.ContentStack.blocks.UspGrid:
if (block.usp_grid.length) {
connections.push(...block.usp_grid.filter((c) => !!c))
}
break
case ContentPageEnum.ContentStack.blocks.CardsGrid:
if (block.cards_grid.length) {
block.cards_grid.forEach((card) => {
connections.push(card)
})
}
break
case ContentPageEnum.ContentStack.blocks.VideoCard:
if (block.video_card) {
connections.push(block.video_card.system)
}
if (block.video_card?.video.sourceConnection.edges[0]) {
assetConnections.push(
block.video_card.video.sourceConnection.edges[0].node
)
}
break
case ContentPageEnum.ContentStack.blocks.Video:
if (block.video?.sourceConnection.edges[0]) {
assetConnections.push(block.video.sourceConnection.edges[0].node)
}
break
case ContentPageEnum.ContentStack.blocks.Jotform:
if (block.jotform) {
connections.push(block.jotform.system)
}
break
default:
const _exhaustiveCheck: never = typeName
break
}
})
}
if (content_page.sidebar) {
content_page.sidebar.forEach((block) => {
const typeName = block.__typename
switch (typeName) {
case ContentPageEnum.ContentStack.sidebar.Content:
if (block.content?.length) {
block.content.forEach((contentBlock) => {
if ("system" in contentBlock) {
assetConnections.push(contentBlock)
} else {
connections.push(contentBlock)
}
})
}
break
case ContentPageEnum.ContentStack.sidebar.JoinLoyaltyContact:
if (block.join_loyalty_contact?.button) {
connections.push(block.join_loyalty_contact.button)
}
break
case ContentPageEnum.ContentStack.sidebar.ScriptedCard:
if (block.scripted_card?.length) {
connections.push(...block.scripted_card)
}
break
case ContentPageEnum.ContentStack.sidebar.TeaserCard:
if (block.teaser_card?.length) {
connections.push(...block.teaser_card)
}
break
case ContentPageEnum.ContentStack.sidebar.QuickLinks:
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts.filter((c) => !!c))
}
break
default:
const _exhaustiveCheck: never = typeName
break
}
})
}
return { connections, assetConnections }
}