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

View File

@@ -0,0 +1,152 @@
import { z } from "zod"
import { removeMultipleSlashes } from "@scandic-hotels/common/utils/url"
import { HotelPageEnum } from "../../../types/hotelPageEnum"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import {
activitiesCardRefSchema,
activitiesCardSchema,
} from "../schemas/blocks/activitiesCard"
import { hotelFaqRefsSchema, hotelFaqSchema } from "../schemas/blocks/hotelFaq"
import { spaPageRefSchema, spaPageSchema } from "../schemas/blocks/spaPage"
import { systemSchema } from "../schemas/system"
import type { ActivitiesCard, SpaPage } from "../../../types/hotelPage"
const contentBlockActivities = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard),
})
.merge(activitiesCardSchema)
const contentBlockSpaPage = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.SpaPage),
})
.merge(spaPageSchema)
export const contentBlock = z.discriminatedUnion("__typename", [
contentBlockActivities,
contentBlockSpaPage,
])
export const hotelPageSchema = z.object({
hotel_page: z
.object({
hotel_navigation: z
.object({
overview: z.string().nullish(),
rooms: z.string().nullish(),
restaurant_bar: z.string().nullish(),
conferences_meetings: z.string().nullish(),
health_wellness: z.string().nullish(),
activities: z.string().nullish(),
offers: z.string().nullish(),
faq: z.string().nullish(),
})
.nullish(),
content: discriminatedUnionArray(contentBlock.options)
.nullable()
.transform((data) => {
let spaPage: SpaPage | undefined
let activitiesCards: ActivitiesCard[] = []
data?.map((block) => {
switch (block.typename) {
case HotelPageEnum.ContentStack.blocks.ActivitiesCard:
activitiesCards.push(block)
break
case HotelPageEnum.ContentStack.blocks.SpaPage:
spaPage = block
break
default:
break
}
})
return { spaPage, activitiesCards }
}),
faq: hotelFaqSchema.nullable(),
hotel_page_id: z.string(),
title: z.string(),
url: z.string(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
})
.transform(({ hotel_navigation, ...rest }) => ({
sectionHeadings: hotel_navigation,
...rest,
})),
})
/** REFS */
const hotelPageActivitiesCardRefs = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard),
})
.merge(activitiesCardRefSchema)
const hotelPageSpaPageRefs = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.SpaPage),
})
.merge(spaPageRefSchema)
const hotelPageBlockRefsItem = z.discriminatedUnion("__typename", [
hotelPageActivitiesCardRefs,
hotelPageSpaPageRefs,
])
export const hotelPageRefsSchema = z.object({
hotel_page: z.object({
content: discriminatedUnionArray(hotelPageBlockRefsItem.options).nullable(),
faq: hotelFaqRefsSchema.nullable(),
system: systemSchema,
}),
trackingProps: z.object({
url: z.string(),
}),
})
export const hotelPageUrlsSchema = z
.object({
all_hotel_page: z.object({
items: z.array(
z
.object({
url: z.string(),
hotel_page_id: z.string(),
system: systemSchema,
})
.transform((data) => {
return {
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
hotelId: data.hotel_page_id,
}
})
),
}),
})
.transform(({ all_hotel_page }) => all_hotel_page.items)
export const batchedHotelPageUrlsSchema = z
.array(
z.object({
data: hotelPageUrlsSchema,
})
)
.transform((allItems) => {
return allItems.flatMap((item) => item.data)
})
export const hotelPageCountSchema = z
.object({
all_hotel_page: z.object({
total: z.number(),
}),
})
.transform(({ all_hotel_page }) => all_hotel_page.total)

View File

@@ -0,0 +1,53 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { notFound } from "@scandic-hotels/trpc/errors"
import { contentstackExtendedProcedureUID } from "@scandic-hotels/trpc/procedures"
import { router } from "../../.."
import { GetHotelPage } from "../../../graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "../../../graphql/request"
import { generateTag } from "../../../utils/generateTag"
import { hotelPageSchema } from "./output"
import type { GetHotelPageData } from "../../../types/hotelPage"
export const hotelPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
const getHotelPageCounter = createCounter(
"trpc.contentstack",
"hotelPage.get"
)
const metricsGetHotelPage = getHotelPageCounter.init({ lang, uid })
metricsGetHotelPage.start()
const response = await request<GetHotelPageData>(
GetHotelPage,
{
locale: lang,
uid,
},
{
key: generateTag(lang, uid),
ttl: "max",
}
)
if (!response.data) {
const notFoundError = notFound(response)
metricsGetHotelPage.noDataError()
throw notFoundError
}
const validatedHotelPage = hotelPageSchema.safeParse(response.data)
if (!validatedHotelPage.success) {
metricsGetHotelPage.validationError(validatedHotelPage.error)
return null
}
metricsGetHotelPage.success()
return validatedHotelPage.data.hotel_page
}),
})

View File

@@ -0,0 +1,99 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { GetHotelPageCount } from "../../../graphql/Query/HotelPage/HotelPageCount.graphql"
import { GetHotelPageUrls } from "../../../graphql/Query/HotelPage/HotelPageUrl.graphql"
import { request } from "../../../graphql/request"
import { batchedHotelPageUrlsSchema, hotelPageCountSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type {
GetHotelPageCountData,
GetHotelPageUrlsData,
} from "../../../types/hotelPage"
export async function getHotelPageCount(lang: Lang) {
const getHotelPageCountCounter = createCounter(
"trpc.contentstack",
"hotelPageCount.get"
)
const metricsGetHotelPageCount = getHotelPageCountCounter.init({ lang })
metricsGetHotelPageCount.start()
const response = await request<GetHotelPageCountData>(
GetHotelPageCount,
{
locale: lang,
},
{
key: `${lang}:hotel_page_count`,
ttl: "max",
}
)
if (!response.data) {
metricsGetHotelPageCount.noDataError()
return 0
}
const validatedResponse = hotelPageCountSchema.safeParse(response.data)
if (!validatedResponse.success) {
metricsGetHotelPageCount.validationError(validatedResponse.error)
return 0
}
metricsGetHotelPageCount.success()
return validatedResponse.data
}
export async function getHotelPageUrls(lang: Lang) {
const getHotelPageUrlsCounter = createCounter(
"trpc.contentstack",
"hotelPageUrls.get"
)
const metricsGetHotelPageUrls = getHotelPageUrlsCounter.init({ lang })
metricsGetHotelPageUrls.start()
const count = await getHotelPageCount(lang)
if (count === 0) {
return []
}
// Calculating the amount of requests needed to fetch all hotel pages.
// Contentstack has a limit of 100 items per request.
// So we need to make multiple requests to fetch urls to all hotel pages.
// The `batchRequest` function is not working here, because the arrayMerge is
// used for other purposes.
const amountOfRequests = Math.ceil(count / 100)
const requests = Array.from({ length: amountOfRequests }).map((_, i) => ({
document: GetHotelPageUrls,
variables: { locale: lang, skip: i * 100 },
cacheKey: `${lang}:hotel_page_urls_batch_${i}`,
}))
const batchedResponse = await Promise.all(
requests.map((req) =>
request<GetHotelPageUrlsData>(req.document, req.variables, {
key: req.cacheKey,
ttl: "max",
})
)
)
const validatedResponse =
batchedHotelPageUrlsSchema.safeParse(batchedResponse)
if (!validatedResponse.success) {
metricsGetHotelPageUrls.validationError(validatedResponse.error)
return []
}
metricsGetHotelPageUrls.success()
return validatedResponse.data
}