From de60febd47c4950640b6b192dc1cc51bf47af37b Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 27 Aug 2024 14:44:15 +0200 Subject: [PATCH] feat(SW-186): implemented queries and typings for card inside header query --- lib/graphql/Query/Header.graphql | 11 + server/routers/contentstack/base/output.ts | 272 +++++++++++------- server/routers/contentstack/base/query.ts | 54 +--- server/routers/contentstack/base/utils.ts | 32 --- .../contentstack/schemas/imageVault.ts | 27 ++ types/components/header/header.ts | 0 types/header.ts | 10 + 7 files changed, 220 insertions(+), 186 deletions(-) delete mode 100644 server/routers/contentstack/base/utils.ts create mode 100644 types/components/header/header.ts create mode 100644 types/header.ts diff --git a/lib/graphql/Query/Header.graphql b/lib/graphql/Query/Header.graphql index fdb079ff2..3e42192be 100644 --- a/lib/graphql/Query/Header.graphql +++ b/lib/graphql/Query/Header.graphql @@ -1,6 +1,10 @@ #import "../Fragments/Refs/System.graphql" #import "../Fragments/Header/InternalOrExternalLink.graphql" +#import "../Fragments/PageLink/ContentPageLink.graphql" +#import "../Fragments/PageLink/LoyaltyPageLink.graphql" +#import "../Fragments/PageLink/AccountPageLink.graphql" +#import "../Fragments/Blocks/Card.graphql" query GetHeader($locale: String!) { all_header(limit: 1, locale: $locale) { @@ -22,6 +26,13 @@ query GetHeader($locale: String!) { ...InternalOrExternalLink } } + cardConnection { + edges { + node { + ...CardBlock + } + } + } } } } diff --git a/server/routers/contentstack/base/output.ts b/server/routers/contentstack/base/output.ts index 52a7131b9..8399dd078 100644 --- a/server/routers/contentstack/base/output.ts +++ b/server/routers/contentstack/base/output.ts @@ -2,6 +2,10 @@ import { z } from "zod" import { Lang } from "@/constants/languages" +import { removeMultipleSlashes } from "@/utils/url" + +import { imageVaultAssetTransformedSchema } from "../schemas/imageVault" + import { Image } from "@/types/image" // Help me write this zod schema based on the type ContactConfig @@ -262,120 +266,176 @@ 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 -> - -const cardButtonSchema = z.object({ - cta_text: z.string(), - external_link: z.object({ - href: z.string(), - title: z.string(), - }), - is_contentstack_link: z.boolean(), - linkConnection: linkConnectionNodeSchema, - open_in_new_tab: z.boolean(), -}) - -const menuItemSchema = z.object({ - title: z.string(), - link: internalExternalLinkSchema, - submenu: z.array( - z.object({ - title: z.string(), - links: z.array(internalExternalLinkSchema), - }) - ), - see_all_link: internalExternalLinkSchema, - // cardConnection: z.array( - // z.object({ - // node: z.object({ - // title: z.string(), - // heading: z.string(), - // body_text: z.string(), - // background_image: z.any(), // TODO: Any for now, should be Image - // has_primary_button: z.boolean(), - // has_secondary_button: z.boolean(), - // scripted_top_title: z.string(), - // primary_button: cardButtonSchema, - // secondary_button: cardButtonSchema, - // }), - // }) - // ), -}) - -export const validateHeaderSchema = z.object({ - all_header: z.object({ - items: z.array( +const linkConnectionNodeSchema = z + .object({ + edges: z.array( z.object({ - top_link: internalExternalLinkSchema.optional(), - menu_items: z.array(menuItemSchema), + 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(), + }), + }), }) ), - }), -}) + }) + .transform((rawData) => { + const node = rawData.edges[0]?.node + if (!node) { + return null + } + const url = node.url + const originalUrl = node.web?.original_url + const lang = node.system.locale + return { originalUrl, url: removeMultipleSlashes(`/${lang}/${url}`) } + }) -export type HeaderDataRaw = z.infer +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, + }), + }) + .transform((rawData) => { + if (!rawData) { + return null + } -export type InternalExternalLink = { - href: string - title: string - isExternal: boolean - openInNewTab: boolean -} + const linkConnectionData = rawData.page_link.linkConnection + const isExternalLink = + rawData.is_external_link && rawData.external_link.href + const isOriginalLink = linkConnectionData?.originalUrl + const externalLink = rawData.external_link + const href = + isExternalLink || !linkConnectionData + ? externalLink.href + : linkConnectionData.originalUrl || linkConnectionData.url + const title = isExternalLink + ? externalLink.title + : rawData.page_link.link_title -export type SubmenuItem = { - title: string - links: InternalExternalLink[] -} + return { + openInNewTab: rawData.open_in_new_tab, + title, + href, + isExternal: !!(isExternalLink || isOriginalLink), + } + }) -export type MenuItem = { - title: string - link: InternalExternalLink | null - seeAllLink: InternalExternalLink | null - submenu: SubmenuItem[] -} +const cardButtonSchema = z + .object({ + cta_text: z.string(), + external_link: z.object({ + href: z.string(), + title: z.string(), + }), + is_contentstack_link: z.boolean(), + linkConnection: linkConnectionNodeSchema, + open_in_new_tab: z.boolean(), + }) + .transform((rawData) => { + if (!rawData) { + return null + } -export type HeaderData = Omit< - HeaderDataRaw["all_header"]["items"][0], - "top_link" | "menu_items" -> & { - topLink: InternalExternalLink | null - menuItems: MenuItem[] -} + const linkConnectionData = rawData.linkConnection + const isExternalLink = !rawData.is_contentstack_link + const externalLink = rawData.external_link + const href = + isExternalLink || !linkConnectionData + ? externalLink.href + : linkConnectionData.originalUrl || linkConnectionData.url + const title = isExternalLink ? externalLink.title : rawData.cta_text -const validateHeaderRefSchema = z.object({ + return { + openInNewTab: rawData.open_in_new_tab, + title, + href, + isExternal: !!(isExternalLink || linkConnectionData?.originalUrl), + } + }) + +const cardSchema = z + .object({ + heading: z.string(), + body_text: z.string(), + background_image: imageVaultAssetTransformedSchema, + has_primary_button: z.boolean(), + has_secondary_button: z.boolean(), + scripted_top_title: z.string(), + primary_button: cardButtonSchema, + secondary_button: cardButtonSchema, + }) + .transform((rawData) => { + return { + scriptedTopTitle: rawData.scripted_top_title, + heading: rawData.heading, + bodyText: rawData.body_text, + backgroundImage: rawData.background_image, + primaryButton: rawData.has_primary_button ? rawData.primary_button : null, + secondaryButton: rawData.has_secondary_button + ? rawData.secondary_button + : null, + } + }) + +const menuItemSchema = z + .object({ + title: z.string(), + link: internalExternalLinkSchema, + submenu: z.array( + z.object({ + title: z.string(), + links: z.array(internalExternalLinkSchema), + }) + ), + see_all_link: internalExternalLinkSchema, + cardConnection: z.object({ + edges: z.array(z.object({ node: cardSchema })), + }), + }) + .transform((rawData) => { + return { + link: rawData.submenu.length ? null : rawData.link, + seeAllLink: rawData.submenu.length ? rawData.see_all_link : null, + submenu: rawData.submenu, + card: rawData.cardConnection.edges[0]?.node, + } + }) + +export const getHeaderSchema = z + .object({ + all_header: z.object({ + items: z.array( + z.object({ + top_link: internalExternalLinkSchema.optional(), + menu_items: z.array(menuItemSchema), + }) + ), + }), + }) + .transform((rawData) => { + const { top_link, menu_items } = rawData.all_header.items[0] + + return { + topLink: top_link, + menuItems: menu_items, + } + }) + +export const getHeaderRefSchema = z.object({ all_header: z.object({ items: z.array( z.object({ @@ -387,5 +447,3 @@ const validateHeaderRefSchema = z.object({ ), }), }) - -export type HeaderRefDataRaw = z.infer diff --git a/server/routers/contentstack/base/query.ts b/server/routers/contentstack/base/query.ts index 0c5f5a9ce..feaf01e15 100644 --- a/server/routers/contentstack/base/query.ts +++ b/server/routers/contentstack/base/query.ts @@ -24,18 +24,13 @@ import { CurrentHeaderRefDataRaw, FooterDataRaw, FooterRefDataRaw, - HeaderData, - HeaderDataRaw, - HeaderRefDataRaw, - InternalExternalLink, - MenuItem, - SubmenuItem, + getHeaderSchema, validateContactConfigSchema, validateCurrentHeaderConfigSchema, validateFooterConfigSchema, - validateHeaderSchema, } from "./output" -import { makeLinkObjectFromInternalExternalLink } from "./utils" + +import { HeaderRefResponse, HeaderResponse } from "@/types/header" const meter = metrics.getMeter("trpc.contentstack.base") // OpenTelemetry metrics: ContactConfig @@ -176,7 +171,7 @@ export const baseQueryRouter = router({ ) // TODO: Add better ref types and error handling for responseRef - const responseRef = await request(GetHeaderRef, { + const responseRef = await request(GetHeaderRef, { locale: lang, }) @@ -186,7 +181,7 @@ export const baseQueryRouter = router({ JSON.stringify({ query: { lang } }) ) - const response = await request( + const response = await request( GetHeader, { locale: lang }, { @@ -213,7 +208,7 @@ export const baseQueryRouter = router({ throw notFoundError } - const validatedHeaderConfig = validateHeaderSchema.safeParse(response.data) + const validatedHeaderConfig = getHeaderSchema.safeParse(response.data) if (!validatedHeaderConfig.success) { getHeaderFailCounter.add(1, { @@ -236,42 +231,7 @@ export const baseQueryRouter = router({ JSON.stringify({ query: { lang } }) ) - const data = validatedHeaderConfig.data.all_header.items[0] - const topLink = makeLinkObjectFromInternalExternalLink(data.top_link) - const menuItems: MenuItem[] = data.menu_items.map((menuItem) => { - let link = null - let seeAllLink = null - let submenu: SubmenuItem[] = [] - - if (!menuItem.submenu.length) { - link = !menuItem.submenu.length - ? makeLinkObjectFromInternalExternalLink(menuItem.link) - : null - seeAllLink = makeLinkObjectFromInternalExternalLink( - menuItem.see_all_link - ) - } else { - submenu = menuItem.submenu.map(({ title, links }) => ({ - title: title, - links: links.map( - (link) => - makeLinkObjectFromInternalExternalLink( - link - ) as InternalExternalLink - ), - })) - } - return { - title: menuItem.title, - link, - seeAllLink, - submenu, - } - }) - - const headerData: HeaderData = { topLink, menuItems } - - return headerData + return validatedHeaderConfig.data }), currentHeader: contentstackBaseProcedure .input(langInput) diff --git a/server/routers/contentstack/base/utils.ts b/server/routers/contentstack/base/utils.ts deleted file mode 100644 index eb89c7aa2..000000000 --- a/server/routers/contentstack/base/utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -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 || !linkConnectionNode - ? 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), - } -} diff --git a/server/routers/contentstack/schemas/imageVault.ts b/server/routers/contentstack/schemas/imageVault.ts index 87d76c9b0..633beb6fa 100644 --- a/server/routers/contentstack/schemas/imageVault.ts +++ b/server/routers/contentstack/schemas/imageVault.ts @@ -93,3 +93,30 @@ export const imageVaultAssetSchema = z.object({ */ AddedBy: z.string(), }) + +export const imageVaultAssetTransformedSchema = imageVaultAssetSchema.transform( + (rawData) => { + const alt = rawData.Metadata?.find((meta) => + meta.Name.includes("AltText_") + )?.Value + + const caption = rawData.Metadata?.find((meta) => + meta.Name.includes("Title_") + )?.Value + + return { + url: rawData.MediaConversions[0].Url, + id: rawData.Id, + meta: { + alt, + caption, + }, + title: rawData.Name, + dimensions: { + width: rawData.MediaConversions[0].Width, + height: rawData.MediaConversions[0].Height, + aspectRatio: rawData.MediaConversions[0].FormatAspectRatio, + }, + } + } +) diff --git a/types/components/header/header.ts b/types/components/header/header.ts new file mode 100644 index 000000000..e69de29bb diff --git a/types/header.ts b/types/header.ts new file mode 100644 index 000000000..c2468bf4d --- /dev/null +++ b/types/header.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +import { + getHeaderRefSchema, + getHeaderSchema, +} from "@/server/routers/contentstack/base/output" + +export type HeaderRefResponse = z.input +export type HeaderResponse = z.input +export type Header = z.output