From dd336ca4ab6b9b3ae7258cf8dcc82de4bee4b8c8 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Fri, 30 Aug 2024 14:31:27 +0200 Subject: [PATCH] feat(SW-285): add support for tags --- lib/graphql/Query/ContentPage.graphql | 22 +++ .../contentstack/contentPage/output.ts | 54 +++++++ .../routers/contentstack/contentPage/query.ts | 31 +++- .../routers/contentstack/contentPage/utils.ts | 132 ++++++++++++++++++ 4 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 server/routers/contentstack/contentPage/utils.ts diff --git a/lib/graphql/Query/ContentPage.graphql b/lib/graphql/Query/ContentPage.graphql index 47db78954..8202e89e1 100644 --- a/lib/graphql/Query/ContentPage.graphql +++ b/lib/graphql/Query/ContentPage.graphql @@ -2,6 +2,11 @@ #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" +#import "../Fragments/Refs/MyPages/AccountPage.graphql" +#import "../Fragments/Refs/ContentPage/ContentPage.graphql" +#import "../Fragments/Refs/LoyaltyPage/LoyaltyPage.graphql" +#import "../Fragments/Refs/System.graphql" + query GetContentPage($locale: String!, $uid: String!) { content_page(uid: $uid, locale: $locale) { blocks { @@ -106,6 +111,23 @@ query GetContentPageRefs($locale: String!, $uid: String!) { } } } + ... on ContentPageBlocksShortcuts { + __typename + shortcuts { + shortcuts { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } } system { ...System diff --git a/server/routers/contentstack/contentPage/output.ts b/server/routers/contentstack/contentPage/output.ts index 8b3f6a0ea..b99a873ff 100644 --- a/server/routers/contentstack/contentPage/output.ts +++ b/server/routers/contentstack/contentPage/output.ts @@ -7,6 +7,7 @@ import { imageVaultAssetSchema } from "../schemas/imageVault" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" import { ImageVaultAsset } from "@/types/components/imageVault" import { Embeds } from "@/types/requests/embeds" +import { RTEEmbedsEnum } from "@/types/requests/rte" import { EdgesWithTotalCount } from "@/types/requests/utils/edges" import { RTEDocument } from "@/types/rte/node" @@ -84,3 +85,56 @@ export type ContentPage = Omit & { heroImage?: ImageVaultAsset blocks: Block[] } + +const rteConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + __typename: z.nativeEnum(RTEEmbedsEnum), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ), +}) + +const contentPageBlockTextContentRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: rteConnectionRefs, + }), + }), +}) + +const contentPageShortcutsRefs = z.object({ + __typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksShortcuts), + shortcuts: z.object({ + shortcuts: z.array( + z.object({ + linkConnection: rteConnectionRefs, + }) + ), + }), +}) + +const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [ + contentPageBlockTextContentRefs, + contentPageShortcutsRefs, +]) + +export const validateContentPageRefsSchema = z.object({ + content_page: z.object({ + blocks: z.array(contentPageBlockRefsItem).nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), +}) + +export type ContentPageRefsDataRaw = z.infer< + typeof validateContentPageRefsSchema +> diff --git a/server/routers/contentstack/contentPage/query.ts b/server/routers/contentstack/contentPage/query.ts index e1215d38b..b473bb16c 100644 --- a/server/routers/contentstack/contentPage/query.ts +++ b/server/routers/contentstack/contentPage/query.ts @@ -4,7 +4,6 @@ import { request } from "@/lib/graphql/request" import { notFound } from "@/server/errors/trpc" import { contentstackExtendedProcedureUID, router } from "@/server/trpc" -import { generateTag } from "@/utils/generateTag" import { makeImageVaultImage } from "@/utils/imageVault" import { removeMultipleSlashes } from "@/utils/url" @@ -15,6 +14,12 @@ import { ContentPageDataRaw, validateContentPageSchema, } from "./output" +import { + fetchContentPageRefs, + generatePageTags, + getContentPageCounter, + validateContentPageRefs, +} from "./utils" import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" import { @@ -26,13 +31,31 @@ export const contentPageQueryRouter = router({ get: contentstackExtendedProcedureUID.query(async ({ ctx }) => { const { lang, uid } = ctx - // TODO: Refs request should be done when adding more data to this query - // which has references to other pages. + const cleanedRefsData = await fetchContentPageRefs(lang, uid) + const validatedRefsData = validateContentPageRefs( + cleanedRefsData, + lang, + uid + ) + const tags = generatePageTags(validatedRefsData, lang) + + getContentPageCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage start", + JSON.stringify({ + query: { lang, uid }, + }) + ) const response = await request( GetContentPage, { locale: lang, uid }, - { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } + { + cache: "force-cache", + next: { + tags, + }, + } ) const { content_page } = removeEmptyObjects(response.data) diff --git a/server/routers/contentstack/contentPage/utils.ts b/server/routers/contentstack/contentPage/utils.ts new file mode 100644 index 000000000..50c49e5e2 --- /dev/null +++ b/server/routers/contentstack/contentPage/utils.ts @@ -0,0 +1,132 @@ +import { metrics } from "@opentelemetry/api" + +import { Lang } from "@/constants/languages" +import { GetContentPageRefs } from "@/lib/graphql/Query/ContentPage.graphql" +import { request } from "@/lib/graphql/request" +import { notFound } from "@/server/errors/trpc" + +import { generateTag, generateTags } from "@/utils/generateTag" + +import { removeEmptyObjects } from "../../utils" +import { ContentPageRefsDataRaw, validateContentPageRefsSchema } from "./output" + +import { ContentBlocksTypenameEnum } from "@/types/components/content/enums" +import { Edges } from "@/types/requests/utils/edges" +import { NodeRefs } from "@/types/requests/utils/refs" + +const meter = metrics.getMeter("trpc.contentPage") +// OpenTelemetry metrics: ContentPage + +export const getContentPageCounter = meter.createCounter( + "trpc.contentstack.contentPage.get" +) + +const getContentPageRefsCounter = meter.createCounter( + "trpc.contentstack.contentPage.get" +) +const getContentPageRefsFailCounter = meter.createCounter( + "trpc.contentstack.contentPage.get-fail" +) +const getContentPageRefsSuccessCounter = meter.createCounter( + "trpc.contentstack.contentPage.get-success" +) + +export async function fetchContentPageRefs(lang: Lang, uid: string) { + getContentPageRefsCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage.refs start", + JSON.stringify({ + query: { lang, uid }, + }) + ) + const refsResponse = await request( + GetContentPageRefs, + { locale: lang, uid }, + { cache: "force-cache", next: { tags: [generateTag(lang, uid)] } } + ) + if (!refsResponse.data) { + const notFoundError = notFound(refsResponse) + getContentPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "http_error", + error: JSON.stringify({ + code: notFoundError.code, + }), + }) + console.error( + "contentstack.contentPage.refs not found error", + JSON.stringify({ + query: { + lang, + uid, + }, + error: { code: notFoundError.code }, + }) + ) + throw notFoundError + } + + return removeEmptyObjects(refsResponse.data) +} + +export function validateContentPageRefs(data: any, lang: Lang, uid: string) { + const validatedData = validateContentPageRefsSchema.safeParse(data) + if (!validatedData.success) { + getContentPageRefsFailCounter.add(1, { + lang, + uid, + error_type: "validation_error", + error: JSON.stringify(validatedData.error), + }) + console.error( + "contentstack.contentPage.refs validation error", + JSON.stringify({ + query: { lang, uid }, + error: validatedData.error, + }) + ) + return null + } + getContentPageRefsSuccessCounter.add(1, { lang, uid }) + console.info( + "contentstack.contentPage.refs success", + JSON.stringify({ + query: { lang, uid }, + }) + ) + return validatedData.data +} + +export function generatePageTags(validatedData: any, lang: Lang): string[] { + const connections = getConnections(validatedData) + return [ + generateTags(lang, connections), + generateTag(lang, validatedData.content_page.system.uid), + ].flat() +} + +export function getConnections(refs: ContentPageRefsDataRaw) { + const connections: Edges[] = [] + if (refs.content_page.blocks) { + refs.content_page.blocks.forEach((item) => { + switch (item.__typename) { + case ContentBlocksTypenameEnum.ContentPageBlocksContent: { + if (item.content.content.embedded_itemsConnection.edges.length) { + connections.push(item.content.content.embedded_itemsConnection) + } + break + } + case ContentBlocksTypenameEnum.ContentPageBlocksShortcuts: { + item.shortcuts.shortcuts.forEach((shortcut) => { + if (shortcut.linkConnection.edges.length) { + connections.push(shortcut.linkConnection) + } + }) + break + } + } + }) + } + return connections +}