UNPKG

n4s

Version:

typed schema validation version of enforce

152 lines (135 loc) 4.29 kB
import { hasOwnProperty, isObject } from 'vest-utils'; import { ctx } from '../../enforceContext'; import type { RuleInstance } from '../../utils/RuleInstance'; import { RuleRunReturn } from '../../utils/RuleRunReturn'; import { findDangerousOwnKey, ownKeys, safeShallowCopy, } from './schemaObjectUtils'; import type { ShapeInputType, ShapeType } from './shape'; /** * Checks if value has any keys not present in schema. */ function getFirstExtraKey<T extends Record<string, any>>( value: T, schema: Record<string, any>, ): string | null { for (const key of ownKeys(value)) { if (!hasOwnProperty(schema, key)) { return key; } } return null; } /** * Validates provided keys against their schema rules and returns parsed entries. * * Missing keys are allowed (partial validation). */ function validateProvidedKeys<T extends Record<string, any>>( value: T, schema: Record<string, any>, ): RuleRunReturn<T> | { parsedEntries: Record<string, any> } { const parsedEntries: Record<string, any> = {}; for (const key of ownKeys(schema)) { if (hasOwnProperty(value, key)) { const fieldValue = value[key]; const res = ctx.run({ value: fieldValue, set: true, meta: { key } }, () => schema[key].run(fieldValue), ); if (!res.pass) { const currentPath = res.path || []; return { ...res, path: [key, ...currentPath], } as RuleRunReturn<T>; } parsedEntries[key] = res.type; } } return { parsedEntries }; } /** * partial(value, schema) validates that: * 1. value's keys are a subset of schema's keys (no extras) * 2. Zero or more keys may be present (empty object is allowed) * 3. For each provided key, the corresponding rule passes */ /** * Validates that an object partially matches a schema - schema keys are optional, no extra keys allowed. * All provided keys must exist in schema and pass their validation rules. * Missing keys are allowed (making all fields optional). * * @template T - The object type to validate * @param value - The object to validate * @param schema - Schema mapping keys to validation rules * @returns RuleRunReturn indicating success or failure * * @example * ```typescript * // Eager API * enforce({ name: 'John' }) * .partial({ * name: enforce.isString(), * age: enforce.isNumber(), * email: enforce.isString() * }); // passes (age and email are optional) * * // Lazy API * const updateSchema = enforce.partial({ * name: enforce.isString(), * email: enforce.isString().matches(/@/), * age: enforce.isNumber() * }); * * updateSchema.test({}); // true (all fields optional) * updateSchema.test({ name: 'Jane' }); // true (partial update) * updateSchema.test({ name: 'Jane', email: 'jane@example.com' }); // true * updateSchema.test({ name: 'Jane', extra: 'x' }); // false (extra key not in schema) * ``` */ // eslint-disable-next-line complexity export function partial<T extends Record<string, any>>( value: T, schema: Record<string, any>, ): RuleRunReturn<T> { if (!isObject(value)) { return RuleRunReturn.Failing(value); } const dangerousSchemaKey = findDangerousOwnKey(schema); if (dangerousSchemaKey) { return { ...RuleRunReturn.Failing(value), path: [dangerousSchemaKey], }; } const dangerousValueKey = findDangerousOwnKey(value); if (dangerousValueKey) { return { ...RuleRunReturn.Failing(value), path: [dangerousValueKey], }; } const extraKey = getFirstExtraKey(value, schema); if (extraKey) { return { ...RuleRunReturn.Failing(value), path: [extraKey], }; } const parsedValue = safeShallowCopy(value); const parsedEntriesOrFailure = validateProvidedKeys(value, schema); if ('pass' in parsedEntriesOrFailure) { return parsedEntriesOrFailure; } return RuleRunReturn.Passing({ ...parsedValue, ...parsedEntriesOrFailure.parsedEntries, } as T); } // Types colocated with partial rule export type PartialRuleInstance<S extends Record<string, RuleInstance<any>>> = RuleInstance<Partial<ShapeType<S>>, [Partial<ShapeInputType<S>>]>; export type PartialShapeValue<S extends Record<string, RuleInstance<any>>> = Partial<ShapeType<S>>;