UNPKG

sveltekit-superforms

Version:

Making SvelteKit forms a pleasure to use!

239 lines (238 loc) 9.37 kB
import { SchemaError } from '../errors.js'; import { assertSchema } from '../utils.js'; import { merge } from 'ts-deepmerge'; import { schemaInfo } from './schemaInfo.js'; export function defaultValues(schema, isOptional = false, path = []) { return _defaultValues(schema, isOptional, path); } function _defaultValues(schema, isOptional, path) { if (!schema) { throw new SchemaError('Schema was undefined', path); } const info = schemaInfo(schema, isOptional, path); if (!info) return undefined; //if (schema.type == 'object') console.log('--- OBJECT ---'); //else console.dir({ path, schema, isOptional }, { depth: 10 }); let objectDefaults = undefined; // Default takes (early) priority. if ('default' in schema) { // Test for object defaults. // Cannot be returned directly, since undefined fields // may have to be replaced with correct default values. if (info.types.includes('object') && schema.default && typeof schema.default == 'object' && !Array.isArray(schema.default)) { objectDefaults = schema.default; } else { if (info.types.length > 1) { if (info.types.includes('unix-time') && (info.types.includes('integer') || info.types.includes('number'))) throw new SchemaError('Cannot resolve a default value with a union that includes a date and a number/integer.', path); } const [type] = info.types; return formatDefaultValue(type, schema.default); } } let _multiType; const isMultiTypeUnion = () => { if (!info.union || info.union.length < 2) return false; if (info.union.some((i) => i.enum)) return true; if (!_multiType) { _multiType = new Set(info.types.map((i) => { return ['integer', 'unix-time'].includes(i) ? 'number' : i; })); } return _multiType.size > 1; }; let output = undefined; // Check unions first, so default values can take precedence over nullable and optional if (!objectDefaults && info.union) { const singleDefault = info.union.filter((s) => typeof s !== 'boolean' && s.default !== undefined); if (singleDefault.length == 1) { return _defaultValues(singleDefault[0], isOptional, path); } else if (singleDefault.length > 1) { throw new SchemaError('Only one default value can exist in a union, or set a default value for the whole union.', path); } else { // Null takes priority over undefined if (info.isNullable) return null; if (info.isOptional) return undefined; if (isMultiTypeUnion()) { throw new SchemaError('Multi-type unions must have a default value, or exactly one of the union types must have.', path); } // Objects must have default values to avoid setting undefined properties on nested data if (info.union.length) { if (info.types[0] == 'object') { if (output === undefined) output = {}; output = info.union.length > 1 ? merge.withOptions({ allowUndefinedOverrides: true }, ...info.union.map((s) => _defaultValues(s, isOptional, path))) : _defaultValues(info.union[0], isOptional, path); } else { return _defaultValues(info.union[0], isOptional, path); } } } } if (!objectDefaults) { // Null takes priority over undefined if (info.isNullable) return null; if (info.isOptional) return undefined; } // Objects if (info.properties) { for (const [key, objSchema] of Object.entries(info.properties)) { assertSchema(objSchema, [...path, key]); const def = objectDefaults && objectDefaults[key] !== undefined ? objectDefaults[key] : _defaultValues(objSchema, !info.required?.includes(key), [...path, key]); //if (def !== undefined) output[key] = def; if (output === undefined) output = {}; output[key] = def; } } else if (objectDefaults) { return objectDefaults; } // TODO: [v3] Handle default values for array elements // if (info.array && info.array.length) { // console.log('===== Array default ====='); // console.dir(info.array, { depth: 10 }); //debug // //if (info.array.length > 1) throw new SchemaError('Only one array type is supported.', path); // console.dir(_defaultValues(info.array[0], info.isOptional, path), { depth: 10 }); //debug // } // Enums, return the first value so it can be a required field if (schema.enum) { return schema.enum[0]; } // Constants if ('const' in schema) { return schema.const; } // Basic type if (isMultiTypeUnion()) { throw new SchemaError('Default values cannot have more than one type.', path); } else if (info.types.length == 0) { //console.warn('No type or format for property:', path); //debug //console.dir(schema, { depth: 10 }); //debug return undefined; } const [formatType] = info.types; return output ?? defaultValue(formatType, schema.enum); } function formatDefaultValue(type, value) { switch (type) { case 'set': return Array.isArray(value) ? new Set(value) : value; case 'Date': case 'date': case 'unix-time': if (typeof value === 'string' || typeof value === 'number') return new Date(value); break; case 'bigint': if (typeof value === 'string' || typeof value === 'number') return BigInt(value); break; case 'symbol': if (typeof value === 'string' || typeof value === 'number') return Symbol(value); break; } return value; } export function defaultValue(type, enumType) { switch (type) { case 'string': return enumType && enumType.length > 0 ? enumType[0] : ''; case 'number': case 'integer': return enumType && enumType.length > 0 ? enumType[0] : 0; case 'boolean': return false; case 'array': return []; case 'object': return {}; case 'null': return null; case 'Date': case 'date': case 'unix-time': // Cannot add default for Date due to https://github.com/Rich-Harris/devalue/issues/51 return undefined; case 'int64': case 'bigint': return BigInt(0); case 'set': return new Set(); case 'symbol': return Symbol(); case 'undefined': case 'any': return undefined; default: throw new SchemaError('Schema type or format not supported, requires explicit default value: ' + type); } } //////////////////////////////////////////////////////////////////////////// export function defaultTypes(schema, path = []) { return _defaultTypes(schema, false, path); } function _defaultTypes(schema, isOptional, path) { if (!schema) { throw new SchemaError('Schema was undefined', path); } const info = schemaInfo(schema, isOptional, path); const output = { __types: info.types }; //if (schema.type == 'object') console.log('--- OBJECT ---'); //debug //else console.dir({ path, info }, { depth: 10 }); //debug // schema.items cannot be an array according to // https://www.learnjsonschema.com/2020-12/applicator/items/ if (info.schema.items && typeof info.schema.items == 'object' && !Array.isArray(info.schema.items)) { output.__items = _defaultTypes(info.schema.items, info.isOptional, path); } if (info.properties) { for (const [key, value] of Object.entries(info.properties)) { assertSchema(value, [...path, key]); output[key] = _defaultTypes(info.properties[key], !info.required?.includes(key), [ ...path, key ]); } } // Check if a Record type is used for additionalProperties if (info.additionalProperties && info.types.includes('object')) { const additionalInfo = schemaInfo(info.additionalProperties, info.isOptional, path); if (additionalInfo.properties && additionalInfo.types.includes('object')) { for (const [key] of Object.entries(additionalInfo.properties)) { output[key] = _defaultTypes(additionalInfo.properties[key], !additionalInfo.required?.includes(key), [...path, key]); } } } if (info.isNullable && !output.__types.includes('null')) { output.__types.push('null'); } if (info.isOptional && !output.__types.includes('undefined')) { output.__types.push('undefined'); } return output; }