Merged in feat/sw-2863-move-contentstack-router-to-trpc-package (pull request #2389)

feat(SW-2863): Move contentstack router to trpc package

* Add exports to packages and lint rule to prevent relative imports

* Add env to trpc package

* Add eslint to trpc package

* Apply lint rules

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-06-26 07:53:01 +00:00
parent 0263ab8c87
commit 002d093af4
921 changed files with 3112 additions and 3008 deletions

View File

@@ -0,0 +1,4 @@
import { mergeRouters } from "../../.."
import { collectionPageQueryRouter } from "./query"
export const collectionPageRouter = mergeRouters(collectionPageQueryRouter)

View File

@@ -0,0 +1,164 @@
import { z } from "zod"
import { CollectionPageEnum } from "../../../types/collectionPage"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import {
cardGridRefsSchema,
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import {
dynamicContentRefsSchema,
dynamicContentSchema as blockDynamicContentSchema,
} from "../schemas/blocks/dynamicContent"
import {
shortcutsRefsSchema,
shortcutsSchema,
} from "../schemas/blocks/shortcuts"
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import {
linkAndTitleSchema,
linkConnectionRefs,
} from "../schemas/linkConnection"
import { systemSchema } from "../schemas/system"
// Block schemas
export const collectionPageCards = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardsGridSchema)
export const collectionPageShortcuts = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
})
.merge(shortcutsSchema)
export const collectionPageUspGrid = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
})
.merge(uspGridSchema)
export const collectionPageDynamicContent = z
.object({
__typename: z.literal(
CollectionPageEnum.ContentStack.blocks.DynamicContent
),
})
.merge(blockDynamicContentSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
collectionPageCards,
collectionPageDynamicContent,
collectionPageShortcuts,
collectionPageUspGrid,
])
const navigationLinksSchema = z
.array(linkAndTitleSchema)
.nullable()
.transform((data) => {
if (!data) {
return null
}
return data
.filter((item) => !!item.link)
.map((item) => ({
url: item.link!.url,
title: item.title || item.link!.title,
}))
})
const topPrimaryButtonSchema = linkAndTitleSchema
.nullable()
.transform((data) => {
if (!data?.link) {
return null
}
return {
url: data.link.url,
title: data.title || data.link.title || null,
}
})
// Content Page Schema and types
export const collectionPageSchema = z.object({
collection_page: z.object({
hero_image: tempImageVaultAssetSchema,
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(),
header: z.object({
heading: z.string(),
preamble: z.string(),
top_primary_button: topPrimaryButtonSchema,
navigation_links: navigationLinksSchema,
}),
meeting_package: z
.object({
show_widget: z.boolean(),
location: z.string(),
})
.nullable(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
/** REFS */
const collectionPageCardsRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardGridRefsSchema)
const collectionPageShortcutsRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
})
.merge(shortcutsRefsSchema)
const collectionPageUspGridRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
})
.merge(uspGridRefsSchema)
const contentPageDynamicContentRefs = z
.object({
__typename: z.literal(
CollectionPageEnum.ContentStack.blocks.DynamicContent
),
})
.merge(dynamicContentRefsSchema)
const collectionPageBlockRefsItem = z.discriminatedUnion("__typename", [
collectionPageShortcutsRefs,
contentPageDynamicContentRefs,
collectionPageCardsRefs,
collectionPageUspGridRefs,
])
const collectionPageHeaderRefs = z.object({
navigation_links: z.array(linkConnectionRefs),
top_primary_button: linkConnectionRefs.nullable(),
})
export const collectionPageRefsSchema = z.object({
collection_page: z.object({
header: collectionPageHeaderRefs,
blocks: discriminatedUnionArray(
collectionPageBlockRefsItem.options
).nullable(),
system: systemSchema,
}),
})

View File

