UNPKG

sveltekit-superforms

Version:

Making SvelteKit validation and displaying of forms easier than ever!

341 lines (340 loc) 13.9 kB
import { SuperFormError } from '../index.js'; import { isInvalidPath, setPaths, traversePath, traversePaths, traversePathsAsync } from '../traversal.js'; import { errorShape, mapErrors, clearErrors } from '../errors.js'; import { clone } from '../utils.js'; import { get } from 'svelte/store'; export function validateForm(path, opts) { // See the validate function inside superForm for implementation. throw new SuperFormError('validateForm can only be used as superForm.validate.'); // eslint-disable-next-line @typescript-eslint/no-explicit-any return { path, opts }; } /** * Validate form data. */ export async function clientValidation(options, checkData, formId, constraints, posted) { return _clientValidation(options.validators, checkData, formId, constraints, posted); } async function _clientValidation(validators, checkData, formId, constraints, posted) { let valid = true; let clientErrors = {}; if (validators) { if ('safeParseAsync' in validators) { // Zod validator const validator = validators; const result = await validator.safeParseAsync(checkData); valid = result.success; if (!result.success) { clientErrors = mapErrors(result.error.format(), errorShape(validator) // eslint-disable-next-line @typescript-eslint/no-explicit-any ); } } else { // SuperForms validator checkData = { ...checkData }; // Add top-level validator fields to non-existing checkData fields // so they will be validated even if the field doesn't exist for (const [key, value] of Object.entries(validators)) { if (typeof value === 'function' && !(key in checkData)) { // @ts-expect-error Setting undefined fields so they will be validated based on field existance. checkData[key] = undefined; } } const validator = validators; const newErrors = []; await traversePathsAsync(checkData, async ({ value, path }) => { // Filter out array indices, the validator structure doesn't contain these. const validationPath = path.filter((p) => isNaN(parseInt(p))); const maybeValidator = traversePath(validator, validationPath); if (typeof maybeValidator?.value === 'function') { const check = maybeValidator.value; let errors; if (Array.isArray(value)) { for (const key in value) { try { errors = await check(value[key]); if (errors) { valid = false; newErrors.push({ path: path.concat([key]), errors: typeof errors === 'string' ? [errors] : errors ?? undefined }); } } catch (e) { valid = false; console.error(`Error in form validators for field "${path}":`, e); } } } else { try { errors = await check(value); if (errors) { valid = false; newErrors.push({ path, errors: typeof errors === 'string' ? [errors] : errors ?? undefined }); } } catch (e) { valid = false; console.error(`Error in form validators for field "${path}":`, e); } } } }); for (const { path, errors } of newErrors) { const errorPath = traversePath(clientErrors, path, ({ parent, key, value }) => { if (value === undefined) parent[key] = {}; return parent[key]; }); if (errorPath) { const { parent, key } = errorPath; parent[key] = errors; } } } } return { valid, posted, errors: clientErrors, data: checkData, constraints, message: undefined, id: formId }; } /** * Validate and set/clear object level errors. */ export async function validateObjectErrors(formOptions, data, Errors) { if (typeof formOptions.validators !== 'object' || !('safeParseAsync' in formOptions.validators)) { return; } const validators = formOptions.validators; const result = await validators.safeParseAsync(data); if (!result.success) { const newErrors = mapErrors(result.error.format(), errorShape(validators)); Errors.update((currentErrors) => { // Clear current object-level errors traversePaths(currentErrors, (pathData) => { if (pathData.key == '_errors') { return pathData.set(undefined); } }); // Add new object-level errors and tainted field errors traversePaths(newErrors, (pathData) => { if (pathData.key == '_errors') { return setPaths(currentErrors, [pathData.path], pathData.value); } }); return currentErrors; }); } else { Errors.update((currentErrors) => { // Clear current object-level errors traversePaths(currentErrors, (pathData) => { if (pathData.key == '_errors') { return pathData.set(undefined); } }); return currentErrors; }); } } /** * Validate a specific form field. * @DCI-context */ export async function validateField(path, formOptions, data, Errors, Tainted, options = {}) { function Errors_clear() { clearErrors(Errors, { undefinePath: path, clearFormLevelErrors: true }); } function Errors_update(errorMsgs) { if (typeof errorMsgs === 'string') errorMsgs = [errorMsgs]; if (options.update === true || options.update == 'errors') { Errors.update((errors) => { const error = traversePath(errors, path, (node) => { if (isInvalidPath(path, node)) { throw new SuperFormError('Errors can only be added to form fields, not to arrays or objects in the schema. Path: ' + node.path.slice(0, -1)); } else if (node.value === undefined) { node.parent[node.key] = {}; return node.parent[node.key]; } else { return node.value; } }); if (!error) throw new SuperFormError('Error path could not be created: ' + path); error.parent[error.key] = errorMsgs ?? undefined; return errors; }); } return errorMsgs ?? undefined; } const errors = await _validateField(path, formOptions.validators, data, Errors, Tainted, options); if (errors.validated) { if (errors.validated === 'all' && !errors.errors) { // We validated the whole data structure, so clear all errors on success after delayed validators. // it will also set the current path to undefined, so it can be used in // the tainted+error check in oninput. Errors_clear(); } else { return Errors_update(errors.errors); } } else if (errors.validated === false && formOptions.defaultValidator == 'clear') { return Errors_update(undefined); } return errors.errors; } // @DCI-context async function _validateField(path, validators, data, Errors, Tainted, options = {}) { if (options.update === undefined) options.update = true; if (options.taint === undefined) options.taint = false; if (typeof options.errors == 'string') options.errors = [options.errors]; const Context = { value: options.value, shouldUpdate: true, currentData: undefined, // Remove numeric indices, they're not used for validators. validationPath: path.filter((p) => isNaN(parseInt(p))) }; async function defaultValidate() { return { validated: false, errors: undefined }; } ///// Roles /////////////////////////////////////////////////////// function Tainted_isPathTainted(path, tainted) { if (tainted === undefined) return false; const leaf = traversePath(tainted, path); if (!leaf) return false; return leaf.value === true; } function Errors_update(updater) { Errors.update(updater); } function Errors_clearFormLevelErrors() { Errors.update(($errors) => { traversePaths($errors, (path) => { if (path.key == '_errors') return path.set(undefined); }); return $errors; }); } function Errors_fromZod(errors, validator) { return mapErrors(errors.format(), errorShape(validator)); } /////////////////////////////////////////////////////////////////// if (!('value' in options)) { // Use value from data Context.currentData = get(data); const dataToValidate = traversePath(Context.currentData, path); Context.value = dataToValidate?.value; } else if (options.update === true || options.update === 'value') { // Value should be updating the data data.update(($data) => { setPaths($data, [path], Context.value); return (Context.currentData = $data); }, { taint: options.taint }); } else { Context.shouldUpdate = false; } //console.log('🚀 ~ file: index.ts:871 ~ validate:', path, value); if (typeof validators !== 'object') { return defaultValidate(); } if ('safeParseAsync' in validators) { // Zod validator if (!Context.shouldUpdate) { // If value shouldn't update, clone and set the new value Context.currentData = clone(Context.currentData ?? get(data)); setPaths(Context.currentData, [path], Context.value); } const result = await validators.safeParseAsync(Context.currentData); if (!result.success) { const newErrors = Errors_fromZod(result.error, validators); if (options.update === true || options.update == 'errors') { // Set errors for other (tainted) fields, that may have been changed const taintedFields = get(Tainted); Errors_update((currentErrors) => { // Clear current object-level errors traversePaths(currentErrors, (pathData) => { if (pathData.key == '_errors') { return pathData.set(undefined); } }); // Add new object-level errors and tainted field errors traversePaths(newErrors, (pathData) => { if (pathData.key == '_errors') { return setPaths(currentErrors, [pathData.path], pathData.value); } if (!Array.isArray(pathData.value)) return; if (Tainted_isPathTainted(pathData.path, taintedFields)) { setPaths(currentErrors, [pathData.path], pathData.value); } return 'skip'; }); return currentErrors; }); } // Finally, set errors for the specific field // it will be set to undefined if no errors, so the tainted+error check // in oninput can determine if errors should be displayed or not. const current = traversePath(newErrors, path); return { validated: true, errors: options.errors ?? current?.value }; } else { // Clear form-level errors Errors_clearFormLevelErrors(); return { validated: true, errors: undefined }; } } else { // SuperForms validator const validator = traversePath(validators, Context.validationPath); if (!validator) { // Path didn't exist throw new SuperFormError('No Superforms validator found: ' + path); } else if (validator.value === undefined) { // No validator, use default return defaultValidate(); } else { const result = (await validator.value(Context.value)); return { validated: true, errors: result ? options.errors ?? result : result }; } } }