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,20 @@
import type {
ContactConfig,
ContactFieldGroups,
} from "../routers/contentstack/base/output"
export function getValueFromContactConfig(
keyString: string,
data: ContactConfig
): string | undefined {
const [groupName, key] = keyString.split(".") as [
ContactFieldGroups,
keyof ContactConfig[ContactFieldGroups],
]
if (data[groupName]) {
const fieldGroup = data[groupName]
return fieldGroup[key]
}
return undefined
}

View File

@@ -0,0 +1,57 @@
import { z } from "zod"
import type { ZodDiscriminatedUnionOption, ZodError } from "zod"
export interface DiscriminatedUnionError {
error: ZodError<unknown>
}
export interface Option extends ZodDiscriminatedUnionOption<"__typename"> {}
/**
* This file is created to handle our discriminated unions
* validations primarily for union returns from Contentstacks
* GraphQL server.
*
* In the case of a new block being added to the union in Contenstack,
* Zod will throw because that typename is not expected ("invalid_union_discriminator").
* Therefore we add a safety that we return everytime we stumble upon
* the issue and then we filter it out in the transform.
*
* This replaces the `cleanEmptyObjects` function that would require
* everyone to never make a mistake in adding __typename to root
* anywhere (or any other potentially global fields in case the return type
* is an Interface e.g).
*/
export function discriminatedUnion<R>(options: Option[]) {
return z
.discriminatedUnion("__typename", [
z.object({ __typename: z.literal(undefined) }),
...options,
])
.catch(({ error }: DiscriminatedUnionError) => {
if (
error.issues.find(
(issue) => issue.code === "invalid_union_discriminator"
)
) {
return { __typename: undefined }
}
throw new Error(error.message)
})
.transform((data) => {
if (data.__typename === "undefined" || data.__typename === undefined) {
return null
}
return data as R
})
}
export function discriminatedUnionArray<T extends Option>(options: T[]) {
return z
.array(discriminatedUnion(options))
.transform((blocks) =>
blocks.filter((block) => !!block)
) as unknown as z.ZodEffects<z.ZodArray<T>>
}

View File

@@ -0,0 +1,61 @@
import { Lang } from "@scandic-hotels/common/constants/language"
import { batchRequest } from "../graphql/batchRequest"
import {
EntryByUrlBatch1,
EntryByUrlBatch2,
} from "../graphql/Query/ResolveEntry.graphql"
import { validateEntryResolveSchema } from "../types/entry"
export function resolveEntryCacheKey(lang: Lang, url: string) {
return `${lang}:${url}:resolveentry`
}
export async function resolve(url: string, lang = Lang.en) {
const variables = { locale: lang, url: url || "/" }
const cacheKey = resolveEntryCacheKey(variables.locale, variables.url)
// The maximum amount of content types you can query is 6, therefor more
// than that is being batched
const response = await batchRequest([
{
document: EntryByUrlBatch1,
variables,
cacheOptions: {
ttl: "max",
key: cacheKey,
},
},
{
document: EntryByUrlBatch2,
variables,
cacheOptions: {
ttl: "max",
key: cacheKey,
},
},
])
const validatedData = validateEntryResolveSchema.safeParse(response.data)
if (!validatedData.success) {
return {
error: validatedData.error,
}
}
for (const value of Object.values(validatedData.data)) {
if (value.total) {
const { content_type_uid, uid } = value.items[0].system
return {
contentType: content_type_uid,
uid,
}
}
}
return {
contentType: null,
uid: null,
}
}

View File

