feat: loosen up the zod validations and return null instead of throwing

This commit is contained in:
Simon Emanuelsson
2024-06-07 10:36:23 +02:00
parent 5c50ac060d
commit aca9221ea6
89 changed files with 1117 additions and 821 deletions

View File

@@ -3,7 +3,7 @@ import {
GetAccountPageRefs,
} from "@/lib/graphql/Query/AccountPage.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import {
@@ -45,15 +45,14 @@ export const accountPageQueryRouter = router({
throw notFound(refsResponse)
}
// Remove empty objects from a fetched content type. Needed since
// Contentstack returns empty objects for all non queried blocks in modular blocks.
// This is an ongoing support case in Contentstack, ticker number #00031579
const cleanedData = removeEmptyObjects(refsResponse.data)
const validatedAccountPageRefs =
validateAccountPageRefsSchema.safeParse(cleanedData)
if (!validatedAccountPageRefs.success) {
throw internalServerError(validatedAccountPageRefs.error)
console.info(`Failed to validate My Page Refs - (uid: ${uid})`)
console.error(validatedAccountPageRefs.error)
return null
}
const connections = getConnections(validatedAccountPageRefs.data)
@@ -81,7 +80,9 @@ export const accountPageQueryRouter = router({
)
if (!validatedAccountPage.success) {
throw internalServerError(validatedAccountPage.error)
console.info(`Failed to validate Account Page - (uid: ${uid})`)
console.error(validatedAccountPage.error)
return null
}
// TODO: Make returned data nicer

View File

@@ -8,12 +8,8 @@ import {
GetCurrentHeaderRef,
} from "@/lib/graphql/Query/CurrentHeader.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import {
contentstackBaseProcedure,
publicProcedure,
router,
} from "@/server/trpc"
import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
@@ -47,12 +43,16 @@ export const baseQueryRouter = router({
)
if (!validatedContactConfigConfig.success) {
throw internalServerError(validatedContactConfigConfig.error)
console.info(
`Failed to validate Contact Config Data - (lang: ${ctx.lang})`
)
console.error(validatedContactConfigConfig.error)
return null
}
return validatedContactConfigConfig.data.all_contact_config.items[0]
}),
header: publicProcedure.input(langInput).query(async ({ input }) => {
header: contentstackBaseProcedure.input(langInput).query(async ({ input }) => {
const responseRef = await request<HeaderRefDataRaw>(GetCurrentHeaderRef, {
locale: input.lang,
})
@@ -79,7 +79,9 @@ export const baseQueryRouter = router({
)
if (!validatedHeaderConfig.success) {
throw internalServerError(validatedHeaderConfig.error)
console.info(`Failed to validate Header - (lang: ${input.lang})`)
console.error(validatedHeaderConfig.error)
return null
}
const logo =
@@ -91,7 +93,7 @@ export const baseQueryRouter = router({
logo,
} as HeaderData
}),
footer: publicProcedure.input(langInput).query(async ({ input }) => {
footer: contentstackBaseProcedure.input(langInput).query(async ({ input }) => {
const responseRef = await request<FooterRefDataRaw>(GetCurrentFooterRef, {
locale: input.lang,
})
@@ -116,7 +118,9 @@ export const baseQueryRouter = router({
)
if (!validatedFooterConfig.success) {
throw internalServerError(validatedFooterConfig.error)
console.info(`Failed to validate Footer - (lang: ${input.lang})`)
console.error(validatedFooterConfig.error)
return null
}
return validatedFooterConfig.data.all_current_footer.items[0]

View File

@@ -6,7 +6,6 @@ import {
GetMyPagesBreadcrumbs,
GetMyPagesBreadcrumbsRefs,
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
import { internalServerError } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import {
@@ -41,7 +40,11 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
)
if (!validatedRefsData.success) {
throw internalServerError(validatedRefsData.error)
console.info(
`Failed to validate Loyaltypage Breadcrumbs Refs - (url: ${variables.url})`
)
console.error(validatedRefsData.error)
return null
}
const tags = getTags(validatedRefsData.data.all_loyalty_page, variables)
@@ -53,14 +56,18 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
)
if (!response.data.all_loyalty_page.items[0].web?.breadcrumbs?.title) {
return []
return null
}
const validatedBreadcrumbsData =
validateLoyaltyPageBreadcrumbsContentstackSchema.safeParse(response.data)
if (!validatedBreadcrumbsData.success) {
throw internalServerError(validatedBreadcrumbsData.error)
console.info(
`Failed to validate Loyaltypage Breadcrumbs Data - (url: ${variables.url})`
)
console.error(validatedBreadcrumbsData.error)
return null
}
return getBreadcrumbs(
@@ -80,7 +87,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
refsResponse.data
)
if (!validatedRefsData.success) {
throw internalServerError(validatedRefsData.error)
console.info(
`Failed to validate My Page Breadcrumbs Refs - (url: ${variables.url})`
)
console.error(validatedRefsData.error)
return null
}
const tags = getTags(validatedRefsData.data.all_account_page, variables)
@@ -99,7 +110,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
validateMyPagesBreadcrumbsContentstackSchema.safeParse(response.data)
if (!validatedBreadcrumbsData.success) {
throw internalServerError(validatedBreadcrumbsData.error)
console.info(
`Failed to validate My Page Breadcrumbs Data - (url: ${variables.url})`
)
console.error(validatedBreadcrumbsData.error)
return null
}
return getBreadcrumbs(

View File

@@ -119,7 +119,11 @@ export const languageSwitcherQueryRouter = router({
validateLanguageSwitcherData.safeParse(urls)
if (!validatedLanguageSwitcherData.success) {
throw internalServerError(validatedLanguageSwitcherData.error)
console.info(
`Failed to validate Language Switcher Data - (contentType: ${ctx.contentType}, lang: ${ctx.lang}, uid: ${ctx.uid})`
)
console.error(validatedLanguageSwitcherData.error)
return null
}
return {

View File

@@ -3,7 +3,7 @@ import {
GetLoyaltyPageRefs,
} from "@/lib/graphql/Query/LoyaltyPage.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import {
@@ -35,9 +35,9 @@ function makeButtonObject(button: any) {
href:
button.is_contentstack_link && button.linkConnection.edges.length
? button.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
)
removeMultipleSlashes(
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
)
: button.external_link.href,
isExternal: !button.is_contentstack_link,
}
@@ -62,17 +62,16 @@ export const loyaltyPageQueryRouter = router({
throw notFound(refsResponse)
}
// Remove empty objects from a fetched content type. Needed since
// Contentstack returns empty objects for all non queried blocks in modular blocks.
// This is an ongoing support case in Contentstack, ticker number #00031579
const cleanedData = removeEmptyObjects(refsResponse.data)
const validatedLoyaltyPageRefs =
validateLoyaltyPageRefsSchema.safeParse(cleanedData)
if (!validatedLoyaltyPageRefs.success) {
console.error("Bad validation for `GetLoyaltyPageRefs`")
console.info(
`Failed to validate Loyaltypage Refs - (lang: ${lang}, uid: ${uid})`
)
console.error(validatedLoyaltyPageRefs.error)
throw internalServerError(validatedLoyaltyPageRefs.error)
return null
}
const connections = getConnections(validatedLoyaltyPageRefs.data)
@@ -97,66 +96,66 @@ export const loyaltyPageQueryRouter = router({
const blocks = response.data.loyalty_page.blocks
? response.data.loyalty_page.blocks.map((block: any) => {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.edges.length
? {
text: block.dynamic_content.link.text,
href: removeMultipleSlashes(
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
),
title:
block.dynamic_content.link.pageConnection.edges[0]
.node.title,
}
: undefined,
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
return {
...block,
shortcuts: {
...block.shortcuts,
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
text: shortcut.text,
openInNewTab: shortcut.open_in_new_tab,
...shortcut.linkConnection.edges[0].node,
url:
shortcut.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
),
})),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return {
...block,
cards_grid: {
...block.cards_grid,
cards: block.cards_grid.cardConnection.edges.map(
({ node: card }: { node: any }) => {
return {
...card,
primaryButton: card.has_primary_button
? makeButtonObject(card.primary_button)
: undefined,
secondaryButton: card.has_secondary_button
? makeButtonObject(card.secondary_button)
: undefined,
}
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
return {
...block,
dynamic_content: {
...block.dynamic_content,
link: block.dynamic_content.link.pageConnection.edges.length
? {
text: block.dynamic_content.link.text,
href: removeMultipleSlashes(
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
),
title:
block.dynamic_content.link.pageConnection.edges[0]
.node.title,
}
: undefined,
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
return {
...block,
shortcuts: {
...block.shortcuts,
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
text: shortcut.text,
openInNewTab: shortcut.open_in_new_tab,
...shortcut.linkConnection.edges[0].node,
url:
shortcut.linkConnection.edges[0].node.web?.original_url ||
removeMultipleSlashes(
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
),
})),
},
}
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return {
...block,
cards_grid: {
...block.cards_grid,
cards: block.cards_grid.cardConnection.edges.map(
({ node: card }: { node: any }) => {
return {
...card,
primaryButton: card.has_primary_button
? makeButtonObject(card.primary_button)
: undefined,
secondaryButton: card.has_secondary_button
? makeButtonObject(card.secondary_button)
: undefined,
}
),
},
}
default:
return block
}
})
}
),
},
}
default:
return block
}
})
: null
const loyaltyPage = {
@@ -170,7 +169,11 @@ export const loyaltyPageQueryRouter = router({
validateLoyaltyPageSchema.safeParse(loyaltyPage)
if (!validatedLoyaltyPage.success) {
throw internalServerError(validatedLoyaltyPage.error)
console.info(
`Failed to validate Loyaltypage Data - (lang: ${lang}, uid: ${uid})`
)
console.error(validatedLoyaltyPage.error)
return null
}
// Assert LoyaltyPage type to get correct typings for RTE fields

View File

@@ -3,7 +3,7 @@ import {
GetNavigationMyPagesRefs,
} from "@/lib/graphql/Query/NavigationMyPages.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
import {
@@ -71,7 +71,11 @@ export const navigationQueryRouter = router({
const validatedMyPagesNavigationRefs =
navigationRefsPayloadSchema.safeParse(refsResponse.data)
if (!validatedMyPagesNavigationRefs.success) {
throw internalServerError(validatedMyPagesNavigationRefs.error)
console.info(
`Failed to validate My Pages Navigation Refs - (lang: ${lang}`
)
console.error(validatedMyPagesNavigationRefs.error)
return null
}
const connections = getConnections(validatedMyPagesNavigationRefs.data)
@@ -99,7 +103,11 @@ export const navigationQueryRouter = router({
response.data
)
if (!validatedMyPagesNavigation.success) {
throw internalServerError(validatedMyPagesNavigation.error)
console.info(
`Failed to validate My Pages Navigation Data - (lang: ${lang}`
)
console.error(validatedMyPagesNavigation.error)
return null
}
const menuItem =
@@ -112,7 +120,11 @@ export const navigationQueryRouter = router({
const validatedNav = getNavigationSchema.safeParse(nav)
if (!validatedNav.success) {
throw internalServerError(validatedNav.error)
console.info(
`Failed to validate My Pages Navigation Return Data - (lang: ${lang}`
)
console.error(validatedNav.error)
return null
}
return validatedNav.data

View File

@@ -1,3 +0,0 @@
/**
* Add User mutations
*/

View File

@@ -104,17 +104,17 @@ export const getFriendTransactionsSchema = z.object({
data: z.array(
z.object({
attributes: z.object({
hotelOperaId: z.string(),
confirmationNumber: z.string(),
checkinDate: z.string(),
checkoutDate: z.string(),
nights: z.number(),
awardPoints: z.number(),
pointsCalculated: z.boolean(),
awardPoints: z.number().default(0),
checkinDate: z.string().default(""),
checkoutDate: z.string().default(""),
confirmationNumber: z.string().default(""),
hotelOperaId: z.string().default(""),
nights: z.number().default(1),
pointsCalculated: z.boolean().default(true),
hotelInformation: z
.object({
hotelName: z.string(),
city: z.string(),
city: z.string().default(""),
name: z.string().default(""),
hotelContent: z.object({
images: z.object({
metaData: z.object({
@@ -135,29 +135,28 @@ export const getFriendTransactionsSchema = z.object({
.optional(),
}),
relationships: z.object({
booking: z.object({
data: z.object({
id: z.string().default(""),
type: z.string().default(""),
}),
links: z.object({
related: z.string().default(""),
}),
}),
hotel: z
.object({
links: z.object({
related: z.string(),
}),
data: z.object({
id: z.string(),
type: z.string(),
id: z.string().default(""),
type: z.string().default(""),
}),
links: z.object({
related: z.string().default(""),
}),
})
.optional(),
booking: z.object({
links: z.object({
related: z.string(),
}),
data: z.object({
id: z.string(),
type: z.string(),
}),
}),
}),
type: z.string(),
type: z.string().default(""),
})
),
links: z
@@ -169,7 +168,3 @@ export const getFriendTransactionsSchema = z.object({
})
.nullable(),
})
type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
export type Transaction = GetFriendTransactionsData["data"][number]

View File

@@ -1,11 +1,4 @@
import * as api from "@/lib/api"
import {
badRequestError,
forbiddenError,
internalServerError,
notFound,
unauthorizedError,
} from "@/server/errors/trpc"
import { protectedProcedure, router } from "@/server/trpc"
import { friendTransactionsInput, staysInput } from "./input"
@@ -34,26 +27,36 @@ export const userQueryRouter = router({
})
if (!apiResponse.ok) {
switch (apiResponse.status) {
case 400:
throw badRequestError(apiResponse)
case 401:
throw unauthorizedError(apiResponse)
case 403:
throw forbiddenError(apiResponse)
default:
throw internalServerError(apiResponse)
}
// switch (apiResponse.status) {
// case 400:
// throw badRequestError(apiResponse)
// case 401:
// throw unauthorizedError(apiResponse)
// case 403:
// throw forbiddenError(apiResponse)
// default:
// throw internalServerError(apiResponse)
// }
console.info(`API Response Failed - Getting User`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
throw notFound(apiJson)
// throw notFound(apiJson)
console.error(
`User has no data - (user: ${JSON.stringify(ctx.session.user)})`
)
return null
}
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
if (!verifiedData.success) {
throw internalServerError(verifiedData.error)
console.info(`Failed to validate User - (name: ${ctx.session.user?.name}`)
console.error(verifiedData.error)
return null
}
return {
@@ -98,28 +101,35 @@ export const userQueryRouter = router({
)
if (!apiResponse.ok) {
switch (apiResponse.status) {
case 400:
throw badRequestError(apiResponse)
case 401:
throw unauthorizedError(apiResponse)
case 403:
throw forbiddenError(apiResponse)
default:
throw internalServerError(apiResponse)
}
// switch (apiResponse.status) {
// case 400:
// throw badRequestError(apiResponse)
// case 401:
// throw unauthorizedError(apiResponse)
// case 403:
// throw forbiddenError(apiResponse)
// default:
// throw internalServerError(apiResponse)
// }
console.info(`API Response Failed - Getting Previous Stays`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
throw internalServerError(verifiedData.error)
console.info(`Failed to validate Previous Stays Data`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
@@ -152,27 +162,34 @@ export const userQueryRouter = router({
)
if (!apiResponse.ok) {
switch (apiResponse.status) {
case 400:
throw badRequestError(apiResponse)
case 401:
throw unauthorizedError(apiResponse)
case 403:
throw forbiddenError(apiResponse)
default:
throw internalServerError(apiResponse)
}
// switch (apiResponse.status) {
// case 400:
// throw badRequestError(apiResponse)
// case 401:
// throw unauthorizedError(apiResponse)
// case 403:
// throw forbiddenError(apiResponse)
// default:
// throw internalServerError(apiResponse)
// }
console.info(`API Response Failed - Getting Upcoming Stays`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
throw internalServerError(verifiedData.error)
console.info(`Failed to validate Upcoming Stays Data`)
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
@@ -186,76 +203,70 @@ export const userQueryRouter = router({
friendTransactions: protectedProcedure
.input(friendTransactionsInput)
.query(async (opts) => {
try {
const { limit, cursor } = opts.input
const { limit, cursor } = opts.input
const params = new URLSearchParams()
params.set("limit", limit.toString())
const params = new URLSearchParams()
params.set("limit", limit.toString())
if (cursor) {
params.set("offset", cursor.toString())
}
if (cursor) {
params.set("offset", cursor.toString())
}
const apiResponse = await api.get(
api.endpoints.v1.friendTransactions,
{
headers: {
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
},
const apiResponse = await api.get(
api.endpoints.v1.friendTransactions,
{
headers: {
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
},
params
)
},
params
)
if (!apiResponse.ok) {
switch (apiResponse.status) {
case 400:
throw badRequestError()
case 401:
throw unauthorizedError()
case 403:
throw forbiddenError()
default:
throw internalServerError()
}
}
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError()
// case 401:
// throw unauthorizedError()
// case 403:
// throw forbiddenError()
// default:
// throw internalServerError()
// }
console.info(`API Response Failed - Getting Friend Transactions`)
console.info(`User: (${JSON.stringify(opts.ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
// const apiJson = friendTransactionsMockJson
const apiJson = await apiResponse.json()
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.info(`Failed to validate Friend Transactions Data`)
console.info(`User: (${JSON.stringify(opts.ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
if (!apiJson.data?.length) {
// throw internalServerError()
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.info(`Get Friend Transactions - Verified Data Error`)
console.error(verifiedData.error)
throw badRequestError()
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
return {
data: verifiedData.data.data.map(({ attributes }) => ({
return {
data: verifiedData.data.data.map(({ attributes }) => {
return {
awardPoints: attributes.awardPoints,
checkinDate: attributes.checkinDate,
checkoutDate: attributes.checkoutDate,
awardPoints: attributes.awardPoints,
hotelName: attributes.hotelInformation?.hotelName,
city: attributes.hotelInformation?.city,
nights: attributes.nights,
confirmationNumber: attributes.confirmationNumber,
})),
nextCursor,
}
} catch (error) {
console.info(`Get Friend Transactions Error`)
console.error(error)
throw internalServerError()
hotelName: attributes.hotelInformation?.name,
nights: attributes.nights,
}
}),
nextCursor,
}
}),
}),