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 { campaignPageQueryRouter } from "./query"
export const campaignPageRouter = mergeRouters(campaignPageQueryRouter)

View File

@@ -0,0 +1,191 @@
import { z } from "zod"
import { CampaignPageEnum } from "../../../types/campaignPage"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import {
accordionRefsSchema,
accordionSchema,
} from "../schemas/blocks/accordion"
import {
carouselCardsRefsSchema,
carouselCardsSchema,
} from "../schemas/blocks/carouselCards"
import { essentialsBlockSchema } from "../schemas/blocks/essentials"
import { campaignPageHotelListingSchema } from "../schemas/blocks/hotelListing"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import {
linkConnectionRefs,
linkConnectionSchema,
} from "../schemas/linkConnection"
import { systemSchema } from "../schemas/system"
const campaignPageEssentials = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.Essentials),
})
.merge(essentialsBlockSchema)
const campaignPageCarouselCards = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.CarouselCards),
})
.merge(carouselCardsSchema)
export const campaignPageAccordion = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.Accordion),
})
.merge(accordionSchema)
export const campaignPageHotelListing = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.HotelListing),
})
.merge(campaignPageHotelListingSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
campaignPageEssentials,
campaignPageCarouselCards,
campaignPageAccordion,
campaignPageHotelListing,
])
export const heroSchema = z.object({
image: tempImageVaultAssetSchema,
heading: z.string(),
theme: z.enum(["Peach", "Burgundy"]).default("Peach"),
benefits: z
.array(z.string())
.nullish()
.transform((data) => data || []),
rate_text: z
.object({
bold_text: z.string().nullish(),
text: z.string().nullish(),
})
.nullish(),
button: z
.intersection(z.object({ cta: z.string() }), linkConnectionSchema)
.transform((data) => {
if (!data.link) {
return null
}
return {
cta: data.cta,
url: data.link?.url || "",
}
}),
})
export const includedHotelsSchema = z
.object({
list_1Connection: z.object({
edges: z.array(
z.object({
node: z.object({
hotel_page_id: z.string(),
}),
})
),
}),
list_2Connection: z.object({
edges: z.array(
z.object({
node: z.object({
hotel_page_id: z.string(),
}),
})
),
}),
})
.transform((data) => {
const list1HotelIds = data.list_1Connection.edges
.map((edge) => edge.node.hotel_page_id)
.filter(Boolean)
const list2HotelIds = data.list_2Connection.edges
.map((edge) => edge.node.hotel_page_id)
.filter(Boolean)
return [...new Set([...list1HotelIds, ...list2HotelIds])]
})
export const campaignPageSchema = z
.object({
campaign_page: z.object({
title: z.string(),
hero: heroSchema,
heading: z.string(),
subheading: z.string().nullish(),
included_hotels: includedHotelsSchema,
preamble: z.object({
is_two_columns: z.boolean().default(false),
first_column: z.string(),
second_column: z.string(),
}),
blocks: discriminatedUnionArray(blocksSchema.options),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
.transform((data) => {
const blocks = data.campaign_page.blocks.map((block) => {
if (
block.__typename === CampaignPageEnum.ContentStack.blocks.HotelListing
) {
return {
...block,
hotel_listing: {
...block.hotel_listing,
hotelIds: data.campaign_page.included_hotels,
},
}
}
return block
})
return {
...data,
campaign_page: {
...data.campaign_page,
blocks: [...blocks],
},
}
})
/** REFS */
const campaignPageCarouselCardsRef = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.CarouselCards),
})
.merge(carouselCardsRefsSchema)
const campaignPageAccordionRefs = z
.object({
__typename: z.literal(CampaignPageEnum.ContentStack.blocks.Accordion),
})
.merge(accordionRefsSchema)
const campaignPageBlockRefsItem = z.discriminatedUnion("__typename", [
campaignPageCarouselCardsRef,
campaignPageAccordionRefs,
])
const heroRefsSchema = z.object({
button: linkConnectionRefs,
})
export const campaignPageRefsSchema = z.object({
campaign_page: z.object({
hero: heroRefsSchema,
blocks: discriminatedUnionArray(
campaignPageBlockRefsItem.options
).nullable(),
system: systemSchema,
}),
})

View File

@@ -0,0 +1,121 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { notFound } from "@scandic-hotels/trpc/errors"
import { contentStackUidWithServiceProcedure } from "@scandic-hotels/trpc/procedures"
import { router } from "../../.."
import {
GetCampaignPage,
GetCampaignPageRefs,
} from "../../../graphql/Query/CampaignPage/CampaignPage.graphql"
import { request } from "../../../graphql/request"
import { generateRefsResponseTag } from "../../../utils/generateTag"
import { campaignPageRefsSchema, campaignPageSchema } from "./output"
import { generatePageTags } from "./utils"
import type {
GetCampaignPageData,
GetCampaignPageRefsData,
} from "../../../types/campaignPage"
import type { TrackingPageData } from "../../types"
export const campaignPageQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const getCampaignPageRefsCounter = createCounter(
"trpc.contentstack",
"campaignPage.get.refs"
)
const metricsGetCampaignPageRefs = getCampaignPageRefsCounter.init({
lang,
uid,
})
metricsGetCampaignPageRefs.start()
const refsResponse = await request<GetCampaignPageRefsData>(
GetCampaignPageRefs,
{ locale: lang, uid },
{
key: generateRefsResponseTag(lang, uid),
ttl: "max",
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
metricsGetCampaignPageRefs.noDataError()
throw notFoundError
}
const validatedRefsData = campaignPageRefsSchema.safeParse(
refsResponse.data
)
if (!validatedRefsData.success) {
metricsGetCampaignPageRefs.validationError(validatedRefsData.error)
return null
}
metricsGetCampaignPageRefs.success()
const tags = generatePageTags(validatedRefsData.data, lang)
const getCampaignPageCounter = createCounter(
"trpc.contentstack",
"campaignPage.get"
)
const metricsGetCampaignPage = getCampaignPageCounter.init({
lang,
uid,
})
metricsGetCampaignPage.start()
const response = await request<GetCampaignPageData>(
GetCampaignPage,
{
locale: lang,
uid,
},
{
key: tags,
ttl: "max",
}
)
if (!response.data) {
const notFoundError = notFound(response)
metricsGetCampaignPage.noDataError()
throw notFoundError
}
const validatedResponse = campaignPageSchema.safeParse(response.data)
if (!validatedResponse.success) {
metricsGetCampaignPage.validationError(validatedResponse.error)
return null
}
const campaignPage = validatedResponse.data.campaign_page
metricsGetCampaignPage.success()
const system = campaignPage.system
const pageName = `campaign-page`
const tracking: TrackingPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: "campaign-page",
pageType: "campaign-page",
pageName,
siteSections: pageName,
siteVersion: "new-web",
}
return {
campaignPage,
tracking,
}
}),
})

View File

@@ -0,0 +1,45 @@
import { CampaignPageEnum } from "../../../types/campaignPage"
import { generateTag, generateTagsFromSystem } from "../../../utils/generateTag"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { CampaignPageRefs } from "../../../types/campaignPage"
import type { System } from "../schemas/system"
export function generatePageTags(
validatedData: CampaignPageRefs,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.campaign_page.system.uid),
].flat()
}
export function getConnections({ campaign_page }: CampaignPageRefs) {
const connections: System["system"][] = [campaign_page.system]
if (campaign_page.blocks) {
campaign_page.blocks.forEach((block) => {
switch (block.__typename) {
case CampaignPageEnum.ContentStack.blocks.CarouselCards: {
block.carousel_cards.card_groups.forEach((group) => {
group.cardConnection.edges.forEach(({ node }) => {
connections.push(node.system)
})
})
break
}
case CampaignPageEnum.ContentStack.blocks.Accordion: {
if (block.accordion.length) {
connections.push(...block.accordion)
}
break
}
}
})
}
return connections
}