Skip to content

Commit

Permalink
fix: bookings parsing errors (#18200)
Browse files Browse the repository at this point in the history
  • Loading branch information
supalarry authored Dec 16, 2024
1 parent 1773aba commit 0932970
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
63 changes: 48 additions & 15 deletions apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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({
Expand All @@ -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 };

Expand All @@ -72,8 +83,6 @@ type BookingWithUser = Booking & { user: DatabaseUser | null };

type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;

const seatedBookingMetadataSchema = z.object({}).catchall(z.string());

@Injectable()
export class OutputBookingsService_2024_08_13 {
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions apps/api/v2/src/lib/safe-parse/default-responses-booking.ts
Original file line number Diff line number Diff line change
@@ -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 = {};
23 changes: 23 additions & 0 deletions apps/api/v2/src/lib/safe-parse/safe-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Logger } from "@nestjs/common";
import { ZodSchema } from "zod";

const logger = new Logger("safeParse");

export function safeParse<T>(schema: ZodSchema<T>, 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;
}
}
3 changes: 2 additions & 1 deletion packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down

0 comments on commit 0932970

Please sign in to comment.