refactor: removed isNotAnApiCall (#13568)

This commit is contained in:
Omar López
2024-02-07 04:43:36 -03:00
committed by GitHub
parent 8469c91973
commit d2b4760e3a
14 changed files with 593 additions and 640 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
import type { NextApiRequest } from "next";
import getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { defaultResponder } from "@calcom/lib/server";
@@ -206,7 +207,7 @@ async function handler(req: NextApiRequest) {
const { userId, isAdmin } = req;
if (isAdmin) req.userId = req.body.userId || userId;
return await handleNewBooking(req);
return await handleNewBooking(req, getBookingDataSchemaForApi);
}
export default defaultResponder(handler);
+1 -3
View File
@@ -17,9 +17,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
const session = await getServerSession({ req, res });
/* To mimic API behavior and comply with types */
req.userId = session?.user?.id || -1;
const booking = await handleNewBooking(req, {
isNotAnApiCall: true,
});
const booking = await handleNewBooking(req);
return booking;
}
+3 -9
View File
@@ -52,9 +52,7 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
noEmail: false,
};
const firstBookingResult = await handleNewBooking(recurringEventReq, {
isNotAnApiCall: true,
});
const firstBookingResult = await handleNewBooking(recurringEventReq);
luckyUsers = firstBookingResult.luckyUsers?.map((user) => user.id);
}
@@ -92,12 +90,8 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
luckyUsers,
};
const promiseEachRecurringBooking: ReturnType<typeof handleNewBooking> = handleNewBooking(
recurringEventReq,
{
isNotAnApiCall: true,
}
);
const promiseEachRecurringBooking: ReturnType<typeof handleNewBooking> =
handleNewBooking(recurringEventReq);
const eachRecurringBooking = await promiseEachRecurringBooking;
@@ -27,7 +27,7 @@ export const useBookingForm = ({ event }: IUseBookingForm) => {
.object({
responses: event?.data
? getBookingResponsesSchema({
eventType: event?.data,
bookingFields: event.data.bookingFields,
view: rescheduleUid ? "reschedule" : "booking",
})
: // Fallback until event is loaded.
@@ -37,9 +37,7 @@ export function useInitialFormValues({
return {};
}
const querySchema = getBookingResponsesPartialSchema({
eventType: {
bookingFields: eventType.bookingFields,
},
bookingFields: eventType.bookingFields,
view: rescheduleUid ? "reschedule" : "booking",
});
@@ -1,80 +1,20 @@
import { z } from "zod";
import {
bookingCreateSchemaLegacyPropsForApi,
bookingCreateBodySchemaForApi,
extendedBookingCreateBody,
} from "@calcom/prisma/zod-utils";
import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
import type { getBookingFieldsWithSystemFields } from "./getBookingFields";
import getBookingResponsesSchema from "./getBookingResponsesSchema";
import type { getEventTypesFromDB } from "./handleNewBooking";
const getBookingDataSchema = (
rescheduleUid: string | undefined,
isNotAnApiCall: boolean,
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>
) => {
const responsesSchema = getBookingResponsesSchema({
eventType: {
bookingFields: eventType.bookingFields,
},
view: rescheduleUid ? "reschedule" : "booking",
});
const bookingDataSchema = isNotAnApiCall
? extendedBookingCreateBody.merge(
z.object({
responses: responsesSchema,
})
)
: bookingCreateBodySchemaForApi
.merge(
z.object({
responses: responsesSchema.optional(),
})
)
.superRefine((val, ctx) => {
if (val.responses && val.customInputs) {
ctx.addIssue({
code: "custom",
message:
"Don't use both customInputs and responses. `customInputs` is only there for legacy support.",
});
return;
}
const legacyProps = Object.keys(bookingCreateSchemaLegacyPropsForApi.shape);
if (val.responses) {
const unwantedProps: string[] = [];
legacyProps.forEach((legacyProp) => {
if (typeof val[legacyProp as keyof typeof val] !== "undefined") {
console.error(
`Deprecated: Unexpected falsy value for: ${unwantedProps.join(
","
)}. They can't be used with \`responses\`. This will become a 400 error in the future.`
);
}
if (val[legacyProp as keyof typeof val]) {
unwantedProps.push(legacyProp);
}
});
if (unwantedProps.length) {
ctx.addIssue({
code: "custom",
message: `Legacy Props: ${unwantedProps.join(",")}. They can't be used with \`responses\``,
});
return;
}
} else if (val.customInputs) {
const { success } = bookingCreateSchemaLegacyPropsForApi.safeParse(val);
if (!success) {
ctx.addIssue({
code: "custom",
message: `With \`customInputs\` you must specify legacy props ${legacyProps.join(",")}`,
});
}
}
});
return bookingDataSchema;
const getBookingDataSchema = ({
view = "booking",
bookingFields,
}: {
view: "booking" | "reschedule";
bookingFields: Awaited<ReturnType<typeof getBookingFieldsWithSystemFields>>;
}) => {
return extendedBookingCreateBody.merge(
z.object({ responses: getBookingResponsesSchema({ bookingFields, view }) })
);
};
export default getBookingDataSchema;
@@ -0,0 +1,69 @@
import { z } from "zod";
import {
bookingCreateBodySchemaForApi,
bookingCreateSchemaLegacyPropsForApi,
} from "@calcom/prisma/zod-utils";
import type { getBookingFieldsWithSystemFields } from "./getBookingFields";
import getBookingResponsesSchema from "./getBookingResponsesSchema";
const getBookingDataSchemaForApi = ({
view = "booking",
bookingFields,
}: {
view: "booking" | "reschedule";
bookingFields: Awaited<ReturnType<typeof getBookingFieldsWithSystemFields>>;
}) => {
const responsesSchema = getBookingResponsesSchema({ bookingFields, view });
return bookingCreateBodySchemaForApi
.merge(
z.object({
responses: responsesSchema.optional(),
})
)
.superRefine((val, ctx) => {
if (val.responses && val.customInputs) {
ctx.addIssue({
code: "custom",
message:
"Don't use both customInputs and responses. `customInputs` is only there for legacy support.",
});
return;
}
const legacyProps = Object.keys(bookingCreateSchemaLegacyPropsForApi.shape);
if (val.responses) {
const unwantedProps: string[] = [];
legacyProps.forEach((legacyProp) => {
if (typeof val[legacyProp as keyof typeof val] !== "undefined") {
console.error(
`Deprecated: Unexpected falsy value for: ${unwantedProps.join(
","
)}. They can't be used with \`responses\`. This will become a 400 error in the future.`
);
}
if (val[legacyProp as keyof typeof val]) {
unwantedProps.push(legacyProp);
}
});
if (unwantedProps.length) {
ctx.addIssue({
code: "custom",
message: `Legacy Props: ${unwantedProps.join(",")}. They can't be used with \`responses\``,
});
return;
}
} else if (val.customInputs) {
const { success } = bookingCreateSchemaLegacyPropsForApi.safeParse(val);
if (!success) {
ctx.addIssue({
code: "custom",
message: `With \`customInputs\` you must specify legacy props ${legacyProps.join(",")}`,
});
}
}
});
};
export default getBookingDataSchemaForApi;
File diff suppressed because it is too large Load Diff
@@ -1,64 +1,55 @@
import z from "zod";
import type { ALL_VIEWS } from "@calcom/features/form-builder/schema";
import { fieldTypesSchemaMap, dbReadResponseSchema } from "@calcom/features/form-builder/schema";
import { dbReadResponseSchema, fieldTypesSchemaMap } from "@calcom/features/form-builder/schema";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils";
type EventType = Parameters<typeof preprocess>[0]["eventType"];
// eslint-disable-next-line @typescript-eslint/ban-types
type View = ALL_VIEWS | (string & {});
type BookingFields = (z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">) | null;
type CommonParams = { bookingFields: BookingFields; view: View };
export const bookingResponse = dbReadResponseSchema;
export const bookingResponsesDbSchema = z.record(dbReadResponseSchema);
const catchAllSchema = bookingResponsesDbSchema;
export const getBookingResponsesPartialSchema = ({
eventType,
view,
}: {
eventType: EventType;
view: View;
}) => {
export const getBookingResponsesPartialSchema = ({ bookingFields, view }: CommonParams) => {
const schema = bookingResponses.unwrap().partial().and(catchAllSchema);
return preprocess({ schema, eventType, isPartialSchema: true, view });
return preprocess({ schema, bookingFields, isPartialSchema: true, view });
};
// Should be used when we know that not all fields responses are present
// - Can happen when we are parsing the prefill query string
// - 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, view }: { eventType: EventType; view: View }) {
export default function getBookingResponsesSchema({ bookingFields, view }: CommonParams) {
const schema = bookingResponses.and(z.record(z.any()));
return preprocess({ schema, eventType, isPartialSchema: false, view });
return preprocess({ schema, bookingFields, isPartialSchema: false, view });
}
// TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder
// It allows anyone using FormBuilder to get the same preprocessing automatically
function preprocess<T extends z.ZodType>({
schema,
eventType,
bookingFields,
isPartialSchema,
view: currentView,
}: {
}: CommonParams & {
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;
};
view: View;
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
const preprocessed = z.preprocess(
(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) => {
if (!bookingFields) return parsedResponses;
bookingFields.forEach((field) => {
const value = parsedResponses[field.name];
if (value === undefined) {
// If there is no response for the field, then we don't need to do any processing
@@ -110,11 +101,11 @@ function preprocess<T extends z.ZodType>({
};
},
schema.superRefine(async (responses, ctx) => {
if (!eventType.bookingFields) {
if (!bookingFields) {
// if eventType has been deleted, we won't have bookingFields and thus we can't validate the responses.
return;
}
for (const bookingField of eventType.bookingFields) {
for (const bookingField of bookingFields) {
const value = responses[bookingField.name];
const stringSchema = z.string();
const emailSchema = isPartialSchema ? z.string() : z.string().refine(emailSchemaRefinement);
@@ -43,7 +43,7 @@ export const getCalEventResponses = ({
const label = field.label || field.defaultLabel;
if (!label) {
//TODO: This error must be thrown while saving event-type as well so that such an event-type can't be saved
throw new Error('Missing label for booking field "' + field.name + '"');
throw new Error(`Missing label for booking field "${field.name}"`);
}
// If the guests field is hidden (disableGuests is set on the event type) don't try and infer guests from attendees list
@@ -591,19 +591,16 @@ async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boole
});
}
export async function getBookingData({
export async function getBookingData<T extends z.ZodType>({
req,
isNotAnApiCall,
eventType,
schema,
}: {
req: NextApiRequest;
isNotAnApiCall: boolean;
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>;
schema: T;
}) {
const bookingDataSchema = getBookingDataSchema(req.body?.rescheduleUid, isNotAnApiCall, eventType);
const reqBody = await bookingDataSchema.parseAsync(req.body);
const reqBody = await schema.parseAsync(req.body);
const reqBodyWithEnd = (reqBody: ReqBodyWithoutEnd): reqBody is ReqBodyWithEnd => {
// Use the event length to auto-set the event end time.
if (!Object.prototype.hasOwnProperty.call(reqBody, "end")) {
@@ -615,11 +612,9 @@ export async function getBookingData({
throw new Error(ErrorCode.RequestBodyWithouEnd);
}
// reqBody.end is no longer an optional property.
if ("customInputs" in reqBody) {
if (reqBody.customInputs) {
// Check if required custom inputs exist
handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs);
}
if (reqBody.customInputs) {
// Check if required custom inputs exist
handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs);
const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi.parse(reqBody);
return {
...reqBody,
@@ -630,31 +625,36 @@ export async function getBookingData({
smsReminderNumber: reqBodyWithLegacyProps.smsReminderNumber,
notes: reqBodyWithLegacyProps.notes,
rescheduleReason: reqBodyWithLegacyProps.rescheduleReason,
};
} else {
if (!reqBody.responses) {
throw new Error("`responses` must not be nullish");
}
const responses = reqBody.responses;
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
getCalEventResponses({
bookingFields: eventType.bookingFields,
responses,
});
return {
...reqBody,
name: responses.name,
email: responses.email,
guests: responses.guests ? responses.guests : [],
location: responses.location?.optionValue || responses.location?.value || "",
smsReminderNumber: responses.smsReminderNumber,
notes: responses.notes || "",
calEventUserFieldsResponses,
rescheduleReason: responses.rescheduleReason,
calEventResponses,
// So TS doesn't complain about unknown properties
calEventUserFieldsResponses: undefined,
calEventResponses: undefined,
customInputs: undefined,
};
}
if (!reqBody.responses) {
throw new Error("`responses` must not be nullish");
}
const responses = reqBody.responses;
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
getCalEventResponses({
bookingFields: eventType.bookingFields,
responses,
});
return {
...reqBody,
name: responses.name,
email: responses.email,
guests: responses.guests ? responses.guests : [],
location: responses.location?.optionValue || responses.location?.value || "",
smsReminderNumber: responses.smsReminderNumber,
notes: responses.notes || "",
calEventUserFieldsResponses,
rescheduleReason: responses.rescheduleReason,
calEventResponses,
// So TS doesn't complain about unknown properties
customInputs: undefined,
};
}
async function createBooking({
@@ -841,13 +841,10 @@ export function getCustomInputsResponses(
eventTypeCustomInputs: Awaited<ReturnType<typeof getEventTypesFromDB>>["customInputs"]
) {
const customInputsResponses = {} as NonNullable<CalendarEvent["customInputs"]>;
if ("customInputs" in reqBody) {
const reqCustomInputsResponses = reqBody.customInputs || [];
if (reqCustomInputsResponses?.length > 0) {
reqCustomInputsResponses.forEach(({ label, value }) => {
customInputsResponses[label] = value;
});
}
if (reqBody.customInputs && (reqBody.customInputs.length || 0) > 0) {
reqBody.customInputs.forEach(({ label, value }) => {
customInputsResponses[label] = value;
});
} else {
const responses = reqBody.responses || {};
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
@@ -994,15 +991,13 @@ export const findBookingQuery = async (bookingId: number) => {
return foundBooking;
};
type BookingDataSchemaGetter =
| typeof getBookingDataSchema
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
async function handler(
req: NextApiRequest & { userId?: number | undefined },
{
isNotAnApiCall = false,
}: {
isNotAnApiCall?: boolean;
} = {
isNotAnApiCall: false,
}
bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema
) {
const { userId } = req;
@@ -1016,6 +1011,17 @@ async function handler(
...eventType,
bookingFields: getBookingFieldsWithSystemFields(eventType),
};
const bookingDataSchema = bookingDataSchemaGetter({
view: req.body?.rescheduleUid ? "reschedule" : "booking",
bookingFields: eventType.bookingFields,
});
const bookingData = await getBookingData({
req,
eventType,
schema: bookingDataSchema,
});
const {
recurringCount,
noEmail,
@@ -1033,11 +1039,7 @@ async function handler(
rescheduleReason,
luckyUsers,
...reqBody
} = await getBookingData({
req,
isNotAnApiCall,
eventType,
});
} = bookingData;
const loggerWithEventDetails = createLoggerWithEventDetails(eventTypeId, reqBody.user, eventTypeSlug);
@@ -1199,7 +1201,10 @@ async function handler(
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
) {
const startAsDate = dayjs(reqBody.start).toDate();
if (eventType.bookingLimits) {
if (
eventType.bookingLimits &&
/* Empty object is truthy */ Object.keys(eventType.bookingLimits).length > 0
) {
await checkBookingLimits(
eventType.bookingLimits as IntervalLimit,
startAsDate,
@@ -1507,7 +1512,7 @@ async function handler(
const attendeesList = [...invitee, ...guests];
const responses = "responses" in reqBody ? reqBody.responses : null;
const responses = reqBody.responses || null;
const evtName = !eventType?.isDynamic ? eventType.eventName : responses?.title;
const eventNameObject = {
@@ -1524,9 +1529,6 @@ async function handler(
t: tOrganizer,
};
const calEventUserFieldsResponses =
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
const iCalUID = getICalUID({
event: { iCalUID: originalRescheduledBooking?.iCalUID, uid: originalRescheduledBooking?.uid },
uid,
@@ -1563,8 +1565,8 @@ async function handler(
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),
},
responses: "calEventResponses" in reqBody ? reqBody.calEventResponses : null,
userFieldsResponses: calEventUserFieldsResponses,
responses: reqBody.calEventResponses || null,
userFieldsResponses: reqBody.calEventUserFieldsResponses || null,
attendees: attendeesList,
location: bookingLocation, // Will be processed by the EventManager later.
conferenceCredentialId,
@@ -6,11 +6,12 @@ import { v5 as uuidv5 } from "uuid";
import { createInstantMeetingWithCalVideo } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import {
getEventTypesFromDB,
getBookingData,
getCustomInputsResponses,
getEventTypesFromDB,
} from "@calcom/features/bookings/lib/handleNewBooking";
import { getFullName } from "@calcom/features/form-builder/utils";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
@@ -83,10 +84,14 @@ async function handler(req: NextApiRequest) {
throw new Error("Only Team Event Types are supported for Instant Meeting");
}
const schema = getBookingDataSchema({
view: req.body?.rescheduleUid ? "reschedule" : "booking",
bookingFields: eventType.bookingFields,
});
const reqBody = await getBookingData({
req,
isNotAnApiCall: true,
eventType,
schema,
});
const { email: bookerEmail, name: bookerName } = reqBody;
+1 -2
View File
@@ -2,8 +2,6 @@ import type { Prisma } from "@prisma/client";
import type { UnitTypeLongPlural } from "dayjs";
import type { TFunction } from "next-i18next";
import z, { ZodNullable, ZodObject, ZodOptional } from "zod";
/* eslint-disable no-underscore-dangle */
import type {
AnyZodObject,
objectInputType,
@@ -274,6 +272,7 @@ export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
)
.optional(),
luckyUsers: z.array(z.number()).optional(),
customInputs: z.undefined().optional(),
})
);
+6 -2
View File
@@ -35,8 +35,12 @@ const workspaces = packagedEmbedTestsOnly
{
test: {
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
// TODO: Ignore the api until tests are fixed
exclude: ["**/node_modules/**/*", "packages/embeds/**/*", "packages/lib/hooks/**/*"],
exclude: [
"**/node_modules/**/*",
"**/.next/**/*",
"packages/embeds/**/*",
"packages/lib/hooks/**/*",
],
setupFiles: ["setupVitest.ts"],
},
},