UNPKG

sveltekit-superforms

Version:

Making SvelteKit forms a pleasure to use!

262 lines (261 loc) 10.7 kB
import { SuperFormError, SchemaError } from './errors.js'; import { parse } from 'devalue'; import { schemaInfo } from './jsonSchema/schemaInfo.js'; import { defaultValues } from './jsonSchema/schemaDefaults.js'; import { setPaths } from './traversal.js'; import { splitPath } from './stringPath.js'; import { assertSchema } from './utils.js'; /** * V1 compatibilty. resetForm = false and taintedMessage = true */ let legacyMode = false; try { // @ts-expect-error Vite define check if (SUPERFORMS_LEGACY) legacyMode = true; } catch { // No legacy mode defined } const unionError = 'FormData parsing failed: Unions are only supported when the dataType option for superForm is set to "json".'; export async function parseRequest(data, schemaData, options) { let parsed; if (data instanceof FormData) { parsed = parseFormData(data, schemaData, options); } else if (data instanceof URL || data instanceof URLSearchParams) { parsed = parseSearchParams(data, schemaData, options); } else if (data instanceof Request) { parsed = await tryParseFormData(data, schemaData, options); } else if ( // RequestEvent data && typeof data === 'object' && 'request' in data && data.request instanceof Request) { parsed = await tryParseFormData(data.request, schemaData, options); } else { parsed = { id: undefined, data: data, posted: false }; } return parsed; } async function tryParseFormData(request, schemaData, options) { let formData = undefined; try { formData = await request.formData(); } catch (e) { if (e instanceof TypeError && e.message.includes('already been consumed')) { // Pass through the "body already consumed" error, which applies to // POST requests when event/request is used after formData has been fetched. throw e; } // No data found, return an empty form return { id: undefined, data: undefined, posted: false }; } return parseFormData(formData, schemaData, options); } export function parseSearchParams(data, schemaData, options) { if (data instanceof URL) data = data.searchParams; const convert = new FormData(); for (const [key, value] of data.entries()) { convert.append(key, value); } const output = parseFormData(convert, schemaData, options); // Set posted to false since it's a URL output.posted = false; return output; } export function parseFormData(formData, schemaData, options) { function tryParseSuperJson() { if (formData.has('__superform_json')) { try { const transport = options && options.transport ? Object.fromEntries(Object.entries(options.transport).map(([k, v]) => [k, v.decode])) : undefined; const output = parse(formData.getAll('__superform_json').join('') ?? '', transport); if (typeof output === 'object') { // Restore uploaded files and add to data const filePaths = Array.from(formData.keys()); for (const path of filePaths.filter((path) => path.startsWith('__superform_file_'))) { const realPath = splitPath(path.substring(17)); setPaths(output, [realPath], formData.get(path)); } for (const path of filePaths.filter((path) => path.startsWith('__superform_files_'))) { const realPath = splitPath(path.substring(18)); const allFiles = formData.getAll(path); setPaths(output, [realPath], Array.from(allFiles)); } return output; } } catch { // } } return null; } const data = tryParseSuperJson(); const id = formData.get('__superform_id')?.toString(); return data ? { id, data, posted: true } : { id, data: _parseFormData(formData, schemaData, options), posted: true }; } function _parseFormData(formData, schema, options) { const output = {}; let schemaKeys; if (options?.strict) { schemaKeys = new Set([...formData.keys()].filter((key) => !key.startsWith('__superform_'))); } else { let unionKeys = []; // Special fix for union schemas, then the keys must be gathered from the objects in the union if (schema.anyOf) { const info = schemaInfo(schema, false, []); if (info.union?.some((s) => s.type !== 'object')) { throw new SchemaError('All form types must be an object if schema is a union.'); } unionKeys = info.union?.flatMap((s) => Object.keys(s.properties ?? {})) ?? []; } schemaKeys = new Set([ ...unionKeys, ...Object.keys(schema.properties ?? {}), ...(schema.additionalProperties ? formData.keys() : []) ].filter((key) => !key.startsWith('__superform_'))); } function parseSingleEntry(key, entry, info) { if (options?.preprocessed && options.preprocessed.includes(key)) { return entry; } if (entry && typeof entry !== 'string') { const allowFiles = legacyMode ? options?.allowFiles === true : options?.allowFiles !== false; return !allowFiles ? undefined : entry.size ? entry : info.isNullable ? null : undefined; } if (info.types.length > 1) { throw new SchemaError(unionError, key); } let [type] = info.types; if (!info.types.length && info.schema.enum) { // Special case for Typescript enums // If the entry is an integer, parse it as such, otherwise string if (info.schema.enum.includes(entry)) type = 'string'; else { type = Number.isInteger(parseInt(entry, 10)) ? 'integer' : 'string'; } } return parseFormDataEntry(key, entry, type ?? 'any', info); } const defaultPropertyType = typeof schema.additionalProperties == 'object' ? schema.additionalProperties : { type: 'string' }; for (const key of schemaKeys) { const property = schema.properties ? schema.properties[key] : defaultPropertyType; assertSchema(property, key); const info = schemaInfo(property ?? defaultPropertyType, !schema.required?.includes(key), [ key ]); if (!info) continue; if (!info.types.includes('boolean') && !schema.additionalProperties && !formData.has(key)) { continue; } const entries = formData.getAll(key); if (info.union && info.union.length > 1) { throw new SchemaError(unionError, key); } if (info.types.includes('array') || info.types.includes('set')) { // If no items, it could be a union containing the info const items = property.items ?? (info.union?.length == 1 ? info.union[0] : undefined); if (!items || typeof items == 'boolean' || (Array.isArray(items) && items.length != 1)) { throw new SchemaError('Arrays must have a single "items" property that defines its type.', key); } const arrayType = Array.isArray(items) ? items[0] : items; assertSchema(arrayType, key); const arrayInfo = schemaInfo(arrayType, info.isOptional, [key]); if (!arrayInfo) continue; // Check for empty files being posted (and filtered) const isFileArray = entries.length && entries.some((e) => e && typeof e !== 'string'); const arrayData = entries.map((e) => parseSingleEntry(key, e, arrayInfo)); if (isFileArray && arrayData.every((file) => !file)) arrayData.length = 0; output[key] = info.types.includes('set') ? new Set(arrayData) : arrayData; } else { output[key] = parseSingleEntry(key, entries[entries.length - 1], info); } } return output; } function parseFormDataEntry(key, value, type, info) { //console.log(`Parsing FormData ${key} (${type}): "${value}"`, info); //debug if (!value) { //console.log(`No FormData for "${key}" (${type}).`, info); //debug // Special case for booleans with default value true if (type == 'boolean' && info.isOptional && info.schema.default === true) { return false; } const defaultValue = defaultValues(info.schema, info.isOptional, [key]); // Special case for empty posted enums, then the empty value should be returned, // otherwise even a required field will get a default value, resulting in that // posting missing enum values must use strict mode. if (info.schema.enum && defaultValue !== null && defaultValue !== undefined) { return value; } if (defaultValue !== undefined) return defaultValue; if (info.isNullable) return null; if (info.isOptional) return undefined; } function typeError() { throw new SchemaError(type[0].toUpperCase() + type.slice(1) + ` type found. ` + `Set the dataType option to "json" and add use:enhance on the client to use nested data structures. ` + `More information: https://superforms.rocks/concepts/nested-data`, key); } switch (type) { case 'string': case 'any': return value; case 'integer': return parseInt(value ?? '', 10); case 'number': return parseFloat(value ?? ''); case 'boolean': return Boolean(value == 'false' ? '' : value).valueOf(); case 'unix-time': { // Must return undefined for invalid dates due to https://github.com/Rich-Harris/devalue/issues/51 const date = new Date(value ?? ''); return !isNaN(date) ? date : undefined; } case 'int64': case 'bigint': return BigInt(value ?? '.'); case 'symbol': return Symbol(String(value)); case 'set': case 'array': case 'object': return typeError(); default: throw new SuperFormError('Unsupported schema type for FormData: ' + type); } }