UNPKG

sveltekit-superforms

Version:

Making SvelteKit validation and displaying of forms easier than ever!

275 lines (274 loc) 10.2 kB
import { SuperFormError } from './index.js'; import { errorShape } from './errors.js'; export function hasEffects(zodType) { const type = unwrapZodType(zodType); if (type.effects) return true; const name = type.zodType._def.typeName; if (name == 'ZodObject') { const obj = type.zodType; for (const field of Object.values(obj._def.shape())) { if (hasEffects(field)) return true; } } else if (name == 'ZodArray') { const array = type.zodType; return hasEffects(array.element); } return false; } export function unwrapZodType(zodType) { const originalType = zodType; let _wrapped = true; let isNullable = false; let isOptional = false; let hasDefault = false; let effects = undefined; let defaultValue = undefined; //let i = 0; while (_wrapped) { //console.log(' '.repeat(++i * 2) + zodType.constructor.name); if (zodType._def.typeName == 'ZodNullable') { isNullable = true; zodType = zodType.unwrap(); } else if (zodType._def.typeName == 'ZodDefault') { hasDefault = true; defaultValue = zodType._def.defaultValue(); zodType = zodType._def.innerType; } else if (zodType._def.typeName == 'ZodOptional') { isOptional = true; zodType = zodType.unwrap(); } else if (zodType._def.typeName == 'ZodEffects') { if (!effects) effects = zodType; zodType = zodType._def.schema; } else if (zodType._def.typeName == 'ZodPipeline') { zodType = zodType._def.out; } else { _wrapped = false; } } return { zodType, originalType, isNullable, isOptional, hasDefault, defaultValue, effects }; } // https://stackoverflow.com/a/8831937/70894 function hashCode(str) { let hash = 0; for (let i = 0, len = str.length; i < len; i++) { const chr = str.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } // Make it unsigned, for the hash appearance if (hash < 0) hash = hash >>> 0; return hash.toString(36); } export function entityHash(schema) { //console.log(_entityHash(schema)); return hashCode(_entityHash(schema)); } export function _entityHash(type) { let hash = ''; const unwrapped = unwrapZodType(type); switch (unwrapped.zodType._def.typeName) { case 'ZodObject': { for (const [field, zodType] of Object.entries(unwrapped.zodType.shape)) { hash += 'ZodObject:' + field + ':' + _entityHash(zodType); } break; } case 'ZodArray': { const inner = unwrapped.zodType; hash += 'ZodArray:' + _entityHash(inner.element); break; } default: hash += unwrapped.zodType._def.typeName; } return hash; } export function entityData(schema, warnings) { const cached = getCached(schema); if (cached) return cached; const entity = { typeInfo: schemaInfo(schema), defaultEntity: defaultValues(schema), constraints: constraints(schema, warnings), keys: Object.keys(schema.keyof().Values), hash: entityHash(schema), errorShape: errorShape(schema) }; setCached(schema, entity); return entity; } function setCached(schema, entity) { entityCache.set(schema, entity); } function getCached(schema) { return entityCache.get(schema); } const entityCache = new WeakMap(); ///// Factory functions for Entity /////////////////////////////////////////// function schemaInfo(schema) { return _mapSchema(schema, (obj) => unwrapZodType(obj)); } export function valueOrDefault(value, strict, implicitDefaults, schemaInfo) { if (value) return value; const { zodType, isNullable, isOptional, hasDefault, defaultValue } = schemaInfo; // Based on schema type, check what the empty value should be parsed to // For convenience, make undefined into nullable if possible. // otherwise all nullable fields requires a default value or optional. // In the database, null is assumed if no other value (undefined doesn't exist there), // so this should be ok. // Also make a check for strict, so empty strings from FormData can also be set here. if (strict && value !== undefined) return value; if (hasDefault) return defaultValue; if (isNullable) return null; if (isOptional) return undefined; if (implicitDefaults) { if (zodType._def.typeName == 'ZodString') return ''; if (zodType._def.typeName == 'ZodNumber') return 0; if (zodType._def.typeName == 'ZodBoolean') return false; // Cannot add default for ZodDate due to https://github.com/Rich-Harris/devalue/issues/51 //if (zodType._def.typeName == "ZodDate") return new Date(NaN); if (zodType._def.typeName == 'ZodArray') return []; if (zodType._def.typeName == 'ZodObject') { return defaultValues(zodType); } if (zodType._def.typeName == 'ZodSet') return new Set(); if (zodType._def.typeName == 'ZodRecord') return {}; if (zodType._def.typeName == 'ZodBigInt') return BigInt(0); if (zodType._def.typeName == 'ZodSymbol') return Symbol(); } return undefined; } /** * Returns the default values for a zod validation schema. * The main gotcha is that undefined values are changed to null if the field is nullable. */ export function defaultValues(schema) { while (schema._def.typeName == 'ZodEffects') { schema = schema._def.schema; } if (!(schema._def.typeName == 'ZodObject')) { throw new SuperFormError('Only Zod schema objects can be used with defaultValues. ' + 'Define the schema with z.object({ ... }) and optionally refine/superRefine/transform at the end.'); } const realSchema = schema; const fields = Object.keys(realSchema.keyof().Values); const schemaTypeInfo = schemaInfo(realSchema); return Object.fromEntries(fields.map((field) => { const typeInfo = schemaTypeInfo[field]; const newValue = valueOrDefault(undefined, true, true, typeInfo); return [field, newValue]; })); } function constraints(schema, warnings) { function constraint(key, zodType, info) { const output = {}; if (zodType._def.typeName == 'ZodString') { const zodString = zodType; const patterns = zodString._def.checks.filter((f) => f.kind == 'regex'); if (patterns.length > 1 && warnings?.multipleRegexps !== false) { console.warn(`Field "${key}" has more than one regexp, only the first one will be used in constraints. Set the warnings.multipleRegexps option to false to disable this warning.`); } const pattern = patterns.length > 0 && patterns[0].kind == 'regex' ? patterns[0].regex.source : undefined; if (pattern) output.pattern = pattern; if (zodString.minLength !== null) output.minlength = zodString.minLength; if (zodString.maxLength !== null) output.maxlength = zodString.maxLength; } else if (zodType._def.typeName == 'ZodNumber') { const zodNumber = zodType; const steps = zodNumber._def.checks.filter((f) => f.kind == 'multipleOf'); if (steps.length > 1 && warnings?.multipleSteps !== false) { console.warn(`Field "${key}" has more than one step, only the first one will be used in constraints. Set the warnings.multipleSteps option to false to disable this warning.`); } const step = steps.length > 0 && steps[0].kind == 'multipleOf' ? steps[0].value : null; if (zodNumber.minValue !== null) output.min = zodNumber.minValue; if (zodNumber.maxValue !== null) output.max = zodNumber.maxValue; if (step !== null) output.step = step; } else if (zodType._def.typeName == 'ZodDate') { const zodDate = zodType; if (zodDate.minDate) output.min = zodDate.minDate.toISOString(); if (zodDate.maxDate) output.max = zodDate.maxDate.toISOString(); } else if (zodType._def.typeName == 'ZodArray') { if (zodType._def.minLength) output.min = zodType._def.minLength.value; if (zodType._def.maxLength) output.max = zodType._def.maxLength.value; if (zodType._def.exactLength) output.min = output.max = zodType._def.exactLength.value; } if (!info.isNullable && !info.isOptional) { output.required = true; } return Object.keys(output).length > 0 ? output : undefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function mapField(key, value) { const info = unwrapZodType(value); value = info.zodType; if (value._def.typeName == 'ZodArray') { return mapField(key, value._def.type); } else if (value._def.typeName == 'ZodObject') { return constraints(value, warnings); } else { return constraint(key, value, info); } } return _mapSchema(schema, (obj, key) => { return mapField(key, obj); }, (data) => !!data); } /////////////////////////////////////////////////////////////////////////// function _mapSchema(schema, factory, filter) { const keys = schema.keyof().Values; return Object.fromEntries(Object.keys(keys) .map((key) => [key, factory(schema.shape[key], key)]) .filter((entry) => (filter ? filter(entry[1]) : true))); }