Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 14 additions & 22 deletions src/lib/schemaEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,7 @@ function schemaInfo<T extends AnyZodObject>(schema: T) {
return _mapSchema(schema, (obj) => unwrapZodType(obj));
}

export function valueOrDefault(
value: unknown,
strict: boolean,
implicitDefaults: true,
schemaInfo: ZodTypeInfo
) {
export function valueOrDefault(value: unknown, schemaInfo: ZodTypeInfo) {
if (value) return value;

const { zodType, isNullable, isOptional, hasDefault, defaultValue } =
Expand All @@ -242,26 +237,23 @@ export function valueOrDefault(
// 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 as AnyZodObject);
}
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();
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 as AnyZodObject);
}
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;
}
Expand Down Expand Up @@ -291,7 +283,7 @@ export function defaultValues<T extends ZodValidation<AnyZodObject>>(
return Object.fromEntries(
fields.map((field) => {
const typeInfo = schemaTypeInfo[field];
const newValue = valueOrDefault(undefined, true, true, typeInfo);
const newValue = valueOrDefault(undefined, typeInfo);

return [field, newValue];
})
Expand Down
170 changes: 106 additions & 64 deletions src/lib/superValidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,37 @@ function formDataToValidation<T extends AnyZodObject>(
data: FormData,
schemaData: SchemaData<T>,
preprocessed?: (keyof z.infer<T>)[]
) {
const output: Record<string, unknown> = {};
): { partial: Partial<z.infer<T>>; parsed: z.infer<T> } {
const strictData: Record<string, unknown> = {};
const parsedData: Record<string, unknown> = {};
const { schemaKeys, entityInfo } = schemaData;

for (const key of schemaKeys) {
const typeInfo = entityInfo.typeInfo[key];
const entries = data.getAll(key);

if (!(typeInfo.zodType._def.typeName == 'ZodArray')) {
parsedData[key] = parseSingleEntry(key, entries[0], typeInfo);
} else {
const arrayType = unwrapZodType(typeInfo.zodType._def.type);
parsedData[key] = entries.map((e) =>
parseSingleEntry(key, e, arrayType)
);
}

if (!entries.length && !typeInfo.isOptional) {
strictData[key] = undefined;
} else {
strictData[key] = parsedData[key];
}
}

for (const key of Object.keys(strictData)) {
if (strictData[key] === undefined) delete strictData[key];
}

return { parsed: parsedData, partial: strictData };

function parseSingleEntry(
key: string,
entry: FormDataEntryValue,
Expand All @@ -178,24 +205,12 @@ function formDataToValidation<T extends AnyZodObject>(
return parseFormDataEntry(key, entry, typeInfo);
}

for (const key of schemaKeys) {
const typeInfo = entityInfo.typeInfo[key];
const entries = data.getAll(key);

if (!(typeInfo.zodType._def.typeName == 'ZodArray')) {
output[key] = parseSingleEntry(key, entries[0], typeInfo);
} else {
const arrayType = unwrapZodType(typeInfo.zodType._def.type);
output[key] = entries.map((e) => parseSingleEntry(key, e, arrayType));
}
}

function parseFormDataEntry(
field: string,
value: string | null,
typeInfo: ZodTypeInfo
): unknown {
const newValue = valueOrDefault(value, false, true, typeInfo);
const newValue = valueOrDefault(value, typeInfo);
const zodType = typeInfo.zodType;
const typeName = zodType._def.typeName;

Expand Down Expand Up @@ -274,8 +289,6 @@ function formDataToValidation<T extends AnyZodObject>(
'Unsupported Zod default type: ' + zodType.constructor.name
);
}

return output as z.infer<T>;
}

///// superValidate helpers /////////////////////////////////////////
Expand All @@ -289,10 +302,12 @@ type SchemaData<T extends AnyZodObject> = {
opts: SuperValidateOptions<T>;
};

type ParsedData = {
type ParsedData<T extends Record<string, unknown>> = {
id: string | undefined;
posted: boolean;
data: Record<string, unknown> | null | undefined;
data: T | null | undefined;
// Used in strict mode
partialData: Partial<T> | null | undefined;
};

/**
Expand All @@ -301,31 +316,32 @@ type ParsedData = {
* should be displayed.
*/
function dataToValidate<T extends AnyZodObject>(
parsed: ParsedData,
parsed: ParsedData<z.infer<T>>,
schemaData: SchemaData<T>
): Record<string, unknown> | undefined {
const strict = schemaData.opts?.strict ?? false;
if (!parsed.data) {
return schemaData.hasEffects || schemaData.opts.errors === true
? schemaData.entityInfo.defaultEntity
: undefined;
} else {
return parsed.data;
}
} else if (strict && parsed.partialData) {
return parsed.partialData;
} else return parsed.data;
}

function parseFormData<T extends AnyZodObject>(
formData: FormData,
schemaData: SchemaData<T>,
options?: SuperValidateOptions<T>
): ParsedData {
preprocessed?: SuperValidateOptions<T>['preprocessed']
): ParsedData<z.infer<T>> {
function tryParseSuperJson() {
if (formData.has('__superform_json')) {
try {
const output = parse(
formData.getAll('__superform_json').join('') ?? ''
);
if (typeof output === 'object') {
return output as z.infer<UnwrapEffects<T>>;
return output as Record<string, unknown>;
}
} catch {
//
Expand All @@ -337,24 +353,25 @@ function parseFormData<T extends AnyZodObject>(
const data = tryParseSuperJson();
const id = formData.get('__superform_id')?.toString() ?? undefined;

return data
? { id, data, posted: true }
: {
id,
data: formDataToValidation(
formData,
schemaData,
options?.preprocessed
),
posted: true
};
if (data) {
return { id, data, posted: true, partialData: null };
}

const parsed = formDataToValidation(formData, schemaData, preprocessed);

return {
id,
data: parsed.parsed,
partialData: parsed.partial,
posted: true
};
}

function parseSearchParams<T extends AnyZodObject>(
data: URL | URLSearchParams,
schemaData: SchemaData<T>,
options?: SuperValidateOptions<T>
): ParsedData {
preprocessed?: SuperValidateOptions<T>['preprocessed']
): ParsedData<z.infer<T>> {
if (data instanceof URL) data = data.searchParams;

const convert = new FormData();
Expand All @@ -363,13 +380,13 @@ function parseSearchParams<T extends AnyZodObject>(
}

// Only FormData can be posted.
const output = parseFormData(convert, schemaData, options);
const output = parseFormData(convert, schemaData, preprocessed);
output.posted = false;
return output;
}

function validateResult<T extends AnyZodObject, M>(
parsed: ParsedData,
parsed: ParsedData<z.infer<T>>,
schemaData: SchemaData<T>,
result: SafeParseReturnType<unknown, z.infer<T>> | undefined
): SuperValidated<T, M> {
Expand All @@ -389,12 +406,12 @@ function validateResult<T extends AnyZodObject, M>(
let errors: ReturnType<typeof mapErrors> = {};

const valid = result?.success ?? false;
const { opts: options, entityInfo } = schemaData;
const addErrors = options.errors ?? options.strict;

if (result) {
if (result.success) {
data = result.data;
} else if (options.errors === true) {
} else if (addErrors) {
errors = mapErrors<T>(result.error.format(), entityInfo.errorShape);
}
}
Expand Down Expand Up @@ -432,17 +449,22 @@ function validateResult<T extends AnyZodObject, M>(
// passthrough, strip, strict
const zodKeyStatus = unwrappedSchema._def.unknownKeys;

const data =
zodKeyStatus == 'passthrough'
? { ...clone(entityInfo.defaultEntity), ...partialData }
: Object.fromEntries(
schemaKeys.map((key) => [
key,
key in partialData
? partialData[key]
: clone(entityInfo.defaultEntity[key])
])
);
let data;

if (options.strict) {
data = parsed.data;
} else if (zodKeyStatus == 'passthrough') {
data = { ...clone(entityInfo.defaultEntity), ...partialData };
} else {
data = Object.fromEntries(
schemaKeys.map((key) => [
key,
key in partialData
? partialData[key]
: clone(entityInfo.defaultEntity[key])
])
);
}

return {
id,
Expand Down Expand Up @@ -505,6 +527,7 @@ function getSchemaData<T extends AnyZodObject>(

export type SuperValidateOptions<T extends AnyZodObject = AnyZodObject> =
Partial<{
strict: boolean;
errors: boolean;
id: string;
warnings: {
Expand Down Expand Up @@ -563,7 +586,9 @@ export async function superValidate<

const schemaData = getSchemaData(schema as UnwrapEffects<T>, options);

async function tryParseFormData(request: Request) {
async function tryParseFormData(
request: Request
): Promise<ParsedData<z.infer<UnwrapEffects<T>>>> {
let formData: FormData | undefined = undefined;
try {
formData = await request.formData();
Expand All @@ -577,18 +602,23 @@ export async function superValidate<
throw e;
}
// No data found, return an empty form
return { id: undefined, data: undefined, posted: false };
return {
id: undefined,
data: undefined,
posted: false,
partialData: undefined
};
}
return parseFormData(formData, schemaData, options);
return parseFormData(formData, schemaData, options?.preprocessed);
}

async function parseRequest() {
let parsed: ParsedData;
let parsed: ParsedData<z.infer<UnwrapEffects<T>>>;

if (data instanceof FormData) {
parsed = parseFormData(data, schemaData, options);
parsed = parseFormData(data, schemaData, options?.preprocessed);
} else if (data instanceof URL || data instanceof URLSearchParams) {
parsed = parseSearchParams(data, schemaData, options);
parsed = parseSearchParams(data, schemaData, options?.preprocessed);
} else if (data instanceof Request) {
parsed = await tryParseFormData(data);
} else if (
Expand All @@ -598,11 +628,16 @@ export async function superValidate<
data.request instanceof Request
) {
parsed = await tryParseFormData(data.request);
} else if (options?.strict) {
// Ensure that defaults are set on data if strict mode is enabled (Should this maybe always happen?)
const params = new URLSearchParams(data as Record<string, string>);
parsed = parseSearchParams(params, schemaData, options?.preprocessed);
} else {
parsed = {
id: undefined,
posted: false,
data: data as Record<string, unknown>,
posted: false
partialData: data as Record<string, unknown>
};
}

Expand All @@ -618,7 +653,13 @@ export async function superValidate<
}

const { parsed, result } = await parseRequest();
return validateResult<UnwrapEffects<T>, M>(parsed, schemaData, result);

const superValidated = validateResult<UnwrapEffects<T>, M>(
parsed,
schemaData,
result
);
return superValidated;
}

////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -672,12 +713,13 @@ export function superValidateSync<

const parsed =
data instanceof FormData
? parseFormData(data, schemaData, options)
? parseFormData(data, schemaData, options?.preprocessed)
: data instanceof URL || data instanceof URLSearchParams
? parseSearchParams(data, schemaData)
: {
id: undefined,
data: data as Record<string, unknown>,
data: data as z.infer<UnwrapEffects<T>>,
partialData: data as z.infer<UnwrapEffects<T>>,
posted: false
}; // Only schema, null or undefined left

Expand Down
Loading