n4s
Version:
typed schema validation version of enforce
152 lines (135 loc) • 4.29 kB
text/typescript
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>>;