Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-17 11:09:33 +02:00
175 changed files with 4058 additions and 1119 deletions

View File

@@ -2,6 +2,10 @@ import { z } from "zod"
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
import {
accordionRefsSchema,
accordionSchema,
} from "../schemas/blocks/accordion"
import {
cardGridRefsSchema,
cardsGridSchema,
@@ -81,8 +85,14 @@ export const contentPageTable = z
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Table),
})
.merge(tableSchema)
export const contentPageAccordion = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Accordion),
})
.merge(accordionSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
contentPageAccordion,
contentPageCards,
contentPageContent,
contentPageDynamicContent,
@@ -158,6 +168,12 @@ export const contentPageSchema = z.object({
}),
})
export const contentPageSchemaBlocks = z.object({
content_page: z.object({
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
}),
})
/** REFS */
const contentPageCardsRefs = z
.object({
@@ -195,7 +211,14 @@ const contentPageUspGridRefs = z
})
.merge(uspGridRefsSchema)
const contentPageAccordionRefs = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Accordion),
})
.merge(accordionRefsSchema)
const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [
contentPageAccordionRefs,
contentPageBlockContentRefs,
contentPageShortcutsRefs,
contentPageCardsRefs,

View File

@@ -1,7 +1,8 @@
import { Lang } from "@/constants/languages"
import {
GetContentPage,
GetContentPageBlocks,
GetContentPageBlocksBatch1,
GetContentPageBlocksBatch2,
} from "@/lib/graphql/Query/ContentPage/ContentPage.graphql"
import { request } from "@/lib/graphql/request"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
@@ -25,6 +26,7 @@ export const contentPageQueryRouter = router({
const { lang, uid } = ctx
const contentPageRefsData = await fetchContentPageRefs(lang, uid)
const contentPageRefs = validateContentPageRefs(
contentPageRefsData,
lang,
@@ -33,6 +35,7 @@ export const contentPageQueryRouter = router({
if (!contentPageRefs) {
return null
}
const tags = generatePageTags(contentPageRefs, lang)
getContentPageCounter.add(1, { lang, uid })
@@ -43,7 +46,7 @@ export const contentPageQueryRouter = router({
})
)
const [mainResponse, blocksResponse] = await Promise.all([
const [mainResponse, blocksResponse1, blocksResponse2] = await Promise.all([
request<GetContentPageSchema>(
GetContentPage,
{ locale: lang, uid },
@@ -55,7 +58,17 @@ export const contentPageQueryRouter = router({
}
),
request<GetContentPageSchema>(
GetContentPageBlocks,
GetContentPageBlocksBatch1,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags,
},
}
),
request<GetContentPageSchema>(
GetContentPageBlocksBatch2,
{ locale: lang, uid },
{
cache: "force-cache",
@@ -70,7 +83,12 @@ export const contentPageQueryRouter = router({
...mainResponse.data,
content_page: {
...mainResponse.data.content_page,
blocks: blocksResponse.data.content_page.blocks,
blocks: [
blocksResponse1.data.content_page.blocks,
blocksResponse2.data.content_page.blocks,
]
.flat(2)
.filter((obj) => !(obj && Object.keys(obj).length < 2)), // Remove empty objects and objects with only typename
},
}

View File

@@ -126,6 +126,12 @@ export function getConnections({ content_page }: ContentPageRefs) {
if (content_page.blocks) {
content_page.blocks.forEach((block) => {
switch (block.__typename) {
case ContentPageEnum.ContentStack.blocks.Accordion: {
if (block.accordion.length) {
connections.push(...block.accordion)
}
break
}
case ContentPageEnum.ContentStack.blocks.Content:
{
if (block.content.length) {

View File

@@ -2,7 +2,12 @@ import { z } from "zod"
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
import { activitiesCard } from "../schemas/blocks/activitiesCard"
import {
activitiesCardRefSchema,
activitiesCardSchema,
} from "../schemas/blocks/activitiesCard"
import { hotelFaqRefsSchema, hotelFaqSchema } from "../schemas/blocks/hotelFaq"
import { systemSchema } from "../schemas/system"
import { HotelPageEnum } from "@/types/enums/hotelPage"
@@ -10,7 +15,7 @@ const contentBlockActivities = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard),
})
.merge(activitiesCard)
.merge(activitiesCardSchema)
export const contentBlock = z.discriminatedUnion("__typename", [
contentBlockActivities,
@@ -19,9 +24,35 @@ export const contentBlock = z.discriminatedUnion("__typename", [
export const hotelPageSchema = z.object({
hotel_page: z.object({
content: discriminatedUnionArray(contentBlock.options).nullable(),
faq: hotelFaqSchema,
hotel_page_id: z.string(),
title: z.string(),
url: z.string(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
})
/** REFS */
const hotelPageActiviesCardRefs = z
.object({
__typename: z.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard),
})
.merge(activitiesCardRefSchema)
const hotelPageBlockRefsItem = z.discriminatedUnion("__typename", [
hotelPageActiviesCardRefs,
])
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(),

View File

@@ -0,0 +1,142 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { GetHotelPageRefs } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import { hotelPageRefsSchema } from "./output"
import { HotelPageEnum } from "@/types/enums/hotelPage"
import { System } from "@/types/requests/system"
import {
GetHotelPageRefsSchema,
HotelPageRefs,
} from "@/types/trpc/routers/contentstack/hotelPage"
const meter = metrics.getMeter("trpc.hotelPage")
// OpenTelemetry metrics: HotelPage
export const getHotelPageCounter = meter.createCounter(
"trpc.contentstack.hotelPage.get"
)
const getHotelPageRefsCounter = meter.createCounter(
"trpc.contentstack.hotelPage.get"
)
const getHotelPageRefsFailCounter = meter.createCounter(
"trpc.contentstack.hotelPage.get-fail"
)
const getHotelPageRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.hotelPage.get-success"
)
export async function fetchHotelPageRefs(lang: Lang, uid: string) {
getHotelPageRefsCounter.add(1, { lang, uid })
console.info(
"contentstack.hotelPage.refs start",
JSON.stringify({
query: { lang, uid },
})
)
const refsResponse = await request<GetHotelPageRefsSchema>(
GetHotelPageRefs,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
getHotelPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "http_error",
error: JSON.stringify({
code: notFoundError.code,
}),
})
console.error(
"contentstack.hotelPage.refs not found error",
JSON.stringify({
query: {
lang,
uid,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
return refsResponse.data
}
export function validateHotelPageRefs(
data: GetHotelPageRefsSchema,
lang: Lang,
uid: string
) {
const validatedData = hotelPageRefsSchema.safeParse(data)
if (!validatedData.success) {
getHotelPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "validation_error",
error: JSON.stringify(validatedData.error),
})
console.error(
"contentstack.hotelPage.refs validation error",
JSON.stringify({
query: { lang, uid },
error: validatedData.error,
})
)
return null
}
getHotelPageRefsSuccessCounter.add(1, { lang, uid })
console.info(
"contentstack.hotelPage.refs success",
JSON.stringify({
query: { lang, uid },
})
)
return validatedData.data
}
export function generatePageTags(
validatedData: HotelPageRefs,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.hotel_page.system.uid),
].flat()
}
export function getConnections({ hotel_page }: HotelPageRefs) {
const connections: System["system"][] = [hotel_page.system]
if (hotel_page.content) {
hotel_page.content.forEach((block) => {
switch (block.__typename) {
case HotelPageEnum.ContentStack.blocks.ActivitiesCard: {
if (block.upcoming_activities_card.length) {
connections.push(...block.upcoming_activities_card)
}
break
}
}
if (hotel_page.faq) {
connections.push(...hotel_page.faq)
}
})
}
return connections
}

View File

@@ -97,6 +97,8 @@ const getAllCachedApiRewards = unstable_cache(
},
})
)
throw apiResponse
}
const data = await apiResponse.json()
@@ -114,7 +116,7 @@ const getAllCachedApiRewards = unstable_cache(
error: validatedApiTierRewards.error,
})
)
return null
throw validatedApiTierRewards.error
}
return validatedApiTierRewards.data

View File

@@ -0,0 +1,188 @@
import { z } from "zod"
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
export const accordionItemsSchema = z.array(
z.object({
question: z.string(),
answer: z.object({
json: z.any(), // JSON
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
pageLinks.accountPageSchema,
pageLinks.contentPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
])
.transform((data) => {
const link = pageLinks.transform(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
})
)
export type Accordion = z.infer<typeof accordionSchema>
enum AccordionEnum {
ContentPageBlocksAccordionBlockAccordionsGlobalAccordion = "ContentPageBlocksAccordionBlockAccordionsGlobalAccordion",
ContentPageBlocksAccordionBlockAccordionsSpecificAccordion = "ContentPageBlocksAccordionBlockAccordionsSpecificAccordion",
}
export const accordionSchema = z.object({
typename: z
.literal(BlocksEnums.block.Accordion)
.optional()
.default(BlocksEnums.block.Accordion),
accordion: z
.object({
title: z.string().optional().default(""),
accordions: z.array(
z.object({
__typename: z.enum([
AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion,
AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion,
]),
global_accordion: z
.object({
global_accordionConnection: z.object({
edges: z.array(
z.object({
node: z.object({
questions: accordionItemsSchema,
}),
})
),
}),
})
.optional(),
specific_accordion: z
.object({
questions: accordionItemsSchema,
})
.optional(),
})
),
})
.transform((data) => {
return {
...data,
accordions: data.accordions.flatMap((acc) => {
switch (acc.__typename) {
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
return (
acc.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
return accordionConnection.questions
}
) || []
)
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
return acc.specific_accordion?.questions || []
}
}),
}
}),
})
const actualRefs = z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.loyaltyPageRefSchema,
])
export const globalAccordionConnectionRefs = z.object({
edges: z.array(
z.object({
node: z.object({
questions: z.array(
z.object({
answer: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: actualRefs,
})
),
}),
}),
})
),
}),
})
),
})
export const specificAccordionConnectionRefs = z.object({
questions: z.array(
z.object({
answer: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: actualRefs,
})
),
}),
}),
})
),
})
export const accordionRefsSchema = z.object({
accordion: z
.object({
accordions: z.array(
z.object({
__typename: z.enum([
AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion,
AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion,
]),
global_accordion: z
.object({
global_accordionConnection: globalAccordionConnectionRefs,
})
.optional(),
specific_accordion: specificAccordionConnectionRefs.optional(),
})
),
})
.transform((data) => {
return data.accordions.flatMap((accordion) => {
switch (accordion.__typename) {
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
return (
accordion.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
return accordionConnection.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
)
}
) || []
)
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
return (
accordion.specific_accordion?.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
) || []
)
}
})
}),
})

