UNPKG

validata-mongo

Version:
146 lines (120 loc) 5.05 kB
import { Contract, isIssue, Issue, Path, Result, ValueProcessor } from 'validata'; import { basicValidation, Check, Coerce, CommonValidationOptions, Convert, createIsCheck, Validate } from 'validata/dev'; const DEEP = Symbol('deep'); interface AdditionalOptions { stripExtraProperties?: boolean; } interface CoerceOptions<T> extends AdditionalOptions { contract?: Contract<T>; } interface ValidationOptions<T> extends CommonValidationOptions<T> { } const tidyDeepPath = (path: Path[]): Path[] => { return path .reduce((acc, item) => { if (acc.length) { const lastItem = acc[acc.length - 1]; if (acc.length > 1) { const prevItem = acc[acc.length - 2]; if (lastItem === DEEP && typeof prevItem === 'string' && typeof item === 'string') { return [...acc.slice(0, -2), `${prevItem}.${item}`]; } } if (lastItem === DEEP) { return [...acc.slice(0, -1), item]; } } return [...acc, item]; }, [] as Path[]) .filter((item) => item !== DEEP); }; const tidyDeepIssuePaths = (issues: Issue[]): Issue[] => issues.map((issue) => Issue.forPath( tidyDeepPath(issue.path), issue.value, issue.reason, issue.info, )); class Generic<T extends Record<string, any>> { public check: Check<T> = (value: unknown): value is T => { return typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date); } public convert: Convert<T> = (value) => { if (typeof value === 'string' && value[0] === '{' && value[value.length - 1] === '}') { try { return JSON.parse(value) as T; } catch { return undefined; } } return undefined; }; public process = (contract: Contract<T>, target: T, path: Path[]): Result<Record<string, unknown>> => { const issues: Issue[] = []; const output = {} as T; (Object.keys(target) as Array<keyof T>).forEach((key) => { const parts = (key as string).split('.'); const firstPart = parts[0]; if (!(firstPart in contract)) { issues.push( Issue.forPath([...path, key], target[key], 'unexpected-property'), ); return; } const childKey = parts.slice(1).join('.'); const check = contract[firstPart]; const value = target[key]; // disallow child objects with dots in the property names if (value && this.check(value)) { Object.keys(value).forEach((nestedKey) => { if (!nestedKey.includes('.')) return; issues.push(Issue.forPath([...path, key, nestedKey], value[nestedKey], 'unexpected-property')); }); } const directProperty = parts.length === 1; const childPath = directProperty ? [...path, key] : [...path, firstPart, DEEP]; const childResult = check.process(directProperty ? value : { [childKey]: value }, childPath); if (isIssue(childResult)) { issues.push(...tidyDeepIssuePaths(childResult.issues)); return; } if (childResult.value === undefined && !(key in target)) return; output[key] = parts.length === 1 ? childResult.value : childResult.value[childKey as keyof T]; }); // require all properties - not for root object & not for deep nesting if (path.length && path[path.length - 1] !== DEEP) { const targetKeys = new Set<Path>(Object.keys(target).map((key) => key.split('.')[0])); const missingKeys = Object.keys(contract).filter((key) => !targetKeys.has(key)); missingKeys.forEach((key) => { const childResult = contract[key].process(undefined, [...path, key]); if (isIssue(childResult)) { issues.push(...tidyDeepIssuePaths(childResult.issues)); } }); } return issues.length ? { issues } : { value: output }; } public coerce: Coerce<T, CoerceOptions<T>> = (options) => (next) => (value, path) => { if (!options) return next(value, path); const coerced = { ...value }; if (!options.contract) return next(coerced, path); if (options.stripExtraProperties) { const allowedProperties = new Set(Object.keys(options.contract)); (Object.keys(coerced) as (keyof T)[]).forEach((key) => { if (allowedProperties.has(key as string)) return; delete coerced[key]; }); } const result = this.process(options.contract, coerced, path); if (isIssue(result)) return result; if (result) { return next(result.value as any, path); } else { return next(coerced, path); } } public validate: Validate<T, ValidationOptions<T>> = (value, path, options) => basicValidation(value, path, options); } export type ObjectOptions<T> = ValidationOptions<T> & AdditionalOptions; export const isObjectSet = <T extends Record<string, any>>(contract?: Contract<T>, options?: ObjectOptions<T>): ValueProcessor<T & Record<string, unknown>> => { const generic = new Generic<T>(); return createIsCheck('object', generic.check, generic.coerce, generic.validate)({ ...options, contract }); };