@@ -0,0 +1,78 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { contentstackExtendedProcedureUID } from "@scandic-hotels/trpc/procedures"
import { router } from "../../.."
import { GetCollectionPage } from "../../../graphql/Query/CollectionPage/CollectionPage.graphql"
import { request } from "../../../graphql/request"
import { collectionPageSchema } from "./output"
import {
fetchCollectionPageRefs,
generatePageTags,
validateCollectionPageRefs,
} from "./utils"
import type { GetCollectionPageSchema } from "../../../types/collectionPage"
import type { TrackingPageData } from "../../types"
export const collectionPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
const collectionPageRefsData = await fetchCollectionPageRefs(lang, uid)
const collectionPageRefs = validateCollectionPageRefs(
collectionPageRefsData,
lang,
uid
)
if (!collectionPageRefs) {
return null
}
const tags = generatePageTags(collectionPageRefs, lang)
const getCollectionPageCounter = createCounter(
"trpc.contentstack",
"collectionPage.get"
)
const metricsGetCollectionPage = getCollectionPageCounter.init({
lang,
uid,
})
metricsGetCollectionPage.start()
const response = await request<GetCollectionPageSchema>(
GetCollectionPage,
{ locale: lang, uid },
{
key: tags,
ttl: "max",
}
)
const collectionPage = collectionPageSchema.safeParse(response.data)
if (!collectionPage.success) {
metricsGetCollectionPage.validationError(collectionPage.error)
return null
}
metricsGetCollectionPage.success()
const tracking: TrackingPageData = {
pageId: collectionPage.data.collection_page.system.uid,
domainLanguage: lang,
publishDate: collectionPage.data.collection_page.system.updated_at,
createDate: collectionPage.data.collection_page.system.created_at,
channel: "collection-page",
pageType: "collectionpage",
pageName: collectionPage.data.trackingProps.url,
siteSections: collectionPage.data.trackingProps.url,
siteVersion: "new-web",
}
return {
collectionPage: collectionPage.data.collection_page,
tracking,
}
}),
})

View File

@@ -0,0 +1,120 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { notFound } from "@scandic-hotels/trpc/errors"
import { GetCollectionPageRefs } from "../../../graphql/Query/CollectionPage/CollectionPage.graphql"
import { request } from "../../../graphql/request"
import {
CollectionPageEnum,
type CollectionPageRefs,
type GetCollectionPageRefsSchema,
} from "../../../types/collectionPage"
import {
generateRefsResponseTag,
generateTag,
generateTagsFromSystem,
} from "../../../utils/generateTag"
import { collectionPageRefsSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { System } from "../schemas/system"
export async function fetchCollectionPageRefs(lang: Lang, uid: string) {
const getCollectionPageRefsCounter = createCounter(
"trpc.contentstack",
"collectionPage.get.refs"
)
const metricsGetCollectionPageRefs = getCollectionPageRefsCounter.init({
lang,
uid,
})
metricsGetCollectionPageRefs.start()
const cacheKey = generateRefsResponseTag(lang, uid)
const refsResponse = await request<GetCollectionPageRefsSchema>(
GetCollectionPageRefs,
{
locale: lang,
uid,
},
{
key: cacheKey,
ttl: "max",
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
metricsGetCollectionPageRefs.noDataError()
throw notFoundError
}
return refsResponse.data
}
export function validateCollectionPageRefs(
data: GetCollectionPageRefsSchema,
lang: Lang,
uid: string
) {
const getCollectionPageRefsCounter = createCounter(
"trpc.contentstack",
"collectionPage.get.refs"
)
const metricsGetCollectionPageRefs = getCollectionPageRefsCounter.init({
lang,
uid,
})
const validatedData = collectionPageRefsSchema.safeParse(data)
if (!validatedData.success) {
metricsGetCollectionPageRefs.validationError(validatedData.error)
return null
}
metricsGetCollectionPageRefs.success()
return validatedData.data
}
export function generatePageTags(
validatedData: CollectionPageRefs,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.collection_page.system.uid),
].flat()
}
export function getConnections({ collection_page }: CollectionPageRefs) {
const connections: System["system"][] = [collection_page.system]
if (collection_page.blocks) {
collection_page.blocks.forEach((block) => {
switch (block.__typename) {
case CollectionPageEnum.ContentStack.blocks.Shortcuts: {
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts)
}
break
}
case CollectionPageEnum.ContentStack.blocks.CardsGrid: {
if (block.cards_grid.length) {
connections.push(...block.cards_grid)
}
break
}
case CollectionPageEnum.ContentStack.blocks.UspGrid: {
if (block.usp_grid.length) {
connections.push(...block.usp_grid)
}
}
}
})
}
return connections
}