View File

@@ -8,7 +8,7 @@ import { tempImageVaultAssetSchema } from "../imageVault"
import { HotelPageEnum } from "@/types/enums/hotelPage"
export const activitiesCard = z.object({
export const activitiesCardSchema = z.object({
typename: z
.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard)
.optional()
@@ -57,3 +57,25 @@ export const activitiesCard = z.object({
}
}),
})
export const activitiesCardRefSchema = z.object({
upcoming_activities_card: z
.object({
hotel_page_activities_content_pageConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.contentPageRefSchema,
]),
})
),
}),
})
.transform((data) => {
return (
data.hotel_page_activities_content_pageConnection.edges.flatMap(
({ node }) => node.system
) || []
)
}),
})

View File

@@ -0,0 +1,75 @@
import { z } from "zod"
import {
accordionItemsSchema,
globalAccordionConnectionRefs,
specificAccordionConnectionRefs,
} from "./accordion"
import { BlocksEnums } from "@/types/enums/blocks"
import { HotelPageEnum } from "@/types/enums/hotelPage"
export const hotelFaqSchema = z
.object({
typename: z
.literal(BlocksEnums.block.Accordion)
.optional()
.default(BlocksEnums.block.Accordion),
title: z.string().optional().default(""),
global_faqConnection: z
.object({
edges: z.array(
z.object({
node: z.object({
questions: accordionItemsSchema,
}),
})
),
})
.optional(),
specific_faq: z
.object({
questions: accordionItemsSchema,
})
.optional(),
})
.transform((data) => {
const array = []
array.push(
data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => {
return faqConnection.questions
}) || []
)
array.push(data.specific_faq?.questions || [])
return { ...data, accordions: array.flat(2) }
})
export const hotelFaqRefsSchema = z
.object({
__typename: z
.literal(HotelPageEnum.ContentStack.blocks.Faq)
.optional()
.default(HotelPageEnum.ContentStack.blocks.Faq),
global_faqConnection: globalAccordionConnectionRefs.optional(),
specific_faq: specificAccordionConnectionRefs.optional(),
})
.transform((data) => {
const array = []
array.push(
data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => {
return faqConnection.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
)
}) || []
)
array.push(
data.specific_faq?.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
) || []
)
return array.flat(2)
})

