diff --git a/lib/graphql/Query/LoyaltyPage.graphql b/lib/graphql/Query/LoyaltyPage.graphql index b71b62c71..cd8327fcb 100644 --- a/lib/graphql/Query/LoyaltyPage.graphql +++ b/lib/graphql/Query/LoyaltyPage.graphql @@ -3,6 +3,11 @@ #import "../Fragments/PageLink/ContentPageLink.graphql" #import "../Fragments/PageLink/LoyaltyPageLink.graphql" +#import "../Fragments/Refs/AccountPage.graphql" +#import "../Fragments/Refs/ContentPage.graphql" +#import "../Fragments/Refs/LoyaltyPage.graphql" +#import "../Fragments/Refs/System.graphql" + query GetLoyaltyPage($locale: String!, $url: String!) { all_loyalty_page(where: { url: $url }, locale: $locale) { items { @@ -145,3 +150,119 @@ query GetLoyaltyPage($locale: String!, $url: String!) { } } } + +query GetLoyaltyPageRefs($locale: String!, $url: String!) { + all_loyalty_page(where: { url: $url }, locale: $locale) { + items { + blocks { + ... on LoyaltyPageBlocksShortcuts { + __typename + shortcuts { + shortcuts { + linkConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + ... on LoyaltyPageBlocksDynamicContent { + __typename + dynamic_content { + link { + pageConnection { + edges { + node { + __typename + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + ... on LoyaltyPageBlocksCardGrid { + __typename + card_grid { + cards { + referenceConnection { + edges { + node { + __typename + ...AccountPageRef + ...ContentPageRef + ...LoyaltyPageRef + } + } + } + } + } + } + ... on LoyaltyPageBlocksContent { + __typename + content { + content { + embedded_itemsConnection { + edges { + node { + # No fragments used since we want to include __typename for each type to avoid fetching SystemAsset + ... on ContentPage { + __typename + system { + ...System + } + } + ... on LoyaltyPage { + __typename + system { + ...System + } + } + } + } + } + } + } + } + } + sidebar { + ... on LoyaltyPageSidebarContent { + __typename + content { + content { + embedded_itemsConnection { + edges { + node { + # No fragments used since we want to include __typename for each type to avoid fetching SystemAsset + ... on ContentPage { + __typename + system { + ...System + } + } + ... on LoyaltyPage { + __typename + system { + ...System + } + } + } + } + } + } + } + } + } + system { + ...System + } + } + } +} diff --git a/server/routers/contentstack/loyaltyPage/output.ts b/server/routers/contentstack/loyaltyPage/output.ts index 17f422477..8ccefb861 100644 --- a/server/routers/contentstack/loyaltyPage/output.ts +++ b/server/routers/contentstack/loyaltyPage/output.ts @@ -9,6 +9,7 @@ import { SidebarTypenameEnum, } from "@/types/components/loyalty/enums" import { Embeds } from "@/types/requests/embeds" +import { PageLinkEnum } from "@/types/requests/pageLinks" import { EdgesWithTotalCount } from "@/types/requests/utils/edges" import { RTEDocument } from "@/types/rte/node" @@ -262,3 +263,99 @@ export type LoyaltyPage = Omit & { blocks: Block[] sidebar: Sidebar[] } + +// Refs types +const pageConnectionRefs = z.object({ + edges: z.array( + z.object({ + node: z.object({ + __typename: z.nativeEnum(PageLinkEnum), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }), + }) + ), +}) + +const loyaltyPageBlockCardGridRefs = z.object({ + __typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid), + card_grid: z.object({ + cards: z.array( + z.object({ + referenceConnection: pageConnectionRefs, + }) + ), + }), +}) + +const loyaltyPageDynamicContentRefs = z.object({ + __typename: z.literal( + LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent + ), + dynamic_content: z.object({ + link: z.object({ + pageConnection: pageConnectionRefs, + }), + }), +}) + +const loyaltyPageShortcutsRefs = z.object({ + __typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts), + shortcuts: z.object({ + shortcuts: z.array( + z.object({ + linkConnection: pageConnectionRefs, + }) + ), + }), +}) + +const loyaltyPageBlockTextContentRefs = z.object({ + __typename: z.literal(LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: pageConnectionRefs, + }), + }), +}) + +const loyaltyPageBlocRefsItem = z.discriminatedUnion("__typename", [ + loyaltyPageBlockCardGridRefs, + loyaltyPageDynamicContentRefs, + loyaltyPageBlockTextContentRefs, + loyaltyPageShortcutsRefs, +]) + +const loyaltyPageSidebarTextContentRef = z.object({ + __typename: z.literal(SidebarTypenameEnum.LoyaltyPageSidebarContent), + content: z.object({ + content: z.object({ + embedded_itemsConnection: pageConnectionRefs, + }), + }), +}) + +const loyaltyPageSidebarRefsItem = z.discriminatedUnion("__typename", [ + loyaltyPageSidebarTextContentRef, +]) + +export const validateLoyaltyPageRefsSchema = z.object({ + all_loyalty_page: z.object({ + items: z.array( + z.object({ + blocks: z.array(loyaltyPageBlocRefsItem).nullable(), + sidebar: z.array(loyaltyPageSidebarRefsItem).nullable(), + system: z.object({ + content_type_uid: z.string(), + uid: z.string(), + }), + }) + ), + }), +}) + +export type LoyaltyPageRefsDataRaw = z.infer< + typeof validateLoyaltyPageRefsSchema +> diff --git a/server/routers/contentstack/loyaltyPage/query.ts b/server/routers/contentstack/loyaltyPage/query.ts index 70c14ba2f..dc934783d 100644 --- a/server/routers/contentstack/loyaltyPage/query.ts +++ b/server/routers/contentstack/loyaltyPage/query.ts @@ -1,15 +1,28 @@ -import GetLoyaltyPage from "@/lib/graphql/Query/LoyaltyPage.graphql" +import { + GetLoyaltyPage, + GetLoyaltyPageRefs, +} from "@/lib/graphql/Query/LoyaltyPage.graphql" import { request } from "@/lib/graphql/request" import { _ } from "@/lib/translation" -import { badRequestError } from "@/server/errors/trpc" +import { badRequestError, internalServerError } from "@/server/errors/trpc" import { publicProcedure, router } from "@/server/trpc" +import { removeEmptyObjects } from "@/utils/contentType" +import { + generateRefsResponseTag, + generateTag, + generateTags, +} from "@/utils/generateTag" + import { getLoyaltyPageInput } from "./input" import { type LoyaltyPage, type LoyaltyPageDataRaw, + type LoyaltyPageRefsDataRaw, + validateLoyaltyPageRefsSchema, validateLoyaltyPageSchema, } from "./output" +import { getConnections } from "./utils" import { LoyaltyBlocksTypenameEnum, @@ -22,10 +35,56 @@ import { RTEDocument } from "@/types/rte/node" export const loyaltyPageQueryRouter = router({ get: publicProcedure.input(getLoyaltyPageInput).query(async ({ input }) => { try { - const loyaltyPageRes = await request(GetLoyaltyPage, { - locale: input.locale, - url: input.href, - }) + const { locale } = input + + const refsResponse = await request( + GetLoyaltyPageRefs, + { + locale, + url: input.href, + }, + { + next: { + tags: [generateRefsResponseTag(locale, "loyalty_page")], + }, + } + ) + + if (!refsResponse.data) { + console.error("Bad response for `GetNavigationMyPagesRefs`") + console.error({ refsResponse }) + throw internalServerError() + } + + const cleanedData = removeEmptyObjects(refsResponse.data) + + const validatedLoyaltyPageRefs = + validateLoyaltyPageRefsSchema.safeParse(cleanedData) + if (!validatedLoyaltyPageRefs.success) { + console.error("Bad validation for `GetLoyaltyPageRefs`") + console.error(validatedLoyaltyPageRefs.error) + throw badRequestError() + } + + const connections = getConnections(validatedLoyaltyPageRefs.data) + + const tags = generateTags(locale, connections) + + tags.push( + generateTag( + locale, + validatedLoyaltyPageRefs.data.all_loyalty_page.items[0].system.uid + ) + ) + + const loyaltyPageRes = await request( + GetLoyaltyPage, + { + locale, + url: input.href, + }, + { next: { tags } } + ) if (!loyaltyPageRes.data) { throw badRequestError() diff --git a/server/routers/contentstack/loyaltyPage/utils.ts b/server/routers/contentstack/loyaltyPage/utils.ts new file mode 100644 index 000000000..200b6c25d --- /dev/null +++ b/server/routers/contentstack/loyaltyPage/utils.ts @@ -0,0 +1,54 @@ +import { LoyaltyPageRefsDataRaw } from "./output" + +import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums" +import type { Edges } from "@/types/requests/utils/edges" +import type { NodeRefs } from "@/types/requests/utils/refs" + +export function getConnections(refs: LoyaltyPageRefsDataRaw) { + const connections: Edges[] = [] + refs.all_loyalty_page.items.forEach((ref) => { + if (ref.blocks) { + ref.blocks.forEach((item) => { + switch (item.__typename) { + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent: { + if (item.content.content.embedded_itemsConnection.edges.length) { + connections.push(item.content.content.embedded_itemsConnection) + } + break + } + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardGrid: { + item.card_grid.cards.forEach((card) => { + if (card.referenceConnection.edges.length) { + connections.push(card.referenceConnection) + } + }) + break + } + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts: { + item.shortcuts.shortcuts.forEach((shortcut) => { + if (shortcut.linkConnection.edges.length) { + connections.push(shortcut.linkConnection) + } + }) + break + } + case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent: { + if (item.dynamic_content.link.pageConnection.edges.length) { + connections.push(item.dynamic_content.link.pageConnection) + } + break + } + } + }) + } + if (ref.sidebar) { + ref.sidebar?.forEach((item) => { + if (item.content.content.embedded_itemsConnection.edges.length) { + connections.push(item.content.content.embedded_itemsConnection) + } + }) + } + }) + + return connections +} diff --git a/server/routers/contentstack/myPages/navigation/output.ts b/server/routers/contentstack/myPages/navigation/output.ts index b4641c327..9785700ca 100644 --- a/server/routers/contentstack/myPages/navigation/output.ts +++ b/server/routers/contentstack/myPages/navigation/output.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { Lang } from "@/constants/languages" -import { PageLinkEnum } from "@/types/requests/myPages/navigation" +import { PageLinkEnum } from "@/types/requests/pageLinks" const pageConnection = z.object({ edges: z.array( diff --git a/types/requests/myPages/navigation.ts b/types/requests/myPages/navigation.ts index 63728d0f2..ea1acfcdf 100644 --- a/types/requests/myPages/navigation.ts +++ b/types/requests/myPages/navigation.ts @@ -1,15 +1,11 @@ +import { PageLinkEnum } from "../pageLinks" + import type { Lang } from "@/constants/languages" import type { System } from "../system" import type { AllRequestResponse } from "../utils/all" import type { Edges } from "../utils/edges" import type { TypenameInterface } from "../utils/typename" -export enum PageLinkEnum { - AccountPage = "AccountPage", - ContentPage = "ContentPage", - LoyaltyPage = "LoyaltyPage", -} - export type MenuItem = { lang: Lang linkText: string diff --git a/types/requests/pageLinks.ts b/types/requests/pageLinks.ts new file mode 100644 index 000000000..1e40320f3 --- /dev/null +++ b/types/requests/pageLinks.ts @@ -0,0 +1,5 @@ +export enum PageLinkEnum { + AccountPage = "AccountPage", + ContentPage = "ContentPage", + LoyaltyPage = "LoyaltyPage", +} diff --git a/utils/contentType.ts b/utils/contentType.ts index 656c9731c..2093b1a7d 100644 --- a/utils/contentType.ts +++ b/utils/contentType.ts @@ -53,3 +53,26 @@ export async function getContentTypeByPathName( return PageTypeEnum.CurrentBlocksPage } } +/** + * Function to remove empty objects from a fetched content type. + * Used since Contentstack returns empty objects for all non + * queried in modular blocks. + */ +export function removeEmptyObjects(obj: T): T { + const copy = obj as any + + for (let key in copy) { + if (typeof copy[key] === "object" && copy[key] !== null) { + copy[key] = removeEmptyObjects(copy[key]) + if (Object.keys(copy[key]).length === 0 && !Array.isArray(copy[key])) { + delete copy[key] + } + } + } + + if (Array.isArray(copy)) { + return copy.filter((item) => item != null).map(removeEmptyObjects) as T + } + + return copy as T +}