diff --git a/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql b/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql index 8e5d74f21..453fe405e 100644 --- a/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql +++ b/lib/graphql/Fragments/Blocks/Refs/Accordion.graphql @@ -1,4 +1,7 @@ +#import "../../AccountPage/Ref.graphql" #import "../../ContentPage/Ref.graphql" +#import "../../HotelPage/Ref.graphql" +#import "../../LoyaltyPage/Ref.graphql" fragment AccordionBlockRefs on Accordion { questions { @@ -7,7 +10,10 @@ fragment AccordionBlockRefs on Accordion { edges { node { __typename + ...AccountPageRef ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef } } } diff --git a/lib/graphql/Query/HotelPage/HotelPage.graphql b/lib/graphql/Query/HotelPage/HotelPage.graphql index e7a99fde8..d44e24f31 100644 --- a/lib/graphql/Query/HotelPage/HotelPage.graphql +++ b/lib/graphql/Query/HotelPage/HotelPage.graphql @@ -63,6 +63,60 @@ query GetHotelPage($locale: String!, $uid: String!) { } } } + system { + ...System + created_at + updated_at + } + } +} + +query GetHotelPageRefs($locale: String!, $uid: String!) { + hotel_page(locale: $locale, uid: $uid) { + faq { + global_faqConnection { + edges { + node { + ...AccordionBlockRefs + } + } + } + specific_faq { + questions { + answer { + embedded_itemsConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...HotelPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + } + content { + __typename + ... on HotelPageContentUpcomingActivitiesCard { + upcoming_activities_card { + hotel_page_activities_content_pageConnection { + edges { + node { + __typename + ...ContentPageRef + } + } + } + } + } + } + system { + ...System + } } } diff --git a/server/routers/contentstack/hotelPage/output.ts b/server/routers/contentstack/hotelPage/output.ts index e14f37d0c..7bf0ff474 100644 --- a/server/routers/contentstack/hotelPage/output.ts +++ b/server/routers/contentstack/hotelPage/output.ts @@ -2,8 +2,12 @@ import { z } from "zod" import { discriminatedUnionArray } from "@/lib/discriminatedUnion" -import { activitiesCardSchema } from "../schemas/blocks/activitiesCard" -import { accordionSchema } from "../schemas/blocks/faq" +import { + activitiesCardRefSchema, + activitiesCardSchema, +} from "../schemas/blocks/activitiesCard" +import { accordionRefsSchema,accordionSchema } from "../schemas/blocks/faq" +import { systemSchema } from "../schemas/system" import { HotelPageEnum } from "@/types/enums/hotelPage" @@ -24,5 +28,30 @@ export const hotelPageSchema = z.object({ 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: accordionRefsSchema.nullable(), + system: systemSchema, }), }) diff --git a/server/routers/contentstack/hotelPage/utils.ts b/server/routers/contentstack/hotelPage/utils.ts new file mode 100644 index 000000000..1b3a0adfe --- /dev/null +++ b/server/routers/contentstack/hotelPage/utils.ts @@ -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( + 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 +} diff --git a/server/routers/contentstack/schemas/blocks/activitiesCard.ts b/server/routers/contentstack/schemas/blocks/activitiesCard.ts index 0d4fba2b1..079414948 100644 --- a/server/routers/contentstack/schemas/blocks/activitiesCard.ts +++ b/server/routers/contentstack/schemas/blocks/activitiesCard.ts @@ -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 + ) || [] + ) + }), +}) diff --git a/server/routers/contentstack/schemas/blocks/faq.ts b/server/routers/contentstack/schemas/blocks/faq.ts index 9810fae9f..1a060dc5a 100644 --- a/server/routers/contentstack/schemas/blocks/faq.ts +++ b/server/routers/contentstack/schemas/blocks/faq.ts @@ -45,6 +45,10 @@ export const accordionSchema = z export const accordionRefsSchema = z .object({ + __typename: z + .literal(HotelPageEnum.ContentStack.blocks.Faq) + .optional() + .default(HotelPageEnum.ContentStack.blocks.Faq), global_faqConnection: globalFaqConnectionRefs.optional(), specific_faq: specificFaqConnectionRefs.optional(), }) @@ -66,5 +70,5 @@ export const accordionRefsSchema = z ) ) || [] ) - return { faq: array.flat(2) } + return array.flat(2) }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 1822e09e0..d22adb43c 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -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(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.contentPage start", + JSON.stringify({ + query: { lang, uid }, + }) + ) + const response = await request( + 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 diff --git a/types/trpc/routers/contentstack/hotelPage.ts b/types/trpc/routers/contentstack/hotelPage.ts index aa1c584c4..e157eaecf 100644 --- a/types/trpc/routers/contentstack/hotelPage.ts +++ b/types/trpc/routers/contentstack/hotelPage.ts @@ -2,14 +2,19 @@ import { z } from "zod" import { contentBlock, + hotelPageRefsSchema, hotelPageSchema, } from "@/server/routers/contentstack/hotelPage/output" import { activitiesCardSchema } from "@/server/routers/contentstack/schemas/blocks/activitiesCard" -// Will be extended once we introduce more functionality to our entries. export interface GetHotelPageData extends z.input {} export interface HotelPage extends z.output {} export interface ActivitiesCard extends z.output {} export type ActivityCard = ActivitiesCard["upcoming_activities_card"] export interface ContentBlock extends z.output {} + +export interface GetHotelPageRefsSchema + extends z.input {} + +export interface HotelPageRefs extends z.output {}