View File

@@ -1,5 +1,6 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "@/lib/graphql/request"
@@ -18,6 +19,12 @@ import {
import { toApiLang } from "@/server/utils"
import { hotelPageSchema } from "../contentstack/hotelPage/output"
import {
fetchHotelPageRefs,
generatePageTags,
getHotelPageCounter,
validateHotelPageRefs,
} from "../contentstack/hotelPage/utils"
import {
getHotelInputSchema,
getHotelsAvailabilityInputSchema,
@@ -70,14 +77,38 @@ const roomsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.rooms-fail"
)
async function getContentstackData(
locale: string,
uid: string | null | undefined
) {
const response = await request<GetHotelPageData>(GetHotelPage, {
locale,
uid,
})
async function getContentstackData(lang: Lang, uid?: string | null) {
if (!uid) {
return null
}
const contentPageRefsData = await fetchHotelPageRefs(lang, uid)
const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid)
if (!contentPageRefs) {
return null
}
const tags = generatePageTags(contentPageRefs, lang)
getHotelPageCounter.add(1, { lang, uid })
console.info(
"contentstack.hotelPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetHotelPageData>(
GetHotelPage,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags,
},
}
)
if (!response.data) {
throw notFound(response)
@@ -86,7 +117,7 @@ async function getContentstackData(
const hotelPageData = hotelPageSchema.safeParse(response.data)
if (!hotelPageData.success) {
console.error(
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${locale})`
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})`
)
console.error(hotelPageData.error)
return null
@@ -101,6 +132,7 @@ export const hotelQueryRouter = router({
.query(async ({ ctx, input }) => {
const { lang, uid } = ctx
const { include } = input
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
@@ -230,6 +262,7 @@ export const hotelQueryRouter = router({
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
faq: contentstackData?.faq,
}
}),
availability: router({

View File

@@ -1,4 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
@@ -9,7 +10,9 @@ import type { FriendTransaction, Stay } from "./output"
const meter = metrics.getMeter("trpc.user")
const getProfileCounter = meter.createCounter("trpc.user.profile")
const getProfileSuccessCounter = meter.createCounter("trpc.user.profile-success")
const getProfileSuccessCounter = meter.createCounter(
"trpc.user.profile-success"
)
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
async function updateStaysBookingUrl(
@@ -41,7 +44,7 @@ async function updateStaysBookingUrl(
// Temporary Url, domain and lang support for current web
const bookingUrl = new URL(
"/hotelreservation/my-booking",
env.PUBLIC_URL ?? ""
env.PUBLIC_URL || "https://www.scandichotels.com" // fallback to production for ephemeral envs (like deploy previews)
)
switch (lang) {
case Lang.sv:
@@ -83,10 +86,7 @@ async function updateStaysBookingUrl(
}
getProfileSuccessCounter.add(1)
console.info(
"api.user.profile updatebookingurl success",
JSON.stringify({})
)
console.info("api.user.profile updatebookingurl success", JSON.stringify({}))
return data.map((d) => {
const originalString =
@@ -98,10 +98,7 @@ async function updateStaysBookingUrl(
bookingUrl.searchParams.set("RefId", encryptedBookingValue)
} else {
bookingUrl.searchParams.set("lastName", apiJson.data.attributes.lastName)
bookingUrl.searchParams.set(
"bookingId",
d.attributes.confirmationNumber
)
bookingUrl.searchParams.set("bookingId", d.attributes.confirmationNumber)
}
return {
...d,
@@ -113,4 +110,4 @@ async function updateStaysBookingUrl(
})
}
export { updateStaysBookingUrl }
export { updateStaysBookingUrl }