Skip to content

Commit

Permalink
Improve validation and types
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara committed Mar 20, 2023
1 parent 98f36b3 commit 90443d1
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 51 deletions.
28 changes: 6 additions & 22 deletions apps/web/lib/getBooking.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { z } from "zod";

import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";

type BookingSelect = {
description: true;
Expand Down Expand Up @@ -45,11 +44,7 @@ function getResponsesFromOldBooking(
};
}

async function getBooking(
prisma: PrismaClient,
uid: string,
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
) {
async function getBooking(prisma: PrismaClient, uid: string) {
const rawBooking = await prisma.booking.findFirst({
where: {
uid,
Expand Down Expand Up @@ -82,9 +77,7 @@ async function getBooking(
return rawBooking;
}

const booking = getBookingWithResponses(rawBooking, {
bookingFields,
});
const booking = getBookingWithResponses(rawBooking);

if (booking) {
// @NOTE: had to do this because Server side cant return [Object objects]
Expand All @@ -104,20 +97,11 @@ export const getBookingWithResponses = <
};
}>
>(
booking: T,
eventType: {
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
}
booking: T
) => {
return {
...booking,
responses: getBookingResponsesPartialSchema({
eventType: {
bookingFields: eventType.bookingFields,
},
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
view: "ALL_VIEWS",
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
};
responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
} as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
};
export default getBooking;
3 changes: 1 addition & 2 deletions apps/web/pages/booking/[uid].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1087,8 +1087,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}

const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);

const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
Expand Down
21 changes: 20 additions & 1 deletion packages/features/bookings/lib/getBookingResponsesSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,28 @@ type EventType = Parameters<typeof preprocess>[0]["eventType"];
// eslint-disable-next-line @typescript-eslint/ban-types
type View = ALL_VIEWS | (string & {});

export const bookingResponse = z.union([
z.string(),
z.boolean(),
z.string().array(),
z.object({
optionValue: z.string(),
value: z.string(),
}),
]);

export const bookingResponsesDbSchema = z.record(bookingResponse);

const catchAllSchema = bookingResponsesDbSchema;

export const getBookingResponsesPartialSchema = ({
eventType,
view,
}: {
eventType: EventType;
view: View;
}) => {
const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
const schema = bookingResponses.unwrap().partial().and(catchAllSchema);

return preprocess({ schema, eventType, isPartialSchema: true, view });
};
Expand All @@ -38,6 +52,9 @@ function preprocess<T extends z.ZodType>({
view: currentView,
}: {
schema: T;
// It is useful when we want to prefill the responses with the partial values. Partial can be in 2 ways
// - Not all required fields are need to be provided for prefill.
// - Even a field response itself can be partial so the content isn't validated e.g. a field with type="phone" can be given a partial phone number(e.g. Specifying the country code like +91)
isPartialSchema: boolean;
eventType: {
bookingFields: (z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">) | null;
Expand All @@ -48,6 +65,7 @@ function preprocess<T extends z.ZodType>({
(responses) => {
const parsedResponses = z.record(z.any()).nullable().parse(responses) || {};
const newResponses = {} as typeof parsedResponses;
// if eventType has been deleted, we won't have bookingFields and thus we can't preprocess or validate them.
if (!eventType.bookingFields) return parsedResponses;
eventType.bookingFields.forEach((field) => {
const value = parsedResponses[field.name];
Expand Down Expand Up @@ -88,6 +106,7 @@ function preprocess<T extends z.ZodType>({
},
schema.superRefine((responses, ctx) => {
if (!eventType.bookingFields) {
// if eventType has been deleted, we won't have bookingFields and thus we can't validate the responses.
return;
}
eventType.bookingFields.forEach((bookingField) => {
Expand Down
6 changes: 3 additions & 3 deletions packages/features/bookings/lib/getCalEventResponses.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type z from "zod";

import { SystemField } from "@calcom/features/bookings/lib/getBookingFields";
import type { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";

Expand All @@ -12,7 +12,7 @@ export const getCalEventResponses = ({
// If the eventType has been deleted and a booking is Accepted later on, then bookingFields will be null and we can't know the label of fields. So, we should store the label as well in the DB
// Also, it is no longer straightforward to identify if a field is system field or not
bookingFields: z.infer<typeof eventTypeBookingFields> | null;
responses: z.infer<ReturnType<typeof getBookingResponsesPartialSchema>>;
responses: z.infer<typeof bookingResponsesDbSchema>;
}) => {
const calEventUserFieldsResponses = {} as NonNullable<CalendarEvent["userFieldsResponses"]>;
const calEventResponses = {} as NonNullable<CalendarEvent["responses"]>;
Expand All @@ -39,7 +39,7 @@ export const getCalEventResponses = ({
for (const [name, value] of Object.entries(responses)) {
const isSystemField = SystemField.safeParse(name);

// Use name for Label because we don't have access to the label.
// Use name for Label because we don't have access to the label. This will not be needed once we start storing the label along with the response
const label = name;

if (!isSystemField.success) {
Expand Down
8 changes: 7 additions & 1 deletion packages/lib/getLabelValueMapFromResponses.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type z from "zod";

import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { CalendarEvent } from "@calcom/types/Calendar";

export default function getLabelValueMapFromResponses(calEvent: CalendarEvent) {
const { customInputs, userFieldsResponses } = calEvent;

let labelValueMap: Record<string, string | string[]> = {};
let labelValueMap: Record<string, z.infer<typeof bookingResponse>> = {};
if (userFieldsResponses) {
for (const [, value] of Object.entries(userFieldsResponses)) {
if (!value.label) {
continue;
}
labelValueMap[value.label] = value.value;
}
} else {
Expand Down
13 changes: 5 additions & 8 deletions packages/trpc/server/routers/viewer/bookings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
import { sendDeclinedEmails, sendLocationChangeEmails, sendRequestRescheduleEmail } from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
Expand Down Expand Up @@ -773,18 +773,14 @@ export const bookingsRouter = router({
scheduledJobs: true,
},
});

const bookingFields = bookingRaw.eventType
? getBookingFieldsWithSystemFields(bookingRaw.eventType)
: null;

const booking = {
...bookingRaw,
responses: getBookingResponsesPartialSchema({
eventType: {
bookingFields,
},
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
view: "ALL_VIEWS",
}),
responses: bookingResponsesDbSchema.parse(bookingRaw.responses),
eventType: bookingRaw.eventType
? {
...bookingRaw.eventType,
Expand Down Expand Up @@ -850,6 +846,7 @@ export const bookingsRouter = router({

const attendeesList = await Promise.all(attendeesListPromises);

// TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted.
const { calEventUserFieldsResponses, calEventResponses } = getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
responses: booking.responses,
Expand Down
26 changes: 12 additions & 14 deletions packages/types/Calendar.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { Dayjs } from "dayjs";
import type { calendar_v3 } from "googleapis";
import type { Time } from "ical.js";
import type { TFunction } from "next-i18next";
import type z from "zod";

import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { Calendar } from "@calcom/features/calendars/weeklyview";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import type { Frequency } from "@calcom/prisma/zod-utils";
Expand Down Expand Up @@ -129,6 +131,14 @@ export type AppsStatus = {
warnings?: string[];
};

type CalEventResponses = Record<
string,
{
label: string;
value: z.infer<typeof bookingResponse>;
}
>;

// If modifying this interface, probably should update builders/calendarEvent files
export interface CalendarEvent {
type: string;
Expand Down Expand Up @@ -164,22 +174,10 @@ export interface CalendarEvent {
seatsPerTimeSlot?: number | null;

// It has responses to all the fields(system + user)
responses?: Record<
string,
{
value: string | string[];
label: string;
}
> | null;
responses?: CalEventResponses | null;

// It just has responses to only the user fields. It allows to easily iterate over to show only user fields
userFieldsResponses?: Record<
string,
{
value: string | string[];
label: string;
}
> | null;
userFieldsResponses?: CalEventResponses | null;
}

export interface EntryPoint {
Expand Down

0 comments on commit 90443d1

Please sign in to comment.