@@ -0,0 +1,113 @@
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { System } from "../routers/contentstack/schemas/system"
import type { Edges } from "../types/edges"
import type { NodeRefs } from "../types/refs"
/**
* Function to generate tag for initial refs request
*
* @param lang Lang
* @param identifier Should be uid for all pages and content_type_uid for
* everything else
* @param affix possible extra value to add to string, e.g lang:identifier:breadcrumbs:refs
* as it is the same entity as the actual page tag otherwise
* @returns string
*/
export function generateRefsResponseTag(
lang: Lang,
identifier: string,
affix?: string
) {
if (affix) {
return `${lang}:${identifier}:${affix}:refs`
}
return `${lang}:${identifier}:refs`
}
/**
* Function to generate all tags to references on entity
*
* @param lang Lang
* @param contentTypeUid content_type_uid of reference
* @param uid system.uid of reference
* @returns string
*/
export function generateRefTag(
lang: Lang,
contentTypeUid: string,
uid: string
) {
return `${lang}:ref:${contentTypeUid}:${uid}`
}
/**
* Function to generate tag for entity being requested
*
* @param lang Lang
* @param uid system.uid of entity
* @param affix possible extra value to add to string, e.g lang:uid:breadcrumbs
* as it is the same entity as the actual page tag otherwise
* @returns string
*/
export function generateTag(lang: Lang, uid: string, affix?: string | null) {
if (affix) {
return `${lang}:${uid}:${affix}`
}
return `${lang}:${uid}`
}
export function generateTags(lang: Lang, connections: Edges<NodeRefs>[]) {
return connections
.map((connection) => {
return connection.edges.map(({ node }) => {
return generateRefTag(
lang,
node.system.content_type_uid,
node.system.uid
)
})
})
.flat()
}
export function generateTagsFromSystem(
lang: Lang,
connections: System["system"][]
) {
return connections.map((system) => {
return generateRefTag(
system.locale ?? lang,
system.content_type_uid,
system.uid
)
})
}
/**
* Function to generate tags for loyalty configuration models
*
* @param lang Lang
* @param contentTypeUid content_type_uid of reference
* @param id system shared identifier, e.g reward_id, level_id
* @returns string
*/
export function generateLoyaltyConfigTag(
lang: Lang,
contentTypeUid: string,
id: string
) {
return `${lang}:loyalty_config:${contentTypeUid}:${id}`
}
/**
* Function to generate tags for hotel page urls
*
* @param lang Lang
* @param hotelId hotelId of reference
* @returns string
*/
export function generateHotelUrlTag(lang: Lang, hotelId: string) {
return `${lang}:hotel_page_url:${hotelId}`
}

View File

@@ -0,0 +1,34 @@
import { SortOption } from "../enums/destinationFilterAndSort"
import type { DestinationCityListItem } from "../types/destinationCityPage"
const CITY_SORTING_STRATEGIES: Partial<
Record<
SortOption,
(a: DestinationCityListItem, b: DestinationCityListItem) => number
>
> = {
[SortOption.Name]: function (a, b) {
return a.cityName.localeCompare(b.cityName)
},
[SortOption.Recommended]: function (a, b) {
if (a.sort_order === null && b.sort_order === null) {
return a.cityName.localeCompare(b.cityName)
}
if (a.sort_order === null) {
return 1
}
if (b.sort_order === null) {
return -1
}
return b.sort_order - a.sort_order
},
}
export function getSortedCities(
cities: DestinationCityListItem[],
sortOption: SortOption
) {
const sortFn = CITY_SORTING_STRATEGIES[sortOption]
return sortFn ? cities.sort(sortFn) : cities
}

View File

@@ -0,0 +1,38 @@
import type {
ImageVaultAsset,
ImageVaultAssetResponse,
} from "../types/imageVault"
export function insertResponseToImageVaultAsset(
response: ImageVaultAssetResponse
): ImageVaultAsset {
const alt = response.Metadata?.find((meta) =>
meta.Name.includes("AltText_")
)?.Value
const caption = response.Metadata?.find((meta) =>
meta.Name.includes("Title_")
)?.Value
const mediaConversion = response.MediaConversions[0]
const aspectRatio =
mediaConversion.FormatAspectRatio ||
mediaConversion.AspectRatio ||
mediaConversion.Width / mediaConversion.Height
return {
url: mediaConversion.Url,
id: response.Id,
meta: {
alt,
caption,
},
title: response.Name,
dimensions: {
width: mediaConversion.Width,
height: mediaConversion.Height,
aspectRatio,
},
focalPoint: response.FocalPoint || { x: 50, y: 50 },
}
}

View File

@@ -0,0 +1,15 @@
import { AvailabilityEnum } from "../enums/selectHotel"
import type { RoomConfiguration } from "../types/roomAvailability"
// Used to ensure `Available` rooms
// are shown before all `NotAvailable`
const statusLookup = {
[AvailabilityEnum.Available]: 1,
[AvailabilityEnum.NotAvailable]: 2,
}
export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
// @ts-expect-error - array indexing
return statusLookup[a.status] - statusLookup[b.status]
}