@@ -6,19 +6,40 @@ import {
66 type FieldPath ,
77 type ZodValidation ,
88 type FormPathLeaves ,
9- SuperFormError
9+ SuperFormError ,
10+ type TaintedFields
1011} from '../index.js' ;
11- import type { z , AnyZodObject } from 'zod' ;
12- import { traversePath , traversePathsAsync } from '../traversal.js' ;
13- import type { FormOptions } from './index.js' ;
12+ import type { z , AnyZodObject , ZodError , ZodTypeAny } from 'zod' ;
13+ import {
14+ isInvalidPath ,
15+ setPaths ,
16+ traversePath ,
17+ traversePaths ,
18+ traversePathsAsync
19+ } from '../traversal.js' ;
20+ import type { FormOptions , SuperForm , TaintOption } from './index.js' ;
1421import { errorShape , mapErrors } from '../errors.js' ;
15- import type { ValidateOptions } from './validateField.js' ;
16- import type { FormPathType } from '$lib/stringPath.js' ;
22+ import type { FormPathType } from '../stringPath.js' ;
23+ import { clearErrors , clone } from '../utils.js' ;
24+ import { get } from 'svelte/store' ;
25+
26+ export type ValidateOptions < V > = Partial < {
27+ value : V ;
28+ update : boolean | 'errors' | 'value' ;
29+ taint : TaintOption ;
30+ errors : string | string [ ] ;
31+ } > ;
1732
33+ /**
34+ * Validate current form data.
35+ */
1836export function validateForm < T extends AnyZodObject > ( ) : Promise <
1937 SuperValidated < ZodValidation < T > >
2038> ;
2139
40+ /**
41+ * Validate a specific field in the form.
42+ */
2243export function validateForm < T extends AnyZodObject > (
2344 path : FormPathLeaves < z . infer < T > > ,
2445 opts ?: ValidateOptions <
@@ -40,6 +61,9 @@ export function validateForm<T extends AnyZodObject>(
4061 return { path, opts } as any ;
4162}
4263
64+ /**
65+ * Validate form data.
66+ */
4367export async function clientValidation < T extends AnyZodObject , M = unknown > (
4468 options : FormOptions < T , M > ,
4569 checkData : z . infer < T > ,
@@ -171,3 +195,316 @@ async function _clientValidation<T extends AnyZodObject, M = unknown>(
171195 id : formId
172196 } ;
173197}
198+
199+ /**
200+ * Validate and set/clear object level errors.
201+ */
202+ export async function validateObjectErrors < T extends AnyZodObject , M > (
203+ formOptions : FormOptions < T , M > ,
204+ data : z . infer < T > ,
205+ Errors : SuperForm < T , M > [ 'errors' ]
206+ ) {
207+ if (
208+ typeof formOptions . validators !== 'object' ||
209+ ! ( 'safeParseAsync' in formOptions . validators )
210+ ) {
211+ return ;
212+ }
213+
214+ const validators = formOptions . validators as AnyZodObject ;
215+ const result = await validators . safeParseAsync ( data ) ;
216+
217+ if ( ! result . success ) {
218+ const newErrors = mapErrors (
219+ result . error . format ( ) ,
220+ errorShape ( validators as AnyZodObject )
221+ ) ;
222+
223+ Errors . update ( ( currentErrors ) => {
224+ // Clear current object-level errors
225+ traversePaths ( currentErrors , ( pathData ) => {
226+ if ( pathData . key == '_errors' ) {
227+ return pathData . set ( undefined ) ;
228+ }
229+ } ) ;
230+
231+ // Add new object-level errors and tainted field errors
232+ traversePaths ( newErrors , ( pathData ) => {
233+ if ( pathData . key == '_errors' ) {
234+ return setPaths ( currentErrors , [ pathData . path ] , pathData . value ) ;
235+ }
236+ } ) ;
237+
238+ return currentErrors ;
239+ } ) ;
240+ } else {
241+ Errors . update ( ( currentErrors ) => {
242+ // Clear current object-level errors
243+ traversePaths ( currentErrors , ( pathData ) => {
244+ if ( pathData . key == '_errors' ) {
245+ return pathData . set ( undefined ) ;
246+ }
247+ } ) ;
248+ return currentErrors ;
249+ } ) ;
250+ }
251+ }
252+
253+ /**
254+ * Validate a specific form field.
255+ * @DCI -context
256+ */
257+ export async function validateField < T extends AnyZodObject , M > (
258+ path : string [ ] ,
259+ formOptions : FormOptions < T , M > ,
260+ data : SuperForm < T , M > [ 'form' ] ,
261+ Errors : SuperForm < T , M > [ 'errors' ] ,
262+ Tainted : SuperForm < T , M > [ 'tainted' ] ,
263+ options : ValidateOptions < unknown > = { }
264+ ) : Promise < string [ ] | undefined > {
265+ function Errors_clear ( ) {
266+ clearErrors ( Errors , { undefinePath : path , clearFormLevelErrors : true } ) ;
267+ }
268+
269+ function Errors_update ( errorMsgs : null | undefined | string | string [ ] ) {
270+ if ( typeof errorMsgs === 'string' ) errorMsgs = [ errorMsgs ] ;
271+
272+ if ( options . update === true || options . update == 'errors' ) {
273+ Errors . update ( ( errors ) => {
274+ const error = traversePath (
275+ errors ,
276+ path as FieldPath < typeof errors > ,
277+ ( node ) => {
278+ if ( isInvalidPath ( path , node ) ) {
279+ throw new SuperFormError (
280+ 'Errors can only be added to form fields, not to arrays or objects in the schema. Path: ' +
281+ node . path . slice ( 0 , - 1 )
282+ ) ;
283+ } else if ( node . value === undefined ) {
284+ node . parent [ node . key ] = { } ;
285+ return node . parent [ node . key ] ;
286+ } else {
287+ return node . value ;
288+ }
289+ }
290+ ) ;
291+
292+ if ( ! error )
293+ throw new SuperFormError (
294+ 'Error path could not be created: ' + path
295+ ) ;
296+
297+ error . parent [ error . key ] = errorMsgs ?? undefined ;
298+ return errors ;
299+ } ) ;
300+ }
301+ return errorMsgs ?? undefined ;
302+ }
303+
304+ const errors = await _validateField (
305+ path ,
306+ formOptions . validators ,
307+ data ,
308+ Errors ,
309+ Tainted ,
310+ options
311+ ) ;
312+
313+ if ( errors . validated ) {
314+ if ( errors . validated === 'all' && ! errors . errors ) {
315+ // We validated the whole data structure, so clear all errors on success after delayed validators.
316+ // it will also set the current path to undefined, so it can be used in
317+ // the tainted+error check in oninput.
318+ Errors_clear ( ) ;
319+ } else {
320+ return Errors_update ( errors . errors ) ;
321+ }
322+ } else if (
323+ errors . validated === false &&
324+ formOptions . defaultValidator == 'clear'
325+ ) {
326+ return Errors_update ( undefined ) ;
327+ }
328+
329+ return errors . errors ;
330+ }
331+
332+ // @DCI -context
333+ async function _validateField < T extends AnyZodObject , M > (
334+ path : string [ ] ,
335+ validators : FormOptions < T , M > [ 'validators' ] ,
336+ data : SuperForm < T , M > [ 'form' ] ,
337+ Errors : SuperForm < T , M > [ 'errors' ] ,
338+ Tainted : SuperForm < T , M > [ 'tainted' ] ,
339+ options : ValidateOptions < unknown > = { }
340+ ) : Promise < { validated : boolean | 'all' ; errors : string [ ] | undefined } > {
341+ if ( options . update === undefined ) options . update = true ;
342+ if ( options . taint === undefined ) options . taint = false ;
343+ if ( typeof options . errors == 'string' ) options . errors = [ options . errors ] ;
344+
345+ const Context = {
346+ value : options . value ,
347+ shouldUpdate : true ,
348+ currentData : undefined as z . infer < T > | undefined ,
349+ // Remove numeric indices, they're not used for validators.
350+ validationPath : path . filter ( ( p ) => isNaN ( parseInt ( p ) ) )
351+ } ;
352+
353+ async function defaultValidate ( ) {
354+ return { validated : false , errors : undefined } as const ;
355+ }
356+
357+ ///// Roles ///////////////////////////////////////////////////////
358+
359+ function Tainted_isPathTainted (
360+ path : string [ ] ,
361+ tainted : TaintedFields < AnyZodObject > | undefined
362+ ) {
363+ if ( tainted === undefined ) return false ;
364+ const leaf = traversePath ( tainted , path as FieldPath < typeof tainted > ) ;
365+ if ( ! leaf ) return false ;
366+ return leaf . value === true ;
367+ }
368+
369+ function Errors_update ( updater : Parameters < typeof Errors . update > [ 0 ] ) {
370+ Errors . update ( updater ) ;
371+ }
372+
373+ function Errors_clearFormLevelErrors ( ) {
374+ Errors . update ( ( $errors ) => {
375+ $errors . _errors = undefined ;
376+ return $errors ;
377+ } ) ;
378+ }
379+
380+ function Errors_fromZod (
381+ errors : ZodError < unknown > ,
382+ validator : AnyZodObject
383+ ) {
384+ return mapErrors ( errors . format ( ) , errorShape ( validator ) ) ;
385+ }
386+
387+ ///////////////////////////////////////////////////////////////////
388+
389+ if ( ! ( 'value' in options ) ) {
390+ // Use value from data
391+ Context . currentData = get ( data ) ;
392+
393+ const dataToValidate = traversePath (
394+ Context . currentData ,
395+ path as FieldPath < typeof Context . currentData >
396+ ) ;
397+
398+ Context . value = dataToValidate ?. value ;
399+ } else if ( options . update === true || options . update === 'value' ) {
400+ // Value should be updating the data
401+ data . update (
402+ ( $data ) => {
403+ setPaths ( $data , [ path ] , Context . value ) ;
404+ return ( Context . currentData = $data ) ;
405+ } ,
406+ { taint : options . taint }
407+ ) ;
408+ } else {
409+ Context . shouldUpdate = false ;
410+ }
411+
412+ //console.log('🚀 ~ file: index.ts:871 ~ validate:', path, value);
413+
414+ if ( typeof validators !== 'object' ) {
415+ return defaultValidate ( ) ;
416+ }
417+
418+ if ( 'safeParseAsync' in validators ) {
419+ // Zod validator
420+ if ( ! Context . shouldUpdate ) {
421+ // If value shouldn't update, clone and set the new value
422+ Context . currentData = clone ( Context . currentData ?? get ( data ) ) ;
423+ setPaths ( Context . currentData , [ path ] , Context . value ) ;
424+ }
425+
426+ const result = await ( validators as ZodTypeAny ) . safeParseAsync (
427+ Context . currentData
428+ ) ;
429+
430+ if ( ! result . success ) {
431+ const newErrors = Errors_fromZod (
432+ result . error ,
433+ validators as AnyZodObject
434+ ) ;
435+
436+ if ( options . update === true || options . update == 'errors' ) {
437+ // Set errors for other (tainted) fields, that may have been changed
438+ const taintedFields = get ( Tainted ) ;
439+
440+ Errors_update ( ( currentErrors ) => {
441+ // Clear current object-level errors
442+ traversePaths ( currentErrors , ( pathData ) => {
443+ if ( pathData . key == '_errors' ) {
444+ return pathData . set ( undefined ) ;
445+ }
446+ } ) ;
447+
448+ // Add new object-level errors and tainted field errors
449+ traversePaths ( newErrors , ( pathData ) => {
450+ if ( pathData . key == '_errors' ) {
451+ return setPaths (
452+ currentErrors ,
453+ [ pathData . path ] ,
454+ pathData . value
455+ ) ;
456+ }
457+ if ( ! Array . isArray ( pathData . value ) ) return ;
458+ if ( Tainted_isPathTainted ( pathData . path , taintedFields ) ) {
459+ setPaths ( currentErrors , [ pathData . path ] , pathData . value ) ;
460+ }
461+ return 'skip' ;
462+ } ) ;
463+
464+ return currentErrors ;
465+ } ) ;
466+ }
467+
468+ // Finally, set errors for the specific field
469+ // it will be set to undefined if no errors, so the tainted+error check
470+ // in oninput can determine if errors should be displayed or not.
471+ const current = traversePath (
472+ newErrors ,
473+ path as FieldPath < typeof newErrors >
474+ ) ;
475+
476+ return {
477+ validated : true ,
478+ errors : options . errors ?? current ?. value
479+ } ;
480+ } else {
481+ // Clear form-level errors
482+ Errors_clearFormLevelErrors ( ) ;
483+ return { validated : true , errors : undefined } ;
484+ }
485+ } else {
486+ // SuperForms validator
487+
488+ const validator = traversePath (
489+ validators ,
490+ Context . validationPath as FieldPath < typeof validators >
491+ ) ;
492+
493+ if ( ! validator ) {
494+ // Path didn't exist
495+ throw new SuperFormError ( 'No Superforms validator found: ' + path ) ;
496+ } else if ( validator . value === undefined ) {
497+ // No validator, use default
498+ return defaultValidate ( ) ;
499+ } else {
500+ const result = ( await validator . value ( Context . value ) ) as
501+ | string [ ]
502+ | undefined ;
503+
504+ return {
505+ validated : true ,
506+ errors : result ? options . errors ?? result : result
507+ } ;
508+ }
509+ }
510+ }
0 commit comments