Make location required in zodSchema as was there in production

pull/6560/head
Hariom Balhara 2023-02-16 14:31:23 +05:30
parent 83610f8c1b
commit 2e380580cd
8 changed files with 59 additions and 43 deletions

View File

@ -295,7 +295,7 @@ const BookingPage = ({
const defaultValues = () => { const defaultValues = () => {
if (!rescheduleUid) { if (!rescheduleUid) {
const defaults = { const defaults = {
responses: {} as z.infer<typeof bookingFormSchema>["responses"], responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
}; };
const responses = eventType.bookingFields.reduce((responses, field) => { const responses = eventType.bookingFields.reduce((responses, field) => {
@ -322,7 +322,7 @@ const BookingPage = ({
} }
const defaults = { const defaults = {
responses: {} as z.infer<typeof bookingFormSchema>["responses"], responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
}; };
const responses = eventType.bookingFields.reduce((responses, field) => { const responses = eventType.bookingFields.reduce((responses, field) => {

View File

@ -5,26 +5,28 @@ import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
type BookingSelect = {
description: true;
customInputs: true;
attendees: {
select: {
email: true;
name: true;
};
};
location: true;
smsReminderNumber: true;
};
// Backward Compatibility for booking created before we had managed booking questions // Backward Compatibility for booking created before we had managed booking questions
function getResponsesFromOldBooking( function getResponsesFromOldBooking(
rawBooking: Prisma.BookingGetPayload<{ rawBooking: Prisma.BookingGetPayload<{
select: { select: BookingSelect;
description: true;
customInputs: true;
attendees: {
select: {
email: true;
name: true;
};
};
location: true;
smsReminderNumber: true;
};
}> }>
) { ) {
const customInputs = rawBooking.customInputs || {}; const customInputs = rawBooking.customInputs || {};
const responses = Object.keys(customInputs).reduce((acc, key) => { const responses = Object.keys(customInputs).reduce((acc, label) => {
acc[slugify(key) as keyof typeof acc] = customInputs[key as keyof typeof customInputs]; acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs];
return acc; return acc;
}, {}); }, {});
return { return {
@ -70,12 +72,10 @@ async function getBooking(
if (!rawBooking) { if (!rawBooking) {
return rawBooking; return rawBooking;
} }
const booking = {
...rawBooking, const booking = getBookingWithResponses(rawBooking, {
responses: getBookingResponsesPartialSchema({ bookingFields,
bookingFields, });
}).parse(rawBooking.responses || getResponsesFromOldBooking(rawBooking)),
};
if (booking) { if (booking) {
// @NOTE: had to do this because Server side cant return [Object objects] // @NOTE: had to do this because Server side cant return [Object objects]
@ -88,4 +88,23 @@ async function getBooking(
export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>; export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>;
export const getBookingWithResponses = <
T extends Prisma.BookingGetPayload<{
select: BookingSelect & {
responses: true;
};
}>
>(
booking: T,
eventType: {
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
}
) => {
return {
...booking,
responses: getBookingResponsesPartialSchema({
bookingFields: eventType.bookingFields,
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
};
};
export default getBooking; export default getBooking;

View File

@ -29,7 +29,6 @@ import {
SystemField, SystemField,
getBookingFieldsWithSystemFields, getBookingFieldsWithSystemFields,
} from "@calcom/features/bookings/lib/getBookingFields"; } from "@calcom/features/bookings/lib/getBookingFields";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { parseRecurringEvent } from "@calcom/lib"; import { parseRecurringEvent } from "@calcom/lib";
import CustomBranding from "@calcom/lib/CustomBranding"; import CustomBranding from "@calcom/lib/CustomBranding";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
@ -48,6 +47,7 @@ import { Button, EmailInput, HeadSeo, Label } from "@calcom/ui";
import { FiX, FiChevronLeft, FiCheck, FiCalendar, FiExternalLink } from "@calcom/ui/components/icon"; import { FiX, FiChevronLeft, FiCheck, FiCalendar, FiExternalLink } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock"; import { timeZone } from "@lib/clock";
import { getBookingWithResponses } from "@lib/getBooking";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking"; import CancelBooking from "@components/booking/CancelBooking";
@ -1018,6 +1018,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
notFound: true, notFound: true,
}; };
} }
const eventTypeRaw = !bookingInfoRaw.eventTypeId const eventTypeRaw = !bookingInfoRaw.eventTypeId
? getDefaultEvent(eventTypeSlug || "") ? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId); : await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
@ -1027,10 +1028,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}; };
} }
const bookingInfo = { const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
...bookingInfoRaw,
responses: getBookingResponsesPartialSchema(eventTypeRaw).parse(bookingInfoRaw.responses),
};
// @NOTE: had to do this because Server side cant return [Object objects] // @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse // probably fixable with json.stringify -> json.parse

View File

@ -193,7 +193,6 @@ export const ensureBookingInputsHaveSystemFields = ({
defaultLabel: "location", defaultLabel: "location",
type: "radioInput", type: "radioInput",
name: "location", name: "location",
// Even though it should be required it is optional in production with backend choosing CalVideo as the fallback
required: false, required: false,
// Populated on the fly from locations. I don't want to duplicate storing locations and instead would like to be able to refer to locations in eventType. // Populated on the fly from locations. I don't want to duplicate storing locations and instead would like to be able to refer to locations in eventType.
// options: `eventType.locations` // options: `eventType.locations`

View File

@ -7,7 +7,7 @@ type EventType = Parameters<typeof preprocess>[0]["eventType"];
export const getBookingResponsesPartialSchema = (eventType: EventType) => { export const getBookingResponsesPartialSchema = (eventType: EventType) => {
const schema = bookingResponses.unwrap().partial().and(z.record(z.any())); const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
return preprocess({ schema, eventType, forQueryParsing: true }); return preprocess({ schema, eventType, isPartialSchema: true });
}; };
// Should be used when we know that not all fields responses are present // Should be used when we know that not all fields responses are present
@ -15,7 +15,7 @@ export const getBookingResponsesPartialSchema = (eventType: EventType) => {
// - Can happen when we are parsing a booking's responses (which was created before we added a new required field) // - Can happen when we are parsing a booking's responses (which was created before we added a new required field)
export default function getBookingResponsesSchema(eventType: EventType) { export default function getBookingResponsesSchema(eventType: EventType) {
const schema = bookingResponses.and(z.record(z.any())); const schema = bookingResponses.and(z.record(z.any()));
return preprocess({ schema, eventType, forQueryParsing: false }); return preprocess({ schema, eventType, isPartialSchema: false });
} }
// TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder // TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder
@ -23,14 +23,12 @@ export default function getBookingResponsesSchema(eventType: EventType) {
function preprocess<T extends z.ZodType>({ function preprocess<T extends z.ZodType>({
schema, schema,
eventType, eventType,
forQueryParsing, isPartialSchema,
}: { }: {
schema: T; schema: T;
forQueryParsing: boolean; isPartialSchema: boolean;
eventType: { eventType: {
bookingFields: z.infer<typeof eventTypeBookingFields> & bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
z.infer<typeof eventTypeBookingFields> &
z.BRAND<"HAS_SYSTEM_FIELDS">;
}; };
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> { }): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
const preprocessed = z.preprocess( const preprocessed = z.preprocess(
@ -71,18 +69,18 @@ function preprocess<T extends z.ZodType>({
eventType.bookingFields.forEach((bookingField) => { eventType.bookingFields.forEach((bookingField) => {
const value = responses[bookingField.name]; const value = responses[bookingField.name];
const stringSchema = z.string(); const stringSchema = z.string();
const emailSchema = forQueryParsing ? z.string() : z.string().email(); const emailSchema = isPartialSchema ? z.string() : z.string().email();
const phoneSchema = forQueryParsing const phoneSchema = isPartialSchema
? z.string() ? z.string()
: z.string().refine((val) => isValidPhoneNumber(val)); : z.string().refine((val) => isValidPhoneNumber(val));
// Tag the message with the input name so that the message can be shown at appropriate place // Tag the message with the input name so that the message can be shown at appropriate place
const m = (message: string) => `{${bookingField.name}}${message}`; const m = (message: string) => `{${bookingField.name}}${message}`;
const isRequired = bookingField.required; const isRequired = bookingField.required;
if ((forQueryParsing || !isRequired) && value === undefined) { if ((isPartialSchema || !isRequired) && value === undefined) {
return; return;
} }
if (isRequired && !forQueryParsing && !value) if (isRequired && !isPartialSchema && !value)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) }); ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
if (bookingField.type === "email") { if (bookingField.type === "email") {
@ -178,7 +176,7 @@ function preprocess<T extends z.ZodType>({
}); });
}) })
); );
if (forQueryParsing) { if (isPartialSchema) {
// Query Params can be completely invalid, try to preprocess as much of it in correct format but in worst case simply don't prefill instead of crashing // Query Params can be completely invalid, try to preprocess as much of it in correct format but in worst case simply don't prefill instead of crashing
return preprocessed.catch(() => { return preprocessed.catch(() => {
console.error("Failed to preprocess query params, prefilling will be skipped"); console.error("Failed to preprocess query params, prefilling will be skipped");

View File

@ -407,6 +407,8 @@ async function handler(
isNotAnApiCall = false, isNotAnApiCall = false,
}: { }: {
isNotAnApiCall?: boolean; isNotAnApiCall?: boolean;
} = {
isNotAnApiCall: false,
} }
) { ) {
const { userId } = req; const { userId } = req;

View File

@ -166,11 +166,11 @@ export const FormBuilder = function FormBuilder({
onChange([ onChange([
{ {
label: "Option 1", label: "Option 1",
value: "1", value: "Option 1",
}, },
{ {
label: "Option 2", label: "Option 2",
value: "2", value: "Option 2",
}, },
]); ]);
} }

View File

@ -204,7 +204,7 @@ export const bookingCreateSchemaLegacyPropsForApi = z.object({
name: z.string(), name: z.string(),
guests: z.array(z.string()).optional(), guests: z.array(z.string()).optional(),
notes: z.string().optional(), notes: z.string().optional(),
location: z.string().optional(), location: z.string(),
smsReminderNumber: z.string().optional().nullable(), smsReminderNumber: z.string().optional().nullable(),
rescheduleReason: z.string().optional(), rescheduleReason: z.string().optional(),
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })), customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),