UNPKG

@palmares/schemas

Version:

This defines a default schema definition for validation of data, it abstract popular schema validation libraries like zod, yup, valibot and others"

670 lines 28.3 kB
import type { DefinitionsOfSchemaType, ValidationFallbackCallbackReturnType } from './types'; import type { SchemaAdapter } from '../adapter'; import type { FieldAdapter } from '../adapter/fields'; import type { ValidationDataBasedOnType } from '../adapter/types'; import type { Validator } from '../validators/utils'; import type { StandardSchemaV1 } from '@standard-schema/spec'; export declare class Schema<TType extends { input: any; validate: any; internal: any; output: any; representation: any; } = { input: any; validate: any; internal: any; output: any; representation: any; }, TDefinitions extends DefinitionsOfSchemaType = DefinitionsOfSchemaType<SchemaAdapter & Palmares.PSchemaAdapter>> implements StandardSchemaV1<TType['input'], TType['output']> { readonly '~standard': StandardSchemaV1.Props<TType['input'], TType['output']>; protected $$type: string; protected fieldType: string; protected __beforeValidationCallbacks: Map<string, (adapterToUse: SchemaAdapter, fieldAdapter: FieldAdapter, schema: Schema<any, any> & { __validateByAdapter: Schema<any, any>['__validateByAdapter']; }, translatedSchemas: any[], value: TType['input'], path: ValidationFallbackCallbackReturnType['errors'][number]['path'], options: Parameters<Schema['__transformToAdapter']>[0]) => ReturnType<Schema['__validateByAdapter']>>; protected __cachedGetParent?: () => Schema<any, any>; protected set __getParent(value: () => Schema<any, any>); protected get __getParent(): (() => Schema<any, any>) | undefined; protected __alreadyAppliedModel?: Promise<any>; protected __runBeforeParseAndData?: (self: any) => Promise<void>; protected __rootFallbacksValidator: Validator; protected __saveCallback?: ((value: any) => (context: any) => Promise<TType['output']>) | ((value: any) => Promise<TType['output']>); protected __modelOmitCallback?: () => void; protected __parsers: Record<'high' | 'medium' | 'low', Map<string, (value: any) => { value: any; preventNextParsers: boolean; } | Promise<{ value: any; preventNextParsers: boolean; }>>> & { _fallbacks: Set<string>; }; protected __refinements: ((args: { value: any; context: any; }) => Promise<void | undefined | { code: string; message: string; }>)[]; protected __nullable: { message: string; allow: boolean; }; protected __optional: { message: string; allow: boolean; }; protected __extends: { callback: (schema: any) => any; toStringCallback?: (schemaAsString: string) => string; } | undefined; protected __transformedSchemas: Record<string, { transformed: boolean; adapter: TDefinitions['schemaAdapter']; schemas: any[]; }>; protected __defaultFunction: (() => Promise<TType['input'] | TType['output']>) | undefined; protected __toRepresentation: ((value: TType['output']) => TType['output']) | undefined; protected __toValidate: ((value: TType['input'], context: TDefinitions['context']) => TType['validate']) | undefined; protected __toInternal: ((value: TType['validate']) => TType['internal']) | undefined; protected __type: { message: string; check: (value: TType['input']) => boolean; }; protected __getDefaultTransformedSchemas(): void; /** * This will validate the data with the fallbacks, so internally, without relaying on the schema adapter. * This is nice because we can support things that the schema adapter is not able to support by default. * * @param errorsAsHashedSet - The errors as a hashed set. This is used to prevent duplicate errors. * @param path - The path of the error. * @param parseResult - The result of the parse method. */ private __validateByFallbacks; /** * This will validate by the adapter. In other words, we send the data to the schema adapter and then we validate * that data. * So understand that, first we send the data to the adapter, the adapter validates it, then, after we validate * from the adapter we validate with the fallbacks so we can do all of the extra validations not handled by * the adapter. * * @param value - The value to be validated. * @param errorsAsHashedSet - The errors as a hashed set. This is used to prevent duplicate errors on the validator. * @param path - The path of the error so we can construct an object with the nested paths of the error. * @param parseResult - The result of the parse method. * * @returns The result and the errors of the parse method. */ protected __validateByAdapter(adapter: SchemaAdapter, fieldAdapter: FieldAdapter, schema: any, value: TType['input'], path: NonNullable<Parameters<Schema['__parse']>[1]>, options: Parameters<Schema['__transformToAdapter']>[0]): Promise<{ errors: any[]; parsed: any; }>; protected __transformToAdapter(_options: { args: Omit<ValidationDataBasedOnType<any>, 'withFallback'>; force?: boolean; toInternalToBubbleUp?: (() => Promise<void>)[]; schemaAdapter?: SchemaAdapter; errorsAsHashedSet?: Set<string>; shouldAddStringVersion?: boolean; context?: any; appendFallbacksBeforeAdapterValidation?: (schema: Schema<any, any>, uniqueNameOfFallback: string, fallbackValidationBeforeAdapter: (adapterToUse: SchemaAdapter, fieldAdapter: FieldAdapter, schema: Omit<Schema<any, any>, '__validateByAdapter'> & { __validateByAdapter: Schema<any, any>['__validateByAdapter']; }, translatedSchemas: any[], value: any, path: ValidationFallbackCallbackReturnType['errors'][number]['path'], options: Parameters<Schema['__transformToAdapter']>[0]) => ReturnType<Schema['__validateByAdapter']>) => void; }): Promise<{ transformed: ReturnType<FieldAdapter['translate']>; asString: string; }[]>; /** */ protected __parsersToTransformValue(value: any, parsersToUse?: Set<string>): Promise<any>; protected __parse(value: TType['input'], path: ValidationFallbackCallbackReturnType["errors"][number]["path"] | undefined, options: Parameters<Schema['__transformToAdapter']>[0]): Promise<{ errors: any[]; parsed: TType['internal']; }>; /** * This let's you refine the schema with custom validations. This is useful when you want to validate something * that is not supported by default by the schema adapter. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().refine((value) => { * if (value < 0) return { code: 'invalid_number', message: 'The number should be greater than 0' }; * }); * * const { errors, parsed } = await numberSchema.parse(-1); * * console.log(errors); * // [{ isValid: false, code: 'invalid_number', message: 'The number should be greater than 0', path: [] }] * ``` * * @param refinementCallback - The callback that will be called to validate the value. */ refine<TRefinementCallback extends (args: { value: TType['input']; context: TDefinitions['context']; }) => Promise<void | undefined | { code: string; message: string; }>>(refinementCallback: TRefinementCallback): Schema<{ input: TType['input']; validate: TType['validate']; internal: TType['internal']; output: TType['output']; representation: TType['representation']; }, TDefinitions>; /** * Allows the value to be either undefined or null. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().optional(); * * const { errors, parsed } = await numberSchema.parse(undefined); * * console.log(parsed); // undefined * * const { errors, parsed } = await numberSchema.parse(null); * * console.log(parsed); // null * * const { errors, parsed } = await numberSchema.parse(1); * * console.log(parsed); // 1 * ``` * * @returns - The schema we are working with. */ optional(options?: { message?: string; allow?: false; }): Schema<{ input: TType["input"] | undefined | null; validate: TType["validate"] | undefined | null; internal: TType["internal"] | undefined | null; output: TType["output"] | undefined | null; representation: TType["representation"] | undefined | null; }, TDefinitions>; /** * Allows the value to be null and ONLY null. You can also use this function to set a custom message when * the value is NULL by setting the { message: 'Your custom message', allow: false } on the options. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().nullable(); * * const { errors, parsed } = await numberSchema.parse(null); * * console.log(parsed); // null * * const { errors, parsed } = await numberSchema.parse(undefined); * * console.log(errors); // [{ isValid: false, code: 'invalid_type', message: 'Invalid type', path: [] }] * ``` * * @param options - The options for the nullable function. * @param options.message - The message to be shown when the value is not null. Defaults to 'Cannot be null'. * @param options.allow - Whether the value can be null or not. Defaults to true. * * @returns The schema. */ nullable(options?: { message: string; allow: false; }): Schema<{ input: TType["input"] | null; validate: TType["validate"] | null; internal: TType["internal"] | null; output: TType["output"] | null; representation: TType["representation"] | null; }, TDefinitions>; /** * Appends a custom schema to the schema, this way it will bypass the creation of the schema in runtime. * * By default when validating, on the first validation we create the schema. Just during the first validation. * With this function, you bypass that, so you can speed up the validation process. * * @example * ```typescript * import * as p from '@palmares/schemas'; * import * as z from 'zod'; * * const numberSchema = p.number().appendSchema(z.number()); * * const { errors, parsed } = await numberSchema.parse(1); * ``` * * @param schema - The schema to be appended. * @param args - The arguments for the schema. * @param args.adapter - The adapter to be used. If not provided, the default adapter will be used. * * @returns The same schema again. */ appendSchema<TType extends any = any>(schema: TDefinitions['schemaAdapter'], args?: { adapter?: SchemaAdapter; }): Schema<{ input: TType; validate: TType; internal: TType; output: TType; representation: TType; }, TDefinitions>; /** * This method will remove the value from the representation of the schema. If the value is undefined it will keep * that way otherwise it will set the value to undefined after it's validated. * This is used in conjunction with the {@link data} function, the {@link parse} function or {@link validate} * function. This will remove the value from the representation of the schema. * * By default, the value will be removed just from the representation, in other words, when you call the {@link data} * function. But if you want to remove the value from the internal representation, you can pass the argument * `toInternal` as true. Then if you still want to remove the value from the representation, you will need to pass * the argument `toRepresentation` as true as well. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const userSchema = p.object({ * id: p.number().optional(), * name: p.string(), * password: p.string().omit() * }); * * const user = await userSchema.data({ * id: 1, * name: 'John Doe', * password: '123456' * }); * * console.log(user); // { id: 1, name: 'John Doe' } * ``` * * * @param args - By default, the value will be removed just from the representation, in other words, when you call * the {@link data} function. * But if you want to remove the value from the internal representation, you can pass the argument `toInternal` * as true. Then if you still want to remove the value from the representation, you will need to pass the * argument `toRepresentation` as true as well. * * @returns The schema. */ omit<TToInternal extends boolean, TToRepresentation extends boolean = boolean extends TToInternal ? true : false>(args?: { toInternal?: TToInternal; toRepresentation?: TToRepresentation; }): Schema<{ input: TToInternal extends true ? TType["input"] | undefined : TType["input"]; validate: TToInternal extends true ? TType["validate"] | undefined : TType["validate"]; internal: TToInternal extends true ? undefined : TType["internal"]; output: TToRepresentation extends true ? TType["output"] | undefined : TType["output"]; representation: TToRepresentation extends true ? undefined : TType["representation"]; }, TDefinitions>; /** * This function is used in conjunction with the {@link validate} function. It's used to save a value to an external * source like a database. You should always return the schema after you save the value, that way we will always have * the correct type of the schema after the save operation. * * You can use the {@link toRepresentation} function to transform and clean the value it returns after the save. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * import { User } from './models'; * * const userSchema = p.object({ * id: p.number().optional(), * name: p.string(), * email: p.string().email(), * }).onSave(async (value) => { * // Create or update the user on the database using palmares models or any other library of your choice. * if (value.id) * await User.default.set(value, { search: { id: value.id } }); * else * await User.default.set(value); * * return value; * }); * * * // Then, on your controller, do something like this: * const { isValid, save, errors } = await userSchema.validate(req.body); * if (isValid) { * const savedValue = await save(); * return Response.json(savedValue, { status: 201 }); * } * * return Response.json({ errors }, { status: 400 }); * ``` * * @param callback - The callback that will be called to save the value on an external source. * * @returns The schema. */ onSave<TSave extends ((value: TType['internal']) => (context: unknown) => Promise<TType['output']>) | ((value: TType['internal']) => Promise<TType['output']>)>(callback: TSave): Schema<{ input: TType['input']; validate: TType['validate']; internal: TType['internal']; output: TType['output']; representation: TType['representation']; }, Omit<TDefinitions, 'hasSave' | 'context'> & { hasSave: true; context: ReturnType<TSave> extends (context: any) => any ? Parameters<ReturnType<TSave>>[0] : any; }>; /** * This function is used to validate the schema and save the value to the database. It is used in * conjunction with the {@link onSave} function. * * Different from other validation libraries, palmares schemas is aware that you want to save. On your * routes/functions we recommend to ALWAYS use this function instead of {@link parse} directly. This is because * this function by default will return an object with the property `save` or the `errors`. If the errors are present, * you can return the errors to the user. If the save property is present, you can use to save the value to an * external source. e.g. a database. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * import { User } from './models'; * * const userSchema = p.object({ * id: p.number().optional(), * name: p.string(), * email: p.string().email(), * }).onSave(async (value) => { * // Create or update the user on the database using palmares models or any other library of your choice. * if (value.id) * await User.default.set(value, { search: { id: value.id } }); * else * await User.default.set(value); * * return value; * }); * * * // Then, on your controller, do something like this: * const { isValid, save, errors } = await userSchema.validate(req.body); * if (isValid) { * const savedValue = await save(); * return Response.json(savedValue, { status: 201 }); * } * * return Response.json({ errors }, { status: 400 }); * ``` * * @param value - The value to be validated. * * @returns An object with the property isValid, if the value is valid, the function `save` will be present. * If the value is invalid, the property errors will be present. */ validate(value: unknown, context?: TDefinitions['context']): Promise<{ isValid: false; errors: any[]; save: undefined; } | { isValid: true; save: () => Promise<TType['representation']>; errors: undefined; }>; /** * Internal function, when we call the {@link validate} function it's this function that gets called * when the user uses the `save` function returned by the {@link validate} function if the value is valid. * * @param value - The value to be saved. * * @returns The value to representation. */ protected _save(value: TType['input'], context: any): Promise<TType['representation']>; /** * This function is used to validate and parse the value to the internal representation of the schema. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().allowString(); * * const { errors, parsed } = await numberSchema.parse('123'); * * console.log(parsed); // 123 * ``` * * @param value - The value to be parsed. * * @returns The parsed value. */ parse(value: unknown): Promise<{ errors?: any[]; parsed: TType['internal']; }>; /** * This function is used to transform the value to the representation without validating it. * This is useful when you want to return a data from a query directly to the user. But for example * you are returning the data of a user, you can clean the password or any other sensitive data. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const userSchema = p.object({ * id: p.number().optional(), * name: p.string(), * email: p.string().email(), * password: p.string().optional() * }).toRepresentation(async (value) => { * return { * id: value.id, * name: value.name, * email: value.email * } * }); * * const user = await userSchema.data({ * id: 1, * name: 'John Doe', * email: 'john@gmail.com', * password: '123456' * }); * ``` */ data(value: TType['output']): Promise<TType['representation']>; instanceOf(args: Schema['__type']): Schema<{ input: TType["input"]; validate: TType["validate"]; internal: TType["internal"]; output: TType["output"]; representation: TType["representation"]; }, TDefinitions>; /** * This function is used to add a default value to the schema. If the value is either undefined or null, * the default value will be used. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().default(0); * * const { errors, parsed } = await numberSchema.parse(undefined); * * console.log(parsed); // 0 * ``` */ default<TDefaultValue extends TType['input'] | (() => Promise<TType['input']>)>(defaultValueOrFunction: TDefaultValue): Schema<{ input: TType["input"] | undefined | null; validate: TType["validate"]; internal: TType["internal"]; output: TType["output"] | undefined | null; representation: TType["representation"]; }, TDefinitions>; /** * This function let's you customize the schema your own way. After we translate the schema on the adapter we call * this function to let you customize the custom schema your own way. Our API does not support passthrough? * No problem, you can use this function to customize the zod schema. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const numberSchema = p.number().extends((schema) => { * return schema.nonnegative(); * }); * * const { errors, parsed } = await numberSchema.parse(-1); * * console.log(errors); * // [{ isValid: false, code: 'nonnegative', message: 'The number should be nonnegative', path: [] }] * ``` * * @param callback - The callback that will be called to customize the schema. * @param toStringCallback - The callback that will be called to transform the schema to a string when you want * to compile the underlying schema to a string so you can save it for future runs. * * @returns The schema. */ extends(callback: (schema: Awaited<ReturnType<NonNullable<TDefinitions['schemaAdapter'][TDefinitions['schemaType']]>['translate']>>) => Awaited<ReturnType<NonNullable<TDefinitions['schemaAdapter'][TDefinitions['schemaType']]>['translate']>>, toStringCallback?: (schemaAsString: string) => string): this; /** * This function is used to transform the value to the representation of the schema. When using the {@link data} * function. With this function you have full control to add data cleaning for example, transforming the data * and whatever. Another use case is when you want to return deeply nested recursive data. * The schema maps to itself. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const recursiveSchema = p.object({ * id: p.number().optional(), * name: p.string(), * }).toRepresentation(async (value) => { * return { * id: value.id, * name: value.name, * children: await Promise.all(value.children.map(async (child) => await recursiveSchema.data(child))) * } * }); * * const data = await recursiveSchema.data({ * id: 1, * name: 'John Doe', * }); * ``` * * @example * ``` * import * as p from '@palmares/schemas'; * * const colorToRGBSchema = p.string().toRepresentation(async (value) => { * switch (value) { * case 'red': return { r: 255, g: 0, b: 0 }; * case 'green': return { r: 0, g: 255, b: 0 }; * case 'blue': return { r: 0, g: 0, b: 255 }; * default: return { r: 0, g: 0, b: 0 }; * } * }); * ``` * @param toRepresentationCallback - The callback that will be called to transform the value to the representation. * @param options - Options for the toRepresentation function. * @param options.after - Whether the toRepresentationCallback should be called after the existing * toRepresentationCallback. Defaults to true. * @param options.before - Whether the toRepresentationCallback should be called before the existing * toRepresentationCallback. Defaults to true. * * @returns The schema with a new return type */ toRepresentation<TRepresentation>(toRepresentationCallback: (value: TType['representation']) => Promise<TRepresentation> | TRepresentation, options?: { after?: boolean; before?: boolean; }): Schema<{ input: TType["input"]; validate: TType["validate"]; internal: TType["internal"]; output: TType["output"]; representation: TRepresentation; }, TDefinitions>; /** * This function is used to transform the value to the internal representation of the schema. This is useful * when you want to transform the value to a type that the schema adapter can understand. For example, you * might want to transform a string to a date. This is the function you use. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const dateSchema = p.string().toInternal((value) => { * return new Date(value); * }); * * const date = await dateSchema.parse('2021-01-01'); * * console.log(date); // Date object * * const rgbToColorSchema = p.object({ * r: p.number().min(0).max(255), * g: p.number().min(0).max(255), * b: p.number().min(0).max(255), * }).toInternal(async (value) => { * if (value.r === 255 && value.g === 0 && value.b === 0) return 'red'; * if (value.r === 0 && value.g === 255 && value.b === 0) return 'green'; * if (value.r === 0 && value.g === 0 && value.b === 255) return 'blue'; * return `rgb(${value.r}, ${value.g}, ${value.b})`; * }); * ``` * * @param toInternalCallback - The callback that will be called to transform the value to the internal representation. * * @returns The schema with a new return type. */ toInternal<TInternal>(toInternalCallback: (value: TType['validate']) => Promise<TInternal>): Schema<{ input: TType["input"]; validate: TType["validate"]; internal: TInternal; output: TType["output"]; representation: TType["representation"]; }, TDefinitions>; /** * Called before the validation of the schema. Let's say that you want to validate a date that might receive a * string, you can convert that string to a date here BEFORE the validation. This pretty much transforms the value * to a type that the schema adapter can understand. * * @example * ``` * import * as p from '@palmares/schemas'; * import * as z from 'zod'; * * const customRecordToMapSchema = p.schema().appendSchema(z.map()).toValidate(async (value) => { * return new Map(value); // Before validating we transform the value to a map. * }); * * const { errors, parsed } = await customRecordToMapSchema.parse({ key: 'value' }); * ``` * * @param toValidateCallback - The callback that will be called to validate the value. * * @returns The schema with a new return type. */ toValidate<TValidate>(toValidateCallback: (value: TType['input'], context: TDefinitions['context']) => Promise<TValidate> | TValidate): Schema<{ input: TType["input"]; validate: TValidate; internal: TType["internal"]; output: TType["output"]; representation: TType["representation"]; }, TDefinitions>; /** * Used to transform the given schema on a stringfied version of the adapter. */ compile(adapter: SchemaAdapter): Promise<string[]>; static new<TType extends { input: any; output: any; internal: any; representation: any; validate: any; }, TDefinitions extends DefinitionsOfSchemaType = DefinitionsOfSchemaType<SchemaAdapter & Palmares.PSchemaAdapter>>(..._args: any[]): Schema<TType, TDefinitions>; } export declare const schema: typeof Schema.new; //# sourceMappingURL=schema.d.ts.map