diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql new file mode 100644 index 000000000..88a03f0bf --- /dev/null +++ b/lib/graphql/Query/Header.graphql @@ -0,0 +1,66 @@ +#import "../Fragments/Refs/System.graphql" + +#import "../Fragments/PageLink/ContentPageLink.graphql" +#import "../Fragments/PageLink/LoyaltyPageLink.graphql" + +query GetHeader($locale: String!) { + all_header(limit: 1, locale: $locale) { + items { + top_link { + is_external_link + open_in_new_tab + page_link { + link_title + linkConnection { + edges { + node { + ...LoyaltyPageLink + ...ContentPageLink + } + } + } + } + external_link { + href + title + } + } + } + } +} + +query GetHeader2($locale: String!) { + all_header(limit: 1, locale: $locale) { + items { + top_link { + external_link { + href + title + } + is_external_link + open_in_new_tab + page_link { + link_title + linkConnection { + edges { + node { + ...LoyaltyPageLink + ...ContentPageLink + } + } + } + } + } + } + } +} + +query GetHeaderRef($locale: String!) { + all_header(limit: 1, locale: $locale) { + items { + system { + ...System + } + } + } +} diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index a7931fb18..5e7201fdc 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + import { Image } from "@/types/image" // Help me write this zod schema based on the type ContactConfig @@ -259,3 +261,79 @@ const validateFooterRefConfigSchema = z.object({ }) export type FooterRefDataRaw = z.infer + +const linkConnectionNodeSchema = z.object({ + edges: z.array( + z.object({ + node: z.object({ + system: z.object({ + uid: z.string(), + locale: z.nativeEnum(Lang), + }), + url: z.string(), + title: z.string(), + web: z.object({ + original_url: z.string().nullable().optional(), + }), + }), + }) + ), +}) + +const internalExternalLinkSchema = z.object({ + external_link: z.object({ + href: z.string(), + title: z.string(), + }), + is_external_link: z.boolean(), + open_in_new_tab: z.boolean(), + page_link: z.object({ + link_title: z.string(), + linkConnection: linkConnectionNodeSchema, + }), +}) + +export type InternalExternalLinkData = z.infer< + typeof internalExternalLinkSchema +> + +export const validateHeaderSchema = z.object({ + all_header: z.object({ + items: z.array( + z.object({ + top_link: internalExternalLinkSchema.optional(), + }) + ), + }), +}) + +export type HeaderDataRaw = z.infer + +export type InternalExternalLink = { + href: string + title: string + isExternal: boolean + openInNewTab: boolean +} + +export type HeaderData = Omit< + HeaderDataRaw["all_header"]["items"][0], + "top_link" +> & { + topLink: InternalExternalLink | null +} + +const validateHeaderRefSchema = z.object({ + all_header: z.object({ + items: z.array( + z.object({ + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }) + ), + }), +}) + +export type HeaderRefDataRaw = z.infer diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 262878260..03b32144e 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -9,6 +9,7 @@ import { GetCurrentHeader, GetCurrentHeaderRef, } from "@/lib/graphql/Query/CurrentHeader.graphql" +import { GetHeader, GetHeaderRef } from "@/lib/graphql/Query/Header.graphql" import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { contentstackBaseProcedure, router } from "@/server/trpc" @@ -23,10 +24,15 @@ import { CurrentHeaderRefDataRaw, FooterDataRaw, FooterRefDataRaw, + HeaderData, + HeaderDataRaw, + HeaderRefDataRaw, validateContactConfigSchema, validateCurrentHeaderConfigSchema, validateFooterConfigSchema, + validateHeaderSchema, } from "./output" +import { makeLinkObjectFromInternalExternalLink } from "./utils" const meter = metrics.getMeter("trpc.contentstack.base") // OpenTelemetry metrics: ContactConfig @@ -39,7 +45,7 @@ const getContactConfigSuccessCounter = meter.createCounter( const getContactConfigFailCounter = meter.createCounter( "trpc.contentstack.contactConfig.get-fail" ) -// OpenTelemetry metrics: Header +// OpenTelemetry metrics: CurrentHeader const getCurrentHeaderRefCounter = meter.createCounter( "trpc.contentstack.currentHeader.ref.get" ) @@ -58,6 +64,24 @@ const getCurrentHeaderSuccessCounter = meter.createCounter( const getCurrentHeaderFailCounter = meter.createCounter( "trpc.contentstack.currentHeader.get-fail" ) + +// OpenTelemetry metrics: Header +const getHeaderRefCounter = meter.createCounter( + "trpc.contentstack.header.ref.get" +) +const getHeaderRefSuccessCounter = meter.createCounter( + "trpc.contentstack.header.ref.get-success" +) +const getHeaderRefFailCounter = meter.createCounter( + "trpc.contentstack.header.ref.get-fail" +) +const getHeaderCounter = meter.createCounter("trpc.contentstack.header.get") +const getHeaderSuccessCounter = meter.createCounter( + "trpc.contentstack.header.get-success" +) +const getHeaderFailCounter = meter.createCounter( + "trpc.contentstack.header.get-fail" +) // OpenTelemetry metrics: Footer const getFooterRefCounter = meter.createCounter( "trpc.contentstack.footer.ref.get" @@ -140,6 +164,84 @@ export const baseQueryRouter = router({ ) return validatedContactConfigConfig.data.all_contact_config.items[0] }), + header: contentstackBaseProcedure.query(async ({ ctx }) => { + const { lang } = ctx + getHeaderRefCounter.add(1, { lang }) + console.info( + "contentstack.header.ref start", + JSON.stringify({ query: { lang } }) + ) + + // TODO: Add better ref types and error handling for responseRef + const responseRef = await request(GetHeaderRef, { + locale: lang, + }) + + getCurrentHeaderCounter.add(1, { lang }) + console.info( + "contentstack.header start", + JSON.stringify({ query: { lang } }) + ) + + const response = await request( + GetHeader, + { locale: lang }, + { + tags: [ + generateTag(lang, responseRef.data.all_header.items[0].system.uid), + ], + } + ) + + if (!response.data) { + const notFoundError = notFound(response) + getHeaderFailCounter.add(1, { + lang, + error_type: "not_found", + error: JSON.stringify({ code: notFoundError.code }), + }) + console.error( + "contentstack.header not found error", + JSON.stringify({ + query: { lang }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + const validatedHeaderConfig = validateHeaderSchema.safeParse(response.data) + + if (!validatedHeaderConfig.success) { + getHeaderFailCounter.add(1, { + lang, + error_type: "validation_error", + error: JSON.stringify(validatedHeaderConfig.error), + }) + console.error( + "contentstack.header validation error", + JSON.stringify({ + query: { lang }, + error: validatedHeaderConfig.error, + }) + ) + return null + } + getHeaderSuccessCounter.add(1, { lang }) + console.info( + "contentstack.header success", + JSON.stringify({ query: { lang } }) + ) + + const data = validatedHeaderConfig.data.all_header.items[0] + const topLink = makeLinkObjectFromInternalExternalLink(data.top_link) + + const headerData: HeaderData = { topLink } + + console.log({ headerData }) + + return headerData + }), currentHeader: contentstackBaseProcedure .input(langInput) .query(async ({ input }) => { diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts new file mode 100644 index 000000000..46782933a --- /dev/null +++ b/server/routers/contentstack/base/utils.ts @@ -0,0 +1,31 @@ +import { removeMultipleSlashes } from "@/utils/url" + +import { InternalExternalLink, InternalExternalLinkData } from "./output" + +export function makeLinkObjectFromInternalExternalLink( + data: InternalExternalLinkData | undefined +): InternalExternalLink | null { + if (!data) { + return null + } + + const linkConnectionNode = + data.page_link.linkConnection.edges[0]?.node || null + const isExternalLink = data.is_external_link && data.external_link.href + const isOriginalLink = linkConnectionNode?.web?.original_url + const externalLink = data.external_link + const href = isExternalLink + ? externalLink.href + : linkConnectionNode?.web?.original_url || + removeMultipleSlashes( + `/${linkConnectionNode.system.locale}/${linkConnectionNode.url}` + ) + const title = isExternalLink ? externalLink.title : data.page_link.link_title + + return { + openInNewTab: data.open_in_new_tab, + title, + href, + isExternal: !!(isExternalLink || isOriginalLink), + } +}