import { difference, equals, isFunction } from 'remeda' import { RefinementCtx, SafeParseReturnType, ZodArray, ZodError, ZodIssueCode, ZodSchema, ZodType, ZodTypeDef, z } from 'zod' import { ArrayCardinality } from 'zod/lib/types' import { ensure, ensureByIndex } from './ensure' import { isEqualByD } from './lodash' import { Uid, byUid } from './uid' export interface ZodFlatError { formErrors: string[] fieldErrors: { [k: string]: string[] } } export type GetUid = (holder: UidHolder) => Uid export type Validate = (object: Obj) => Obj export type Insert = (object: Obj) => Obj export interface Model { schema: ZodSchema, validate: Validate getUid: GetUid } export interface Stat { uid: Uid count: number } export type GetUniqueValue = (object: Obj) => unknown export function getSchemaDescription(schema: ZodType) { return ensure(schema.description, () => { let schemaIdentifier: string // console.log('schema', stringify(schema)) if ('shape' in schema._def && isFunction(schema._def.shape)) { schemaIdentifier = JSON.stringify(schema._def.shape()) } else { schemaIdentifier = JSON.stringify(schema) } return new Error(`Schema does not have a description (use .describe() to add it): ${schemaIdentifier}`) }) } export function getArraySchema(schema: ZodSchema, getUniqueValue: GetUniqueValue) { const $description = getSchemaDescription(schema) + 'Array' const $schema = z.array(schema).describe($description) return withDuplicatesRefinement($schema, getUniqueValue) } export function getNonEmptyArraySchema(schema: ZodSchema, getUniqueValue: GetUniqueValue) { const $description = getSchemaDescription(schema) + 'NonEmptyArray' const $schema = z.array(schema).nonempty().describe($description) return withDuplicatesRefinement($schema, getUniqueValue) } export function withDuplicatesRefinement(schema: ZodArray, Cardinality>, getUniqueValue: GetUniqueValue) { return schema.superRefine(getDuplicatesRefinement(getSchemaDescription(schema), getUniqueValue)) } export function getDuplicatesRefinement(name: string, getUniqueValue: GetUniqueValue) { return function (objects: Obj[], context: RefinementCtx) { try { const stats = getDuplicateStats(objects, getUniqueValue) stats.map(stat => context.addIssue({ code: ZodIssueCode.custom, params: stat, message: `Found ${name} duplicates: ${JSON.stringify(stat)}`, })) } catch (error) { context.addIssue({ code: ZodIssueCode.custom, params: { error }, message: `Error while counting ${name} duplicates`, }) } } } export function getDuplicateStats(objects: Obj[], getUniqueValue: GetUniqueValue) { const stats = getUniqueCountStats(objects, getUniqueValue) return stats.filter(stat => stat.count > 1) } export function getUniqueCountStats(objects: Obj[], getUniqueValue: GetUniqueValue): Stat[] { const stats: Stat[] = [] return objects.reduce((stats, value) => { const uid = getUniqueValue(value) const index = stats.findIndex(s => equals(s.uid, uid)) const stat = stats[index] if (stat) { stat.count++ } else { stats.push({ uid, count: 1 }) } return stats }, stats) } export const insert = (name: string) => (schema: ZodType) => (getUid: GetUid) => (array: Array) => (object: Input) => { try { const $object = schema.parse(object) const duplicate = array.find(o => isEqualByD(o, $object, getUid)) if (duplicate) throw new Error(`Duplicate ${name} found: ${JSON.stringify(getUid(duplicate))}`) array.push($object) return $object } catch (error) { throw { object, error } } } export function getGenericInserter(name: string, schema: ZodType, getUid: GetUid) { return insert(name)(schema)(getUid) } export function getInserter(name: string, schema: ZodType, getUid: GetUid, array: Array) { return insert(name)(schema)(getUid)(array) } export function getMultiInserter(name: string, schema: ZodType, getUid: GetUid, array: Array) { const inserter = insert(name)(schema)(getUid)(array) return (objects: Array) => objects.map(inserter) } // export function getInserterWithDefaults(name: string, schema: ZodType, getUid: GetUid, array: Array, defaults: Partial) { // const doInsert = insert(name)(schema)(getUid)(array) // return (object: Input) => doInsert(merge({}, defaults, object)) // } export function getFinder(getUid: GetUid, array: Array) { return function (uidHolder: UidHolder) { return array.find(byUid(getUid, uidHolder)) } } export function getName(schema: ZodSchema) { const description = ensure(schema.description, new Error(`Cannot get schema name: ${JSON.stringify(schema)}`)) const splinters = description.split(' ') return ensureByIndex(splinters, 0) } export const mustIncludeAllOf = (required: El[]) => (elements: El[]) => difference(required, elements).length === 0 export function getErrorReports(results: SafeParseReturnType[], schema: ZodType) { const errors = results.reduce(function (reports: ErrorReport[], result, index) { if (result.success) { return reports } else { const report = { index, error: result.error, } return reports.concat([report]) } }, []) return errors } export interface ErrorReport { index: number, error: ZodError }