fix: optional email validation (#27329)
* fix: validate optional email fields when input is provided #27299 * fix: validate optional email fields when input is provided * fix: validate optional email fields when input is provided * refactor: move empty value handling out of zod-utils * fix: remove PII from error logs * test: verify optional email allows empty submission * test: e2e validation for optional email using url state --------- Co-authored-by: Eunjae Lee <hey@eunjae.dev> Co-authored-by: Romit <85230081+romitg2@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
co-authored by
Eunjae Lee
Romit
Peer Richelsen
parent
68022f369c
commit
b45d7c15db
@@ -920,3 +920,56 @@ test.describe("Past booking cancellation", () => {
|
||||
await expect(page.locator('[data-testid="cancel"]')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Optional Email Field Validation", () => {
|
||||
test("should validate format if input is provided, but allow empty submission", async ({ page, users }) => {
|
||||
|
||||
const user = await users.create({
|
||||
eventTypes: [
|
||||
{
|
||||
title: "Test Event",
|
||||
slug: "test-event",
|
||||
length: 30,
|
||||
bookingFields: [
|
||||
{ name: "name", type: "name", required: true },
|
||||
{ name: "email", type: "email", required: true },
|
||||
{
|
||||
name: "altEmail",
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Alternate Email"
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`/${user.username}/test-event`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('input[name="name"]').fill('Test Booker');
|
||||
await page.locator('input[name="email"]').fill('booker@example.com');
|
||||
|
||||
const optionalField = page.getByLabel(/Alternate Email/i);
|
||||
|
||||
// Visibility
|
||||
await expect(optionalField).toBeVisible();
|
||||
|
||||
// Trigger validation error
|
||||
await optionalField.fill('not-an-email');
|
||||
await page.click('[data-testid="confirm-book-button"]');
|
||||
|
||||
// Verify form didn't submit
|
||||
await expect(page).not.toHaveURL(/.*isSuccessBookingPage=true.*/);
|
||||
|
||||
// Verify empty submission is allowed
|
||||
await optionalField.fill('');
|
||||
|
||||
// Click the button to submit
|
||||
await page.click('[data-testid="confirm-book-button"]');
|
||||
|
||||
// Verify it reached the end
|
||||
await expect(page).toHaveURL(/.*isSuccessBookingPage=true.*/, { timeout: 15000 });
|
||||
await expect(page.getByTestId('success-page')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1854,4 +1854,73 @@ describe("require email/domain validation", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(`optional email field should fail if invalid input is provided`, async () => {
|
||||
const schema = getBookingResponsesSchema({
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "optionalEmail",
|
||||
type: "email",
|
||||
required: false,
|
||||
},
|
||||
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
view: "ALL_VIEWS",
|
||||
});
|
||||
|
||||
const parsedResponses = await schema.safeParseAsync({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
optionalEmail: "not-a-valid-email",
|
||||
});
|
||||
|
||||
expect(parsedResponses.success).toBe(false);
|
||||
if (!parsedResponses.success) {
|
||||
expect(parsedResponses.error.issues[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
message: `{optionalEmail}${CUSTOM_EMAIL_VALIDATION_ERROR_MSG}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(`optional email field should pass if left empty`, async () => {
|
||||
const schema = getBookingResponsesSchema({
|
||||
bookingFields: [
|
||||
{
|
||||
name: "name",
|
||||
type: "name",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
type: "email",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "optionalEmail",
|
||||
type: "email",
|
||||
required: false,
|
||||
},
|
||||
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
view: "ALL_VIEWS",
|
||||
});
|
||||
|
||||
const parsedResponses = await schema.safeParseAsync({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
optionalEmail: "",
|
||||
});
|
||||
|
||||
expect(parsedResponses.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,24 @@ import { 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 {
|
||||
bookingResponses,
|
||||
emailSchemaRefinement,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
|
||||
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 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;
|
||||
};
|
||||
|
||||
export const bookingResponse = dbReadResponseSchema;
|
||||
export const bookingResponsesDbSchema = z.record(dbReadResponseSchema);
|
||||
@@ -46,18 +58,38 @@ const doesEmailMatchEntry = (bookerEmail: string, entry: string): boolean => {
|
||||
|
||||
return bookerEmailLower.endsWith("@" + entry.toLowerCase());
|
||||
};
|
||||
export const getBookingResponsesPartialSchema = ({ bookingFields, view, translateFn }: CommonParams) => {
|
||||
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
|
||||
// - 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({ bookingFields, view, translateFn }: CommonParams) {
|
||||
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
|
||||
@@ -97,7 +129,8 @@ function preprocess<T extends z.ZodType>({
|
||||
const log = logger.getSubLogger({ prefix: ["getBookingResponsesSchema"] });
|
||||
const preprocessed = z.preprocess(
|
||||
(responses) => {
|
||||
const parsedResponses = z.record(z.any()).nullable().parse(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 (!bookingFields) return parsedResponses;
|
||||
@@ -109,12 +142,17 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
const views = field.views;
|
||||
const isFieldApplicableToCurrentView =
|
||||
currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true;
|
||||
currentView === "ALL_VIEWS"
|
||||
? true
|
||||
: views
|
||||
? views.find((view) => view.id === currentView)
|
||||
: true;
|
||||
if (!isFieldApplicableToCurrentView) {
|
||||
// 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];
|
||||
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({
|
||||
@@ -129,7 +167,11 @@ function preprocess<T extends z.ZodType>({
|
||||
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") {
|
||||
else if (
|
||||
field.type === "multiemail" ||
|
||||
field.type === "checkbox" ||
|
||||
field.type === "multiselect"
|
||||
) {
|
||||
newResponses[field.name] = value instanceof Array ? value : [value];
|
||||
}
|
||||
// Parse JSON
|
||||
@@ -141,12 +183,17 @@ function preprocess<T extends z.ZodType>({
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch (e) {
|
||||
log.error(`Failed to parse JSON for field ${field.name}: ${value}`, 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);
|
||||
parsedValue.optionValue = ensureValidPhoneNumber(
|
||||
parsedValue.optionValue
|
||||
);
|
||||
}
|
||||
newResponses[field.name] = parsedValue;
|
||||
} else if (field.type === "phone") {
|
||||
@@ -167,7 +214,9 @@ function preprocess<T extends z.ZodType>({
|
||||
return;
|
||||
}
|
||||
|
||||
const attendeePhoneNumberField = bookingFields.find((field) => field.name === "attendeePhoneNumber");
|
||||
const attendeePhoneNumberField = bookingFields.find(
|
||||
(field) => field.name === "attendeePhoneNumber"
|
||||
);
|
||||
const isAttendeePhoneNumberFieldHidden = attendeePhoneNumberField?.hidden;
|
||||
|
||||
const emailField = bookingFields.find((field) => field.name === "email");
|
||||
@@ -181,7 +230,9 @@ 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 emailSchema = isPartialSchema
|
||||
? z.string()
|
||||
: z.string().refine(emailSchemaRefinement);
|
||||
const phoneSchema = isPartialSchema
|
||||
? z.string()
|
||||
: z.string().refine(async (val) => {
|
||||
@@ -189,12 +240,18 @@ function preprocess<T extends z.ZodType>({
|
||||
});
|
||||
// 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;
|
||||
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;
|
||||
currentView === "ALL_VIEWS"
|
||||
? true
|
||||
: views
|
||||
? views.find((view) => view.id === currentView)
|
||||
: true;
|
||||
let hidden = bookingField.hidden;
|
||||
const numOptions = bookingField.options?.length ?? 0;
|
||||
if (bookingField.hideWhenJustOneOption) {
|
||||
@@ -211,12 +268,18 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
|
||||
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`),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (bookingField.type === "email") {
|
||||
if (!bookingField.hidden && (checkOptional || bookingField.required)) {
|
||||
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({
|
||||
@@ -226,37 +289,46 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
|
||||
// validate the excluded emails
|
||||
const bookerEmail = value;
|
||||
const excludedEmails =
|
||||
bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || [];
|
||||
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"),
|
||||
});
|
||||
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];
|
||||
const fieldTypeSchema =
|
||||
fieldTypesSchemaMap[
|
||||
bookingField.type as keyof typeof fieldTypesSchemaMap
|
||||
];
|
||||
|
||||
if (fieldTypeSchema) {
|
||||
fieldTypeSchema.superRefine({
|
||||
@@ -273,13 +345,19 @@ function preprocess<T extends z.ZodType>({
|
||||
const emailsParsed = emailSchema.array().safeParse(value);
|
||||
|
||||
if (isRequired && (!value || value.length === 0)) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
|
||||
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 === "")) {
|
||||
if (
|
||||
bookingField.name === "guests" &&
|
||||
value.every((email: string) => email === "")
|
||||
) {
|
||||
// reset guests to empty array, otherwise it adds "" for every input
|
||||
responses[bookingField.name] = [];
|
||||
continue;
|
||||
@@ -294,7 +372,10 @@ function preprocess<T extends z.ZodType>({
|
||||
const emails = emailsParsed.data;
|
||||
emails.sort().some((item, i) => {
|
||||
if (item === emails[i + 1]) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("duplicate_email") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("duplicate_email"),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -303,18 +384,27 @@ function preprocess<T extends z.ZodType>({
|
||||
|
||||
if (bookingField.type === "multiselect") {
|
||||
if (isRequired && (!value || value.length === 0)) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m(`error_required_field`) });
|
||||
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") });
|
||||
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") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("Invalid array of strings"),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -326,7 +416,10 @@ function preprocess<T extends z.ZodType>({
|
||||
// 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") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("invalid_number"),
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -335,7 +428,10 @@ function preprocess<T extends z.ZodType>({
|
||||
if (bookingField.type === "boolean") {
|
||||
const schema = z.boolean();
|
||||
if (!schema.safeParse(value).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid Boolean") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("Invalid Boolean"),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -347,9 +443,14 @@ function preprocess<T extends z.ZodType>({
|
||||
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
|
||||
(isRequired || value?.value) && checkOptional
|
||||
? true
|
||||
: optionField?.required && !optionValue
|
||||
) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("error_required_field") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("error_required_field"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,7 +458,10 @@ function preprocess<T extends z.ZodType>({
|
||||
// `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") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("invalid_number"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,10 +471,17 @@ function preprocess<T extends z.ZodType>({
|
||||
|
||||
// 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)) {
|
||||
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") });
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("Invalid string"),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -385,7 +496,10 @@ function preprocess<T extends z.ZodType>({
|
||||
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);
|
||||
console.error(
|
||||
"Failed to preprocess query params, prefilling will be skipped",
|
||||
res?.error
|
||||
);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -901,7 +901,9 @@ export const emailSchema = emailRegexSchema;
|
||||
// The PR at https://github.com/colinhacks/zod/pull/2157 addresses this issue and improves email validation
|
||||
// I introduced this refinement(to be used with z.email()) as a short term solution until we upgrade to a zod
|
||||
// version that will include updates in the above PR.
|
||||
export const emailSchemaRefinement = (value: string) => {
|
||||
export const emailSchemaRefinement = (value: string | null | undefined) => {
|
||||
// If there's no value, it's NOT a valid email format, so return false.
|
||||
if (!value) return false;
|
||||
return emailSchema.safeParse(value).success;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user