fix: change URL prefill behavior to skip only invalid fields (#26982)

* fix: change URL prefill behavior to skip only invalid fields

Previously, when prefilling booking form fields from URL query parameters,
if any single field failed validation, all prefilling was skipped and an
empty object was returned.

This change modifies the behavior so that only invalid fields are skipped,
while valid fields are still prefilled. The implementation:

- Adds validateSingleFieldForPrefill() to validate individual fields
- Adds parseFieldByField() to collect valid fields when full parse fails
- Modifies getBookingResponsesPartialSchema to use field-by-field parsing
  as a fallback when full schema validation fails
- Logs which specific fields were skipped and why

This improves user experience by preserving valid prefill data even when
some fields have invalid values.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: reduce code duplication in partial prefill validation

Simplify the implementation by integrating field-by-field error handling
directly into the existing preprocess() function instead of using separate
helper functions.

Changes:
- Remove validateSingleFieldForPrefill() and parseFieldByField() functions
- Simplify getBookingResponsesPartialSchema to directly use preprocess()
- Add try-catch wrapper in preprocess() when isPartialSchema=true
- Continue processing other fields even if one fails during preprocessing
- Log skipped fields with reasons at debug level

This reduces code duplication while maintaining the same behavior of
skipping only invalid fields during URL prefill.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fixes

* refactor: rename getRefinedValue to superRefineField and add edge case tests

- Rename getRefinedValue to superRefineField for clarity
- Remove duplicate multiselect test block
- Add test for empty object name field during partial prefill
- Add test for invalid URL format during partial prefill
- Add test for null responses during partial prefill

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Simplifu

* Simplif more

* Enhance booking responses schema tests to validate required fields and name variants. Updated error handling for empty names and added tests for first and last name requirements. Refactored schema validation logic to improve clarity and maintainability.

* fix types

* test: add test for email validation behavior during partial prefill

Documents that email validation is relaxed to z.string() during partial prefill,
consistent with phone field behavior where partial values like '+91' are accepted.
This addresses Cubic AI feedback (confidence 9/10) to verify email handling.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix

* Add tests: packages/features/bookings/lib/getBookingResponsesSchema.test.ts

Generated by Paragon from proposal for PR #26982

* Revert "Add tests: packages/features/bookings/lib/getBookingResponsesSchema.test.ts"

This reverts commit fc2b7ad7ca8099bc4f68e97b383ea16b41cdd772.

* Simplify

* fix: sanitize partial prefill log to avoid exposing user input data

Replaced raw Zod issues array in console.warn with a count of
validation errors to prevent sensitive user data from being logged.

Issue identified by cubic (confidence 9/10).

Co-Authored-By: unknown <>

* fix

* Remove throw Error

* Refactor booking responses schema tests to use helper functions for assertions

- Introduced `expectResponsesToBe` and `expectParsingToFail` helper functions to streamline test assertions.
- Replaced direct assertions with these helper functions for improved readability and maintainability.
- Removed redundant error handling code to simplify test logic.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom BalharaandDevin AI authored and GitHub committed 2026-02-18 20:01:23 +05:30
1 parent eef47ddefd
commit ad65fbbd81
3 files changed
+1989 -1418

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -1,21 +1,16 @@
import { isValidPhoneNumber } from "libphonenumber-js/max";
import z from "zod";
import type { ALL_VIEWS } from "@calcom/features/form-builder/schema";
import { fieldTypesSchemaMap } from "@calcom/features/form-builder/schema";
import { type FieldZodCtx, fieldTypesSchemaMap } from "@calcom/features/form-builder/schema";
import { dbReadResponseSchema } from "@calcom/lib/dbReadResponseSchema";
import logger from "@calcom/lib/logger";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils";
import { isValidPhoneNumber } from "libphonenumber-js/max";
import z from "zod";
type View = ALL_VIEWS | (string & {});
type BookingFields = (z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">) | null;
type TranslationFunction = (key: string, options?: Record<string, unknown>) => string;
type CommonParams = {
bookingFields: BookingFields;
view: View;
translateFn?: TranslationFunction;
};
type CommonParams = { bookingFields: BookingFields; view: View; translateFn?: TranslationFunction };
export const bookingResponse = dbReadResponseSchema;
export const bookingResponsesDbSchema = z.record(dbReadResponseSchema);
@@ -29,6 +24,272 @@ const ensureValidPhoneNumber = (value: string) => {
return value.replace(/^ +/, "+");
};
/**
* Processes a single field's response value based on its type.
* Returns the processed value that should be stored in newResponses[field.name].
*/
function preprocessField({
field,
value,
isPartialSchema,
log,
}: {
field: NonNullable<BookingFields>[number];
value: unknown;
isPartialSchema: boolean;
log: ReturnType<typeof logger.getSubLogger>;
}): unknown {
const fieldTypeSchema = fieldTypesSchemaMap[field.type as keyof typeof fieldTypesSchemaMap];
// TODO: Move all the schemas along with their respective types to fieldTypeSchema, that would make schemas shared across Routing Forms builder and Booking Question Formm builder
if (fieldTypeSchema) {
return fieldTypeSchema.preprocess({
response: value,
isPartialSchema,
field,
});
}
if (field.type === "boolean") {
// Turn a boolean in string to a real boolean
return value === "true" || value === true;
}
// Make sure that the value is an array
else if (field.type === "multiemail" || field.type === "checkbox" || field.type === "multiselect") {
return value instanceof Array ? value : [value];
}
// Parse JSON
else if (field.type === "radioInput" && typeof value === "string") {
let parsedValue = {
optionValue: "",
value: "",
};
try {
parsedValue = JSON.parse(value);
} catch (e) {
log.error(`Failed to parse JSON for field ${field.name}`, e);
}
const optionsInputs = field.optionsInputs;
const optionInputField = optionsInputs?.[parsedValue.value];
if (optionInputField && optionInputField.type === "phone") {
parsedValue.optionValue = ensureValidPhoneNumber(parsedValue.optionValue);
}
return parsedValue;
} else if (field.type === "phone") {
return ensureValidPhoneNumber(typeof value === "string" ? value : String(value));
} else {
return value;
}
}
/**
* Runs superRefine validation for a field's response value based on its type.
* Handles all field type validations including email, phone, multiselect, etc.
* Throws on configuration errors (e.g., invalid variant) - caller should wrap in try-catch for partial schemas.
*/
async function superRefineField({
field,
value,
isPartialSchema,
isRequired,
checkOptional,
zodCtx,
translateFn,
responses,
}: {
field: NonNullable<BookingFields>[number];
value: unknown;
isPartialSchema: boolean;
isRequired: boolean;
checkOptional: boolean;
zodCtx: FieldZodCtx;
translateFn?: TranslationFunction;
responses: Record<string, unknown>;
}): Promise<void> {
const stringSchema = z.string();
const emailSchema = isPartialSchema ? z.string() : z.string().refine(emailSchemaRefinement);
const phoneSchema = isPartialSchema
? z.string()
: z.string().refine(async (val) => {
return isValidPhoneNumber(val);
});
// Tag the message with the input name so that the message can be shown at appropriate place
const m = (message: string, options?: Record<string, unknown>) => {
const translatedMessage = translateFn ? translateFn(message, options) : message;
return `{${field.name}}${translatedMessage}`;
};
if (isRequired && !isPartialSchema && !value) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
return;
}
if (field.type === "email") {
if (!field.hidden && (isRequired || (value && String(value).trim() !== ""))) {
// Email RegExp to validate if the input is a valid email
if (!emailSchema.safeParse(value).success) {
zodCtx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
}
if (value) {
const bookerEmail = String(value);
const excludedEmails = field.excludeEmails?.split(",").map((domain) => domain.trim()) || [];
const match = excludedEmails.find((excludedEntry) => doesEmailMatchEntry(bookerEmail, excludedEntry));
if (match) {
zodCtx.addIssue({
code: z.ZodIssueCode.custom,
message: m("exclude_emails_match_found_error_message"),
});
}
const requiredEmails =
field.requireEmails
?.split(",")
.map((domain) => domain.trim())
.filter(Boolean) || [];
const requiredEmailsMatch = requiredEmails.find((requiredEntry) =>
doesEmailMatchEntry(bookerEmail, requiredEntry)
);
if (requiredEmails.length > 0 && !requiredEmailsMatch) {
zodCtx.addIssue({
code: z.ZodIssueCode.custom,
message: m("require_emails_no_match_found_error_message"),
});
}
}
}
return;
}
const fieldTypeSchema = fieldTypesSchemaMap[field.type as keyof typeof fieldTypesSchemaMap];
if (fieldTypeSchema) {
fieldTypeSchema.superRefine({
// We use `unknown` here because the response type is not trivial to know here
// We know for sure that the value here is preprocessed(considering how we have called z.preprocess())
// and thus the fieldTypeSchema implementation could rely on having the correct type as per its preprocess fn return value
response: value as unknown as any,
ctx: zodCtx,
m,
field,
isPartialSchema,
});
return;
}
if (field.type === "multiemail") {
const emailsParsed = emailSchema.array().safeParse(value);
if (isRequired && (!value || (value as unknown[]).length === 0)) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
return;
}
if (!emailsParsed.success) {
// If additional guests are shown but all inputs are empty then don't show any errors
if (field.name === "guests" && (value as string[]).every((email: string) => email === "")) {
// reset guests to empty array, otherwise it adds "" for every input
responses[field.name] = [];
return;
}
zodCtx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
return;
}
const emails = emailsParsed.data;
emails.sort().some((item, i) => {
if (item === emails[i + 1]) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("duplicate_email") });
return true;
}
});
return;
}
if (field.type === "multiselect") {
if (isRequired && (!value || (value as unknown[]).length === 0)) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
return;
}
if (!stringSchema.array().safeParse(value).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid array of strings") });
}
return;
}
if (field.type === "checkbox") {
if (!stringSchema.array().safeParse(value).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid array of strings") });
}
return;
}
if (field.type === "phone") {
// Determine if the phone field needs validation
const needsValidation = isRequired || (value && (value as string).trim() !== "");
// Validate phone number if the field is not hidden and requires validation
if (!field.hidden && needsValidation) {
if (!(await phoneSchema.safeParseAsync(value)).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
}
}
return;
}
if (field.type === "boolean") {
const schema = z.boolean();
if (!schema.safeParse(value).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid Boolean") });
}
return;
}
if (field.type === "radioInput") {
if (field.optionsInputs) {
const typedValue = value as { optionValue?: string; value?: string } | undefined;
const optionValue = typedValue?.optionValue;
const optionField = field.optionsInputs[typedValue?.value ?? ""];
const typeOfOptionInput = optionField?.type;
if (
// Either the field is required or there is a radio selected, we need to check if the optionInput is required or not.
(isRequired || typedValue?.value) && checkOptional ? true : optionField?.required && !optionValue
) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("error_required_field") });
return;
}
if (optionValue) {
// `typeOfOptionInput` can be any of the main types. So, the same validations should run for `optionValue`
if (typeOfOptionInput === "phone") {
if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
}
}
}
}
return;
}
// Use fieldTypeConfig.propsType to validate for propsType=="text" or propsType=="select" as in those cases, the response would be a string.
// If say we want to do special validation for 'address' that can be added to `fieldTypesSchemaMap`
if (["address", "text", "select", "number", "radio", "textarea"].includes(field.type)) {
const schema = stringSchema;
if (!schema.safeParse(value).success) {
zodCtx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid string") });
}
return;
}
zodCtx.addIssue({
code: z.ZodIssueCode.custom,
message: `Can't parse unknown booking field type: ${field.type}`,
});
}
/**
* Checks if a booker email matches an email/domain entry.
* Supports three formats:
@@ -52,14 +313,7 @@ const doesEmailMatchEntry = (bookerEmail: string, entry: string): boolean => {
};
export const getBookingResponsesPartialSchema = ({ bookingFields, view, translateFn }: CommonParams) => {
const schema = bookingResponses.unwrap().partial().and(catchAllSchema);
return preprocess({
schema,
bookingFields,
isPartialSchema: true,
view,
translateFn,
});
return preprocess({ schema, bookingFields, isPartialSchema: true, view, translateFn });
};
// Should be used when we know that not all fields responses are present
@@ -67,13 +321,7 @@ export const getBookingResponsesPartialSchema = ({ bookingFields, view, translat
// - Can happen when we are parsing a booking's responses (which was created before we added a new required field)
export default function getBookingResponsesSchema({ bookingFields, view, translateFn }: CommonParams) {
const schema = bookingResponses.and(z.record(z.any()));
return preprocess({
schema,
bookingFields,
isPartialSchema: false,
view,
translateFn,
});
return preprocess({ schema, bookingFields, isPartialSchema: false, view, translateFn });
}
// Should be used when we want to check if the optional fields are entered and valid as well
@@ -93,6 +341,39 @@ export function getBookingResponsesSchemaWithOptionalChecks({
});
}
type FieldZodCtxState = {
issues: z.IssueData[];
} | null;
const buildFieldZodCtx = ({
zodCtx,
isPartialSchema,
}: {
zodCtx: z.RefinementCtx;
isPartialSchema: boolean;
}): {
fieldZodCtx: FieldZodCtx;
state: FieldZodCtxState;
} => {
if (isPartialSchema) {
const state: FieldZodCtxState = {
issues: [],
};
return {
fieldZodCtx: {
addIssue: (issue: z.IssueData) => {
state.issues.push(issue);
},
},
state,
};
}
return {
fieldZodCtx: zodCtx,
state: null,
};
};
// 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>({
@@ -130,45 +411,18 @@ function preprocess<T extends z.ZodType>({
// If the field is not applicable in the current view, then we don't need to do any processing
return;
}
const fieldTypeSchema = fieldTypesSchemaMap[field.type as keyof typeof fieldTypesSchemaMap];
// TODO: Move all the schemas along with their respective types to fieldTypeSchema, that would make schemas shared across Routing Forms builder and Booking Question Formm builder
if (fieldTypeSchema) {
newResponses[field.name] = fieldTypeSchema.preprocess({
response: value,
isPartialSchema,
field,
});
return;
}
if (field.type === "boolean") {
// Turn a boolean in string to a real boolean
newResponses[field.name] = value === "true" || value === true;
}
// Make sure that the value is an array
else if (field.type === "multiemail" || field.type === "checkbox" || field.type === "multiselect") {
newResponses[field.name] = value instanceof Array ? value : [value];
}
// Parse JSON
else if (field.type === "radioInput" && typeof value === "string") {
let parsedValue = {
optionValue: "",
value: "",
};
try {
parsedValue = JSON.parse(value);
} catch (e) {
log.error(`Failed to parse JSON for field ${field.name}`, e);
try {
newResponses[field.name] = preprocessField({ field, value, isPartialSchema, log });
} catch (e) {
if (!isPartialSchema) {
throw e;
}
const optionsInputs = field.optionsInputs;
const optionInputField = optionsInputs?.[parsedValue.value];
if (optionInputField && optionInputField.type === "phone") {
parsedValue.optionValue = ensureValidPhoneNumber(parsedValue.optionValue);
}
newResponses[field.name] = parsedValue;
} else if (field.type === "phone") {
newResponses[field.name] = ensureValidPhoneNumber(value);
} else {
newResponses[field.name] = value;
const errorMessage = e instanceof Error ? e.message : "preprocessing failed";
const invalidFieldName = field.name;
// Remove invalid field like it never existed in the first place
delete parsedResponses[invalidFieldName];
console.warn(`Skipped invalid field during preprocessing: ${invalidFieldName} (${errorMessage})`);
}
});
@@ -196,18 +450,6 @@ function preprocess<T extends z.ZodType>({
for (const bookingField of bookingFields) {
const value = responses[bookingField.name];
const stringSchema = z.string();
const emailSchema = isPartialSchema ? z.string() : z.string().refine(emailSchemaRefinement);
const phoneSchema = isPartialSchema
? z.string()
: z.string().refine(async (val) => {
return isValidPhoneNumber(val);
});
// Tag the message with the input name so that the message can be shown at appropriate place
const m = (message: string, options?: Record<string, unknown>) => {
const translatedMessage = translateFn ? translateFn(message, options) : message;
return `{${bookingField.name}}${translatedMessage}`;
};
const views = bookingField.views;
const isFieldApplicableToCurrentView =
currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true;
@@ -226,219 +468,46 @@ function preprocess<T extends z.ZodType>({
continue;
}
if (isRequired && !isPartialSchema && !value) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m(`error_required_field`),
});
return;
}
if (bookingField.type === "email") {
if (!bookingField.hidden && (isRequired || (value && value.trim() !== ""))) {
// Email RegExp to validate if the input is a valid email
if (!emailSchema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
}
// validate the excluded emails
if (value) {
const bookerEmail = value;
const excludedEmails =
bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || [];
const match = excludedEmails.find((excludedEntry) =>
doesEmailMatchEntry(bookerEmail, excludedEntry)
);
if (match) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("exclude_emails_match_found_error_message"),
});
}
const requiredEmails =
bookingField.requireEmails
?.split(",")
.map((domain) => domain.trim())
.filter(Boolean) || [];
const requiredEmailsMatch = requiredEmails.find((requiredEntry) =>
doesEmailMatchEntry(bookerEmail, requiredEntry)
);
if (requiredEmails.length > 0 && !requiredEmailsMatch) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("require_emails_no_match_found_error_message"),
});
}
}
}
continue;
}
const fieldTypeSchema = fieldTypesSchemaMap[bookingField.type as keyof typeof fieldTypesSchemaMap];
if (fieldTypeSchema) {
fieldTypeSchema.superRefine({
response: value,
ctx,
m,
field: bookingField,
isPartialSchema,
});
continue;
}
if (bookingField.type === "multiemail") {
const emailsParsed = emailSchema.array().safeParse(value);
if (isRequired && (!value || value.length === 0)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m(`error_required_field`),
});
continue;
}
if (!emailsParsed.success) {
// If additional guests are shown but all inputs are empty then don't show any errors
if (bookingField.name === "guests" && value.every((email: string) => email === "")) {
// reset guests to empty array, otherwise it adds "" for every input
responses[bookingField.name] = [];
continue;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
continue;
}
const emails = emailsParsed.data;
emails.sort().some((item, i) => {
if (item === emails[i + 1]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("duplicate_email"),
});
return true;
}
});
continue;
}
if (bookingField.type === "multiselect") {
if (isRequired && (!value || value.length === 0)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m(`error_required_field`),
});
continue;
}
if (!stringSchema.array().safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("Invalid array of strings"),
});
}
continue;
}
if (bookingField.type === "checkbox") {
if (!stringSchema.array().safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("Invalid array of strings"),
});
}
continue;
}
if (bookingField.type === "phone") {
// Determine if the phone field needs validation
const needsValidation = isRequired || (value && value.trim() !== "");
// Validate phone number if the field is not hidden and requires validation
if (!bookingField.hidden && needsValidation) {
if (!(await phoneSchema.safeParseAsync(value)).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("invalid_number"),
});
}
}
continue;
}
if (bookingField.type === "boolean") {
const schema = z.boolean();
if (!schema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("Invalid Boolean"),
});
}
continue;
}
if (bookingField.type === "radioInput") {
if (bookingField.optionsInputs) {
const optionValue = value?.optionValue;
const optionField = bookingField.optionsInputs[value?.value];
const typeOfOptionInput = optionField?.type;
if (
// Either the field is required or there is a radio selected, we need to check if the optionInput is required or not.
(isRequired || value?.value) && checkOptional ? true : optionField?.required && !optionValue
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("error_required_field"),
});
return;
}
if (optionValue) {
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
if (typeOfOptionInput === "phone") {
if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("invalid_number"),
});
}
}
}
}
continue;
}
// Use fieldTypeConfig.propsType to validate for propsType=="text" or propsType=="select" as in those cases, the response would be a string.
// If say we want to do special validation for 'address' that can be added to `fieldTypesSchemaMap`
if (["address", "text", "select", "number", "radio", "textarea"].includes(bookingField.type)) {
const schema = stringSchema;
if (!schema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("Invalid string"),
});
}
continue;
}
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Can't parse unknown booking field type: ${bookingField.type}`,
// For partial schemas, use a proxy ctx to capture issues
const { fieldZodCtx, state } = buildFieldZodCtx({
zodCtx: ctx,
isPartialSchema,
});
let superRefineError = false;
try {
await superRefineField({
field: bookingField,
value,
isPartialSchema,
isRequired,
checkOptional,
zodCtx: fieldZodCtx,
translateFn,
responses,
});
} catch (e) {
if (!isPartialSchema) {
throw e;
}
superRefineError = true;
}
// For partial schemas, remove invalid fields from responses
const issues = state?.issues ?? [];
if (isPartialSchema && (superRefineError || issues.length > 0)) {
delete responses[bookingField.name];
console.warn(
`Partial prefill: skipped field '${bookingField.name}' due to ${issues.length} validation error(s)`
);
}
}
})
);
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
return preprocessed.catch(function (res?: { error?: unknown[] }) {
console.error("Failed to preprocess query params, prefilling will be skipped", res?.error);
return preprocessed.catch((res?: { error?: unknown[] }) => {
console.error("Failed to validate query params, prefilling will be skipped entirely", res?.error);
return {};
});
}
+67 -43
View File
@@ -1,13 +1,32 @@
import { fieldSchema, fieldTypeEnum, variantsConfigSchema } from "@calcom/prisma/zod-utils";
import { z } from "zod";
import { fieldSchema, fieldTypeEnum, variantsConfigSchema, type FieldType } from "@calcom/prisma/zod-utils";
import { fieldTypesConfigMap } from "./fieldTypes";
import { preprocessNameFieldDataWithVariant } from "./utils";
import { getConfig as getVariantsConfig } from "./utils/variantsConfig";
const nonEmptyString = () => z.string().refine((value: string) => value.trim().length > 0);
type FieldTypeSchemaConfig<TInput = unknown, TPreprocessedOutput = unknown> = {
preprocess: (data: {
field: z.infer<typeof fieldSchema>;
response: TInput;
isPartialSchema: boolean;
}) => TPreprocessedOutput;
superRefine: (data: {
field: z.infer<typeof fieldSchema>;
response: TPreprocessedOutput;
isPartialSchema: boolean;
ctx: FieldZodCtx;
m: (key: string, options?: Record<string, unknown>) => string;
}) => void;
};
function defineFieldSchema<TInput, TOutput>(
config: FieldTypeSchemaConfig<TInput, TOutput>
): FieldTypeSchemaConfig<TInput, TOutput> {
return config;
}
export type ALL_VIEWS = "ALL_VIEWS";
// It is the config that is specific to a type and doesn't make sense in all fields individually. Any field with the type will automatically inherit this config.
@@ -92,36 +111,16 @@ export const fieldTypeConfigSchema = z
export const fieldsSchema = z.array(fieldSchema);
export const fieldTypesSchemaMap: Partial<
Record<
FieldType,
{
/**
* - preprocess the responses received through prefill query params
* - preprocess the values being filled in the booking form.
* - does not run for the responses received from DB
*/
preprocess: (data: {
field: z.infer<typeof fieldSchema>;
response: string;
isPartialSchema: boolean;
}) => unknown;
/**
* - Validates the response received through prefill query params
* - Validates the values being filled in the booking form.
* - does not run for the responses received from DB
*/
superRefine: (data: {
field: z.infer<typeof fieldSchema>;
response: string;
isPartialSchema: boolean;
ctx: z.RefinementCtx;
m: (key: string, options?: Record<string, unknown>) => string;
}) => void;
}
>
> = {
name: {
function stringifyResponse(response: unknown): string {
if (typeof response !== "string") {
return String(response);
} else {
return response;
}
}
export const fieldTypesSchemaMap = {
name: defineFieldSchema<unknown, unknown>({
preprocess: ({ response, field }) => {
const fieldTypeConfig = fieldTypesConfigMap[field.type];
@@ -138,7 +137,28 @@ export const fieldTypesSchemaMap: Partial<
correctedVariant = variantInResponse;
}
return preprocessNameFieldDataWithVariant(correctedVariant, response);
// We return this default value so that it meets the requirement of 'name' field being required in zod-utils#bookingResponses
const defaultValue = "";
if (response === null || response === undefined) {
return defaultValue;
}
if (typeof response === "string") {
return preprocessNameFieldDataWithVariant(correctedVariant, response);
}
if (typeof response === "object" && "firstName" in response && typeof response.firstName === "string") {
const firstAndLastNameResponse = {
firstName: response.firstName,
lastName: "",
};
if ("lastName" in response && typeof response.lastName === "string") {
firstAndLastNameResponse.lastName = response.lastName;
}
return preprocessNameFieldDataWithVariant(correctedVariant, firstAndLastNameResponse);
}
return defaultValue;
},
superRefine: ({ field, response, isPartialSchema, ctx, m }) => {
const stringSchema = z.string();
@@ -176,7 +196,7 @@ export const fieldTypesSchemaMap: Partial<
if (!variantSupportedFields.includes(subField.type)) {
throw new Error(`Unsupported field.type with variants: ${subField.type}`);
}
const valueIdentified = response as unknown as Record<string, string>;
const valueIdentified = response as Record<string, string>;
if (subField.required) {
if (!isPartialSchema && !valueIdentified[subField.name])
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
@@ -187,10 +207,10 @@ export const fieldTypesSchemaMap: Partial<
}
});
},
},
textarea: {
}),
textarea: defineFieldSchema<unknown, string>({
preprocess: ({ response }) => {
return response.trim();
return stringifyResponse(response).trim();
},
superRefine: ({ field, response, ctx, m }) => {
const fieldTypeConfig = fieldTypesConfigMap[field.type];
@@ -219,17 +239,17 @@ export const fieldTypesSchemaMap: Partial<
return;
}
},
},
url: {
}),
url: defineFieldSchema<unknown, string>({
preprocess: ({ response }) => {
return response.trim();
return stringifyResponse(response).trim();
},
superRefine: ({ response, ctx, m }) => {
const value = response ?? "";
const urlSchema = z.string().url();
// Check for malformed protocols (missing second slash test case)
if (value.match(/^https?:\/[^\/]/)) {
if (value.match(/^https?:\/[^/]/)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("url_validation_error"),
@@ -257,5 +277,9 @@ export const fieldTypesSchemaMap: Partial<
message: m("url_validation_error"),
});
},
},
}),
};
export type FieldZodCtx = {
addIssue: (issue: z.IssueData) => void;
};