Skip to content

Commit

Permalink
feat(frontend): reusable validation schema (#94)
Browse files Browse the repository at this point in the history
* feature(frontend): reusable zod validation schemas

* test(frontend): zod validation schemas unit tests
  • Loading branch information
sebastien-comeau authored Jan 21, 2025
1 parent dc7cccd commit a38cb23
Show file tree
Hide file tree
Showing 18 changed files with 1,596 additions and 2 deletions.
2 changes: 0 additions & 2 deletions frontend/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
// clsx and cn
["clsx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
// Plain Javascript Object
":\\s*?[\"'`]([^\"'`]*).*?,",
// JavaScript string variable with keywords
[
"(?:\\b(?:const|let|var)\\s+)?[\\w$_]*(?:[Ss]tyles|[Cc]lasses|[Cc]lassnames)[\\w\\d]*\\s*(?:=|\\+=)\\s*['\"]([^'\"]*)['\"]"
Expand Down
85 changes: 85 additions & 0 deletions frontend/app/.server/validation/email-address-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import isEmail from 'validator/es/lib/isEmail';
import type { z } from 'zod';

import { stringSchema } from '~/.server/validation/string-schema';

/**
* Interface defining customizable error messages for email address validation schema.
*/
export interface EmailAddressSchemaErrorMessages {
/**
* Error message for when the email address format is invalid.
* @default 'Email address format is invalid.'
*/
format_error?: string;

/**
* Error message for when the email address is not a string.
* @default 'Email address must be a string.'
*/
invalid_type_error?: string;

/**
* Error message for when the email address exceeds the maximum length.
* @default 'Email address must be less than or equal to {maximum} characters
*/
max_length_error?: string;

/**
* Error message for when the email address is required.
* @default 'Email address is required.'
*/
required_error?: string;
}

/**
* Configuration options for email address validation, including maximum length and error messages.
*/
export interface EmailAddressSchemaOptions {
errorMessages?: EmailAddressSchemaErrorMessages;
maxLength?: number;
}

const DEFAULT_MESSAGES = {
format_error: 'Email address format is invalid.',
invalid_type_error: 'Email address must be a string.',
max_length_error: 'Email address must be less than or equal to {maximum} characters.',
required_error: 'Email address is required.',
} as const satisfies Required<EmailAddressSchemaErrorMessages>;

/**
* Creates a Zod schema for validating names with customizable options.
*
* @param options - Configuration options for validation.
* @returns A Zod schema for validating names.
*/
export function emailAddressSchema(options: EmailAddressSchemaOptions = {}): z.ZodEffects<z.ZodString> {
const defaultMaxEmailLength = 254; // matches validator.js

const { errorMessages = {}, maxLength = defaultMaxEmailLength } = options;

const messages: Required<EmailAddressSchemaErrorMessages> = {
...DEFAULT_MESSAGES,
...errorMessages,
};

return stringSchema({
errorMessages: {
invalid_type_error: messages.invalid_type_error,
max_length_error: messages.max_length_error,
min_length_error: messages.required_error,
required_error: messages.required_error,
},
minLength: 1,
maxLength: Math.min(defaultMaxEmailLength, maxLength),
}).superRefine((email, ctx) => {
if (!email) return;
if (isEmail(email)) return;

ctx.addIssue({
code: 'custom',
message: messages.format_error,
fatal: true,
});
});
}
72 changes: 72 additions & 0 deletions frontend/app/.server/validation/first-name-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { z } from 'zod';

import { nameSchema } from '~/.server/validation/name-schema';

/**
* Interface defining customizable error messages for firstName validation schema.
*/
export interface FirstNameSchemaErrorMessages {
/**
* Error message for when the first name contains digits.
* @default 'First name must not contain any digits.'
*/
format_error?: string;

/**
* Error message for when the first name is not a string.
* @default 'First name must be a string.'
*/
invalid_type_error?: string;

/**
* Error message for when the first name exceeds the maximum length.
* @default 'First name must contain at most {maximum} characters.'
*/
max_length_error?: string;

/**
* Error message for when the first name is required.
* @default 'First name is required.'
*/
required_error?: string;
}

/**
* Configuration options for firstName validation, including maximum length and error messages.
*/
export interface FirstNameSchemaOptions {
errorMessages?: FirstNameSchemaErrorMessages;
maxLength?: number;
}

const DEFAULT_MESSAGES = {
format_error: 'First name must not contain any digits.',
invalid_type_error: 'First name must be a string.',
max_length_error: 'First name must contain at most {maximum} characters.',
required_error: 'First name is required.',
} as const satisfies Required<FirstNameSchemaErrorMessages>;

/**
* Creates a Zod schema for validating firstNames with customizable options.
*
* @param options - Configuration options for validation.
* @returns A Zod schema for validating firstNames.
*/
export function firstNameSchema(options: FirstNameSchemaOptions = {}): z.ZodString {
const { errorMessages = {}, maxLength = 100 } = options;

const messages: Required<FirstNameSchemaErrorMessages> = {
...DEFAULT_MESSAGES,
...errorMessages,
};

return nameSchema({
errorMessages: {
format_error: messages.format_error,
invalid_type_error: messages.invalid_type_error,
max_length_error: messages.max_length_error,
required_error: messages.required_error,
},
maxLength,
});
}
5 changes: 5 additions & 0 deletions frontend/app/.server/validation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './email-address-schema';
export * from './first-name-schema';
export * from './last-name-schema';
export * from './name-schema';
export * from './string-schema';
72 changes: 72 additions & 0 deletions frontend/app/.server/validation/last-name-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { z } from 'zod';

import { nameSchema } from '~/.server/validation/name-schema';

/**
* Interface defining customizable error messages for last name validation schema.
*/
export interface LastNameSchemaErrorMessages {
/**
* Error message for when the last name contains digits.
* @default 'Last name must not contain any digits.'
*/
format_error?: string;

/**
* Error message for when the last name is not a string.
* @default 'Last name must be a string.'
*/
invalid_type_error?: string;

/**
* Error message for when the last name exceeds the maximum length.
* @default 'Last name must contain at most {maximum} characters.'
*/
max_length_error?: string;

/**
* Error message for when the last name is required.
* @default 'Last name is required.'
*/
required_error?: string;
}

/**
* Configuration options for last name validation, including maximum length and error messages.
*/
export interface LastNameSchemaOptions {
errorMessages?: LastNameSchemaErrorMessages;
maxLength?: number;
}

const DEFAULT_MESSAGES = {
format_error: 'Last name must not contain any digits.',
invalid_type_error: 'Last name must be a string.',
max_length_error: 'Last name must contain at most {maximum} characters.',
required_error: 'Last name is required.',
} as const satisfies Required<LastNameSchemaErrorMessages>;

/**
* Creates a Zod schema for validating last names with customizable options.
*
* @param options - Configuration options for validation.
* @returns A Zod schema for validating last names.
*/
export function lastNameSchema(options: LastNameSchemaOptions = {}): z.ZodString {
const { errorMessages = {}, maxLength = 100 } = options;

const messages: Required<LastNameSchemaErrorMessages> = {
...DEFAULT_MESSAGES,
...errorMessages,
};

return nameSchema({
errorMessages: {
format_error: messages.format_error,
invalid_type_error: messages.invalid_type_error,
max_length_error: messages.max_length_error,
required_error: messages.required_error,
},
maxLength,
});
}
56 changes: 56 additions & 0 deletions frontend/app/.server/validation/name-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { z } from 'zod';

import { stringSchema } from '~/.server/validation/string-schema';

/**
* Interface defining customizable error messages for name validation schema.
*/
export interface NameSchemaErrorMessages {
format_error?: string;
invalid_type_error?: string;
max_length_error?: string;
required_error?: string;
}

/**
* Configuration options for name validation, including maximum length and error messages.
*/
export interface NameSchemaOptions {
errorMessages?: NameSchemaErrorMessages;
maxLength?: number;
}

const DEFAULT_MESSAGES = {
format_error: 'Name must not contain any digits.',
invalid_type_error: 'Name must be a string.',
max_length_error: 'Name must contain at most {maximum} characters.',
required_error: 'Name is required.',
} as const satisfies Required<NameSchemaErrorMessages>;

/**
* Creates a Zod schema for validating names with customizable options.
*
* @param options - Configuration options for validation.
* @returns A Zod schema for validating names.
*/
export function nameSchema(options: NameSchemaOptions = {}): z.ZodString {
const { errorMessages = {}, maxLength = 100 } = options;

const messages: Required<NameSchemaErrorMessages> = {
...DEFAULT_MESSAGES,
...errorMessages,
};

return stringSchema({
errorMessages: {
format_error: messages.format_error,
invalid_type_error: messages.invalid_type_error,
max_length_error: messages.max_length_error,
min_length_error: messages.required_error,
required_error: messages.required_error,
},
format: 'non-digit',
minLength: 1,
maxLength,
});
}
Loading

0 comments on commit a38cb23

Please sign in to comment.