- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
290 lines
12 KiB
JavaScript
290 lines
12 KiB
JavaScript
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]);
|
|
// Determine the default value for this property. If an object-level default
|
|
// was provided, attempt to convert it to the correct typed form using the
|
|
// property's schema information (e.g. convert ISO strings back to Date).
|
|
let def;
|
|
if (objectDefaults && objectDefaults[key] !== undefined) {
|
|
try {
|
|
const propInfo = schemaInfo(objSchema, !info.required?.includes(key), [...path, key]);
|
|
if (propInfo) {
|
|
// Use the first resolved type to format simple values (dates, sets, maps, bigint, symbol)
|
|
const propType = propInfo.types[0];
|
|
// If property is an object and the provided default is an object,
|
|
// we need to recursively process it to convert nested values (e.g. Date strings to Dates)
|
|
if (propType === 'object' &&
|
|
typeof objectDefaults[key] === 'object' &&
|
|
objectDefaults[key] !== null &&
|
|
!Array.isArray(objectDefaults[key])) {
|
|
// Create a temporary schema with the parent's default to recursively process it
|
|
const schemaWithDefault = {
|
|
...objSchema,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
default: objectDefaults[key]
|
|
};
|
|
def = _defaultValues(schemaWithDefault, !info.required?.includes(key), [
|
|
...path,
|
|
key
|
|
]);
|
|
}
|
|
else {
|
|
def = formatDefaultValue(propType, objectDefaults[key]);
|
|
}
|
|
}
|
|
else {
|
|
def = objectDefaults[key];
|
|
}
|
|
}
|
|
catch {
|
|
// If anything goes wrong determining/formatting, fall back to provided value
|
|
def = objectDefaults[key];
|
|
}
|
|
}
|
|
else {
|
|
def = _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 'map':
|
|
return Array.isArray(value) ? new Map(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 'stringbool':
|
|
// For stringbool, return empty string - let Zod validation handle it
|
|
// The schema should define a default if one is needed
|
|
return '';
|
|
case 'set':
|
|
return new Set();
|
|
case 'map':
|
|
return new Map();
|
|
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);
|
|
let output = {
|
|
__types: info.types
|
|
};
|
|
if (info.union) {
|
|
output = merge(output, ...info.union.map((u) => _defaultTypes(u, info.isOptional, path)));
|
|
}
|
|
//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;
|
|
}
|