diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 6ff5d1a762..03bed729ed 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -1,7 +1,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { bookingResponsesSchema, - seatedBookingResponsesSchema, + seatedBookingDataSchema, } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; @@ -340,7 +340,7 @@ export class InputBookingsService_2024_08_13 { throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`); } - const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data); + const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data); const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email); if (!attendee) { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index a3e5b6a7b6..7dcf2774a6 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -1,4 +1,11 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { + defaultBookingMetadata, + defaultBookingResponses, + defaultSeatedBookingData, + defaultSeatedBookingMetadata, +} from "@/lib/safe-parse/default-responses-booking"; +import { safeParse } from "@/lib/safe-parse/safe-parse"; import { Injectable } from "@nestjs/common"; import { plainToClass } from "class-transformer"; import { DateTime } from "luxon"; @@ -30,9 +37,10 @@ export const bookingResponsesSchema = z guests: z.array(z.string()).optional(), rescheduleReason: z.string().optional(), }) - .passthrough(); + .passthrough() + .describe("BookingResponses"); -export const seatedBookingResponsesSchema = z +export const seatedBookingDataSchema = z .object({ responses: z .object({ @@ -47,7 +55,10 @@ export const seatedBookingResponsesSchema = z }) .passthrough(), }) - .passthrough(); + .passthrough() + .describe("SeatedBookingData"); + +const seatedBookingMetadataSchema = z.object({}).catchall(z.string()).describe("SeatedBookingMetadata"); type DatabaseUser = { id: number; name: string | null; email: string; username: string | null }; @@ -72,8 +83,6 @@ type BookingWithUser = Booking & { user: DatabaseUser | null }; type DatabaseMetadata = z.infer; -const seatedBookingMetadataSchema = z.object({}).catchall(z.string()); - @Injectable() export class OutputBookingsService_2024_08_13 { constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {} @@ -82,8 +91,12 @@ export class OutputBookingsService_2024_08_13 { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; - const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); - const metadata = bookingMetadataSchema.parse(databaseBooking.metadata); + const bookingResponses = safeParse( + bookingResponsesSchema, + databaseBooking.responses, + defaultBookingResponses + ); + const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata); const location = metadata?.videoCallUrl || databaseBooking.location; const booking = { @@ -167,8 +180,12 @@ export class OutputBookingsService_2024_08_13 { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; - const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); - const metadata = bookingMetadataSchema.parse(databaseBooking.metadata); + const bookingResponses = safeParse( + bookingResponsesSchema, + databaseBooking.responses, + defaultBookingResponses + ); + const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata); const location = metadata?.videoCallUrl || databaseBooking.location; const booking = { @@ -225,7 +242,7 @@ export class OutputBookingsService_2024_08_13 { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; - const metadata = bookingMetadataSchema.parse(databaseBooking.metadata); + const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata); const location = metadata?.videoCallUrl || databaseBooking.location; const booking = { @@ -254,7 +271,11 @@ export class OutputBookingsService_2024_08_13 { // note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually parsed.attendees = databaseBooking.attendees.map((attendee) => { - const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data); + const { responses } = safeParse( + seatedBookingDataSchema, + attendee.bookingSeat?.data, + defaultSeatedBookingData + ); const attendeeData = { name: attendee.name, @@ -267,7 +288,11 @@ export class OutputBookingsService_2024_08_13 { }; const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" }); attendeeParsed.bookingFieldsResponses = responses || {}; - attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata); + attendeeParsed.metadata = safeParse( + seatedBookingMetadataSchema, + attendee.bookingSeat?.metadata, + defaultSeatedBookingMetadata + ); // note(Lauris): as of now email is not returned for privacy delete attendeeParsed.bookingFieldsResponses.email; @@ -320,7 +345,7 @@ export class OutputBookingsService_2024_08_13 { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; - const metadata = bookingMetadataSchema.parse(databaseBooking.metadata); + const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata); const location = metadata?.videoCallUrl || databaseBooking.location; const booking = { @@ -353,7 +378,11 @@ export class OutputBookingsService_2024_08_13 { // note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually parsed.attendees = databaseBooking.attendees.map((attendee) => { - const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data); + const { responses } = safeParse( + seatedBookingDataSchema, + attendee.bookingSeat?.data, + defaultSeatedBookingData + ); const attendeeData = { name: attendee.name, @@ -366,7 +395,11 @@ export class OutputBookingsService_2024_08_13 { }; const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" }); attendeeParsed.bookingFieldsResponses = responses || {}; - attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata); + attendeeParsed.metadata = safeParse( + seatedBookingMetadataSchema, + attendee.bookingSeat?.metadata, + defaultSeatedBookingMetadata + ); // note(Lauris): as of now email is not returned for privacy delete attendeeParsed.bookingFieldsResponses.email; return attendeeParsed; diff --git a/apps/api/v2/src/lib/safe-parse/default-responses-booking.ts b/apps/api/v2/src/lib/safe-parse/default-responses-booking.ts new file mode 100644 index 0000000000..fa06643b6b --- /dev/null +++ b/apps/api/v2/src/lib/safe-parse/default-responses-booking.ts @@ -0,0 +1,12 @@ +export const defaultBookingResponses = { + name: "unknown", + email: "unknown", + guests: [], + rescheduleReason: "unknown", +}; + +export const defaultBookingMetadata = { videoCallUrl: "unknown" }; + +export const defaultSeatedBookingData = { responses: { name: "unknown", email: "unknown" } }; + +export const defaultSeatedBookingMetadata = {}; diff --git a/apps/api/v2/src/lib/safe-parse/safe-parse.ts b/apps/api/v2/src/lib/safe-parse/safe-parse.ts new file mode 100644 index 0000000000..198cb22e08 --- /dev/null +++ b/apps/api/v2/src/lib/safe-parse/safe-parse.ts @@ -0,0 +1,23 @@ +import { Logger } from "@nestjs/common"; +import { ZodSchema } from "zod"; + +const logger = new Logger("safeParse"); + +export function safeParse(schema: ZodSchema, value: unknown, defaultValue: T): T { + const result = schema.safeParse(value); + if (result.success) { + return result.data; + } else { + const errorStack = new Error().stack; + + logger.error( + `Zod parsing failed.\n` + + `1. Schema: ${schema.description || "UnnamedSchema"}\n` + + `2. Input: ${JSON.stringify(value, null, 2)}\n` + + `3. Zod Error: ${result.error}\n` + + `4. Call Stack: ${errorStack}` + ); + + return defaultValue; + } +} diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index f5268dca8c..000492f319 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -453,7 +453,8 @@ export const bookingMetadataSchema = z videoCallUrl: z.string().optional(), }) .and(z.record(z.string())) - .nullable(); + .nullable() + .describe("BookingMetadata"); export const customInputOptionSchema = z.array( z.object({