sveltekit-superforms
Advanced tools
Comparing version 0.5.15 to 0.5.16
@@ -133,4 +133,5 @@ import { enhance, applyAction } from '$app/forms'; | ||
} | ||
// Do not await on onUpdated, since we're already finished with the request | ||
if (options.onUpdated) { | ||
await options.onUpdated({ form }); | ||
options.onUpdated({ form }); | ||
} | ||
@@ -177,3 +178,9 @@ } | ||
continue; | ||
console.log('🚀 ~ file: index.ts:349 ~ Data.subscribe ~ UPDATE key:', key); | ||
// Date comparison is a mess, since it can come from the server as undefined, | ||
// or be Invalid Date from a proxy. | ||
if ((f[key] instanceof Date || previousForm[key] instanceof Date) && | ||
((isNaN(f[key]) && isNaN(previousForm[key])) || | ||
f[key]?.getTime() == previousForm[key]?.getTime())) { | ||
continue; | ||
} | ||
const validator = options.validators && options.validators[key]; | ||
@@ -356,7 +363,7 @@ if (validator) { | ||
} | ||
const form = Form(formEl); | ||
const htmlForm = Form(formEl); | ||
let currentRequest; | ||
return enhance(formEl, async (submit) => { | ||
let cancelled = false; | ||
if (form.isSubmitting() && options.multipleSubmits == 'prevent') { | ||
if (htmlForm.isSubmitting() && options.multipleSubmits == 'prevent') { | ||
//d('Prevented form submission'); | ||
@@ -367,3 +374,3 @@ cancelled = true; | ||
else { | ||
if (form.isSubmitting() && options.multipleSubmits == 'abort') { | ||
if (htmlForm.isSubmitting() && options.multipleSubmits == 'abort') { | ||
if (currentRequest) | ||
@@ -406,3 +413,3 @@ currentRequest.abort(); | ||
//d('Submitting'); | ||
form.submitting(); | ||
htmlForm.submitting(); | ||
switch (options.dataType) { | ||
@@ -500,5 +507,5 @@ case 'json': | ||
} | ||
form.completed(cancelled); | ||
htmlForm.completed(cancelled); | ||
}; | ||
}); | ||
} |
import type { z, AnyZodObject } from 'zod'; | ||
import type { Entity } from './server/entity'; | ||
export type ValidationErrors<T extends AnyZodObject> = Partial<Record<keyof z.infer<T>, string[] | undefined>>; | ||
@@ -18,7 +19,5 @@ export type InputConstraints = Partial<{ | ||
message: string | null; | ||
constraints: Record<keyof z.infer<T>, InputConstraints | undefined>; | ||
meta?: { | ||
types: Record<keyof z.infer<T>, string>; | ||
}; | ||
constraints: Entity<T>['constraints']; | ||
meta?: Entity<T>['meta']; | ||
}; | ||
export declare function deepEqual(obj1: unknown, obj2: unknown): boolean; |
import type { InputConstraints } from '..'; | ||
import { z, type ZodTypeAny, type AnyZodObject } from 'zod'; | ||
type DefaultFields<T extends AnyZodObject> = Partial<{ | ||
[Property in keyof z.infer<T>]: z.infer<T>[Property] | ((value: z.infer<T>[Property] | null | undefined, data: z.infer<T>) => z.infer<T>[Property] | null | undefined); | ||
}>; | ||
export type ZodTypeInfo = { | ||
@@ -13,10 +10,6 @@ zodType: ZodTypeAny; | ||
type EntityRecord<T extends AnyZodObject, K> = Record<keyof z.infer<T>, K>; | ||
export type DefaultEntityOptions<T extends AnyZodObject> = { | ||
defaults?: DefaultFields<T>; | ||
implicitDefaults?: boolean; | ||
}; | ||
type EntityMetaData<T extends AnyZodObject> = { | ||
export type EntityMetaData<T extends AnyZodObject> = { | ||
types: EntityRecord<T, string>; | ||
}; | ||
type Entity<T extends AnyZodObject> = { | ||
export type Entity<T extends AnyZodObject> = { | ||
typeInfo: EntityRecord<T, ZodTypeInfo>; | ||
@@ -27,4 +20,6 @@ defaultEntity: z.infer<T>; | ||
}; | ||
export declare function entityData<T extends AnyZodObject>(schema: T, options?: DefaultEntityOptions<T>): Entity<T>; | ||
export declare function entityData<T extends AnyZodObject>(schema: T): Entity<T>; | ||
export declare function zodTypeInfo(zodType: ZodTypeAny): ZodTypeInfo; | ||
export declare function checkMissingFields<T extends AnyZodObject>(schema: T, data: Partial<z.infer<T>> | null | undefined): void; | ||
export declare function valueOrDefault(value: unknown, strict: boolean, implicitDefaults: true, typeInfo: ZodTypeInfo): {} | null | undefined; | ||
/** | ||
@@ -34,6 +29,3 @@ * Returns the default values for a zod validation schema. | ||
*/ | ||
export declare function defaultEntity<T extends AnyZodObject>(schema: T, options?: { | ||
defaults?: DefaultFields<T>; | ||
implicitDefaults?: boolean; | ||
}): z.infer<T>; | ||
export declare function defaultEntity<T extends AnyZodObject>(schema: T): z.infer<T>; | ||
export {}; |
import { z, ZodDefault, ZodNullable, ZodOptional, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodArray, ZodEffects, ZodBigInt, ZodObject, ZodSymbol } from 'zod'; | ||
function setValidationDefaults(data, fields) { | ||
for (const stringField of Object.keys(fields)) { | ||
const field = stringField; | ||
const currentData = data[field]; | ||
if (typeof fields[field] === 'function') { | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const func = fields[field]; | ||
data[field] = func(currentData, data); | ||
} | ||
else if (!currentData) { | ||
data[field] = fields[field]; | ||
} | ||
} | ||
} | ||
export function entityData(schema, options = {}) { | ||
if (entityCache.has(schema)) | ||
return entityCache.get(schema); | ||
export function entityData(schema) { | ||
const cached = getCached(schema); | ||
if (cached) | ||
return cached; | ||
const typeInfos = typeInfo(schema); | ||
const defaultEnt = defaultEntity(schema); | ||
const entity = { | ||
typeInfo: typeInfos, | ||
defaultEntity: defaultEntity(schema, options), | ||
defaultEntity: defaultEnt, | ||
constraints: constraints(schema, typeInfos), | ||
meta: meta(schema) | ||
}; | ||
entityCache.set(schema, entity); | ||
setCached(schema, entity); | ||
return entity; | ||
} | ||
function setCached(schema, entity) { | ||
entityCache.set(schema, entity); | ||
} | ||
function getCached(schema) { | ||
return entityCache.get(schema); | ||
} | ||
const entityCache = new WeakMap(); | ||
@@ -68,2 +62,48 @@ ///// Factory functions for Entity /////////////////////////////////////////// | ||
} | ||
export function checkMissingFields(schema, data) { | ||
const entity = entityData(schema); | ||
const missingFields = Object.keys(entity.constraints).filter((field) => entity.constraints[field]?.required && | ||
entity.defaultEntity[field] === undefined && | ||
(!data || data[field] === undefined || data[field] === null)); | ||
if (missingFields.length) { | ||
const errors = missingFields.map((field) => `"${String(field)}" (${entity.typeInfo[field].zodType.constructor.name})`); | ||
throw new Error(`Unsupported default value for schema field(s): ${errors.join(', ')}. Add default, optional or nullable to those fields in the schema.`); | ||
} | ||
} | ||
export function valueOrDefault(value, strict, implicitDefaults, typeInfo) { | ||
if (value) | ||
return value; | ||
const { zodType, isNullable, isOptional, defaultValue } = typeInfo; | ||
// 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 (defaultValue !== undefined) | ||
return defaultValue; | ||
if (isNullable) | ||
return null; | ||
if (isOptional) | ||
return undefined; | ||
if (implicitDefaults) { | ||
if (zodType instanceof ZodString) | ||
return ''; | ||
if (zodType instanceof ZodNumber) | ||
return 0; | ||
if (zodType instanceof ZodBoolean) | ||
return false; | ||
if (zodType instanceof ZodArray) | ||
return []; | ||
if (zodType instanceof ZodObject) | ||
return {}; | ||
if (zodType instanceof ZodBigInt) | ||
return BigInt(0); | ||
if (zodType instanceof ZodSymbol) | ||
return Symbol(); | ||
} | ||
return undefined; | ||
} | ||
/** | ||
@@ -73,50 +113,6 @@ * Returns the default values for a zod validation schema. | ||
*/ | ||
export function defaultEntity(schema, options = {}) { | ||
function valueOrDefault(field, value, strict, implicitDefaults, typeInfo) { | ||
if (value) | ||
return value; | ||
const { zodType, isNullable, isOptional, defaultValue } = typeInfo; | ||
// Based on schema type, check what the empty value should be parsed to | ||
function emptyValue() { | ||
// 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 (defaultValue !== undefined) | ||
return defaultValue; | ||
if (isNullable) | ||
return null; | ||
if (isOptional) | ||
return undefined; | ||
if (implicitDefaults) { | ||
if (zodType instanceof ZodString) | ||
return ''; | ||
if (zodType instanceof ZodNumber) | ||
return 0; | ||
if (zodType instanceof ZodBoolean) | ||
return false; | ||
if (zodType instanceof ZodArray) | ||
return []; | ||
if (zodType instanceof ZodObject) | ||
return {}; | ||
if (zodType instanceof ZodBigInt) | ||
return BigInt(0); | ||
if (zodType instanceof ZodSymbol) | ||
return Symbol(); | ||
} | ||
throw new Error(`Unsupported type for ${strict ? 'strict' : 'falsy'} ${implicitDefaults ? 'implicit' : 'explicit'} values on field "${String(field)}": ${zodType.constructor.name}. Add default, optional or nullable to the schema.`); | ||
} | ||
return emptyValue(); | ||
} | ||
options = { ...options }; | ||
export function defaultEntity(schema) { | ||
const fields = Object.keys(schema.keyof().Values); | ||
let output = {}; | ||
let defaultKeys; | ||
if (options.defaults) { | ||
setValidationDefaults(output, options.defaults); | ||
defaultKeys = Object.keys(options.defaults); | ||
} | ||
const schemaTypeInfo = typeInfo(schema); | ||
@@ -126,6 +122,6 @@ // Need to set empty properties after defaults are set. | ||
const typeInfo = schemaTypeInfo[field]; | ||
const value = defaultKeys && defaultKeys.includes(field) | ||
const newValue = defaultKeys && defaultKeys.includes(field) | ||
? output[field] | ||
: valueOrDefault(field, undefined, true, options.implicitDefaults ?? true, typeInfo); | ||
return [field, value]; | ||
: valueOrDefault(undefined, true, true, typeInfo); | ||
return [field, newValue]; | ||
})); | ||
@@ -174,4 +170,7 @@ return output; | ||
} | ||
if (!info.isNullable && !info.isOptional) | ||
if (!info.isNullable && | ||
!info.isOptional && | ||
(info.defaultValue === undefined || info.defaultValue === null)) { | ||
output.required = true; | ||
} | ||
return Object.keys(output).length > 0 ? output : undefined; | ||
@@ -178,0 +177,0 @@ } |
import { type RequestEvent } from '@sveltejs/kit'; | ||
import type { Validation } from '..'; | ||
import { type DefaultEntityOptions } from './entity'; | ||
import { z, type AnyZodObject } from 'zod'; | ||
@@ -12,2 +11,5 @@ export { defaultEntity } from './entity'; | ||
export declare function noErrors<T extends AnyZodObject>(form: Validation<T>): Validation<T>; | ||
type DefaultFields<T extends AnyZodObject> = Partial<{ | ||
[Property in keyof z.infer<T>]: z.infer<T>[Property] | ((value: z.infer<T>[Property] | null | undefined, data: z.infer<T>) => z.infer<T>[Property] | null | undefined); | ||
}>; | ||
/** | ||
@@ -20,6 +22,8 @@ * Validates a Zod schema for usage in a SvelteKit form. | ||
*/ | ||
export declare function superValidate<T extends AnyZodObject>(data: RequestEvent | Request | FormData | Partial<z.infer<T>> | null | undefined, schema: T, options?: DefaultEntityOptions<T> & { | ||
export declare function superValidate<T extends AnyZodObject>(data: RequestEvent | Request | FormData | Partial<z.infer<T>> | null | undefined, schema: T, options?: { | ||
noErrors?: boolean; | ||
includeMeta?: boolean; | ||
checkMissingEntityFields?: boolean; | ||
defaults?: DefaultFields<T>; | ||
}): Promise<Validation<T>>; | ||
export declare function actionResult<T extends Record<string, unknown> | string>(type: T extends string ? 'redirect' : 'success' | 'failure' | 'error', data?: T, status?: number): Response; |
import { fail, json } from '@sveltejs/kit'; | ||
import { parse, stringify } from 'devalue'; | ||
import { entityData, zodTypeInfo } from './entity'; | ||
import { z, ZodAny, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodLiteral, ZodUnion, ZodArray, ZodBigInt, ZodObject, ZodSymbol, ZodEnum } from 'zod'; | ||
import { checkMissingFields, entityData, valueOrDefault, zodTypeInfo } from './entity'; | ||
import { z, ZodAny, ZodString, ZodNumber, ZodBoolean, ZodDate, ZodLiteral, ZodUnion, ZodArray, ZodBigInt, ZodEnum } from 'zod'; | ||
export { defaultEntity } from './entity'; | ||
@@ -43,3 +43,3 @@ export function setError(form, field, error, options = { overwrite: false }) { | ||
function parseEntry(field, value, typeInfo) { | ||
const newValue = valueOrDefault(field, value, false, true, typeInfo); | ||
const newValue = valueOrDefault(value, false, true, typeInfo); | ||
// If the value was empty, it now contains the default value, | ||
@@ -97,40 +97,15 @@ // so it can be returned immediately | ||
} | ||
function valueOrDefault(field, value, strict, implicitDefaults, typeInfo) { | ||
if (value) | ||
return value; | ||
const { zodType, isNullable, isOptional, defaultValue } = typeInfo; | ||
// Based on schema type, check what the empty value should be parsed to | ||
function emptyValue() { | ||
// 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 (defaultValue !== undefined) | ||
return defaultValue; | ||
if (isNullable) | ||
return null; | ||
if (isOptional) | ||
return undefined; | ||
if (implicitDefaults) { | ||
if (zodType instanceof ZodString) | ||
return ''; | ||
if (zodType instanceof ZodNumber) | ||
return 0; | ||
if (zodType instanceof ZodBoolean) | ||
return false; | ||
if (zodType instanceof ZodArray) | ||
return []; | ||
if (zodType instanceof ZodObject) | ||
return {}; | ||
if (zodType instanceof ZodBigInt) | ||
return BigInt(0); | ||
if (zodType instanceof ZodSymbol) | ||
return Symbol(); | ||
function setValidationDefaults(data, fields) { | ||
for (const stringField of Object.keys(fields)) { | ||
const field = stringField; | ||
const currentData = data[field]; | ||
if (typeof fields[field] === 'function') { | ||
// eslint-disable-next-line @typescript-eslint/ban-types | ||
const func = fields[field]; | ||
data[field] = func(currentData, data); | ||
} | ||
throw new Error(`Unsupported type for ${strict ? 'strict' : 'falsy'} ${implicitDefaults ? 'implicit' : 'explicit'} values on field "${String(field)}": ${zodType.constructor.name}. Add default, optional or nullable to the schema.`); | ||
else if (!currentData) { | ||
data[field] = fields[field]; | ||
} | ||
} | ||
return emptyValue(); | ||
} | ||
@@ -145,28 +120,26 @@ /** | ||
export async function superValidate(data, schema, options = {}) { | ||
options = { noErrors: false, includeMeta: false, ...options }; | ||
options = { | ||
checkMissingEntityFields: true, | ||
noErrors: false, | ||
includeMeta: false, | ||
...options | ||
}; | ||
const schemaKeys = Object.keys(schema.keyof().Values); | ||
const entityInfo = entityData(schema); | ||
function emptyEntity() { | ||
return { | ||
valid: false, | ||
errors: {}, | ||
data: entityInfo.defaultEntity, | ||
empty: true, | ||
message: null, | ||
constraints: entityInfo.constraints, | ||
meta: options.includeMeta ? entityInfo.meta : undefined | ||
}; | ||
} | ||
function parseSuperJson(data) { | ||
if (data.has('__superform_json')) { | ||
const output = parse(data.get('__superform_json')?.toString() ?? ''); | ||
if (typeof output === 'object') | ||
return output; | ||
else | ||
throw 'Invalid superform JSON type'; | ||
function parseFormData(data) { | ||
function tryParseSuperJson(data) { | ||
if (data.has('__superform_json')) { | ||
try { | ||
const output = parse(data.get('__superform_json')?.toString() ?? ''); | ||
if (typeof output === 'object') { | ||
return output; | ||
} | ||
} | ||
catch { | ||
// | ||
} | ||
} | ||
return null; | ||
} | ||
return null; | ||
} | ||
function parseFormData(data) { | ||
const superJson = parseSuperJson(data); | ||
const superJson = tryParseSuperJson(data); | ||
return superJson | ||
@@ -186,17 +159,47 @@ ? superJson | ||
} | ||
if (!data) { | ||
return emptyEntity(); | ||
} | ||
else if (data instanceof FormData) { | ||
let checkMissing = true; | ||
// If FormData exists, don't check for missing fields. | ||
// Checking only at GET requests, basically, where | ||
// the data is coming from the DB. | ||
if (data instanceof FormData) { | ||
data = parseFormData(data); | ||
checkMissing = false; | ||
} | ||
else if (data instanceof Request) { | ||
data = await tryParseFormData(data); | ||
checkMissing = !data; | ||
} | ||
else if ('request' in data && data.request instanceof Request) { | ||
else if (data && data.request instanceof Request) { | ||
data = await tryParseFormData(data.request); | ||
checkMissing = !data; | ||
} | ||
if (!data) | ||
return emptyEntity(); | ||
const status = schema.safeParse(data); | ||
else if (data) { | ||
// Make a copy of the data, so defaults can be applied to it. | ||
data = { ...data }; | ||
} | ||
if (checkMissing && options.checkMissingEntityFields) { | ||
// Empty or Partial entity | ||
checkMissingFields(schema, data); | ||
} | ||
if (!data) { | ||
const emptyEntity = { | ||
valid: false, | ||
errors: {}, | ||
// Copy the default entity so it's not modified | ||
data: { ...entityInfo.defaultEntity }, | ||
empty: true, | ||
message: null, | ||
constraints: entityInfo.constraints, | ||
meta: options.includeMeta ? entityInfo.meta : undefined | ||
}; | ||
if (options.defaults) { | ||
setValidationDefaults(emptyEntity.data, options.defaults); | ||
} | ||
return emptyEntity; | ||
} | ||
const partialData = data; | ||
if (options.defaults) { | ||
setValidationDefaults(partialData, options.defaults); | ||
} | ||
const status = schema.safeParse(partialData); | ||
if (!status.success) { | ||
@@ -206,3 +209,2 @@ const errors = options.noErrors | ||
: status.error.flatten().fieldErrors; | ||
const parsedData = data; | ||
return { | ||
@@ -213,5 +215,5 @@ valid: false, | ||
key, | ||
parsedData[key] === undefined | ||
partialData[key] === undefined | ||
? entityInfo.defaultEntity[key] | ||
: parsedData[key] | ||
: partialData[key] | ||
])), | ||
@@ -218,0 +220,0 @@ empty: false, |
{ | ||
"name": "sveltekit-superforms", | ||
"version": "0.5.15", | ||
"version": "0.5.16", | ||
"author": "Andreas Söderlund <ciscoheat@gmail.com> (https://blog.encodeart.dev)", | ||
@@ -5,0 +5,0 @@ "description": "Supercharge your SvelteKit forms with this powerhouse of a library!", |
1341
82630