Skip to content

Commit 519b4ec

Browse files
committed
Tests for array and object errors.
1 parent 2a76afc commit 519b4ec

File tree

6 files changed

+135
-133
lines changed

6 files changed

+135
-133
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Changed
1717

18+
- As with effects, array and object errors now forces the whole Zod schema to be validated client-side.
1819
- The `Validation` type is now called `SuperValidated`.
1920
- `StringPath` and `StringPathLeaves` are renamed to `FormPath` and `FormPathLeaves`.
2021

@@ -29,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2930

3031
### Added
3132

33+
- Errors can now be added to arrays and objects in the schema.
3234
- Added a `posted` store, a boolean which is false if the form hasn't been posted during its current lifetime.
3335
- `reset` now has an additional `data` option that can be used to re-populate the form with data, and `id` to set a different form id.
3436
- `intProxy`, `numberProxy`, `dateProxy` and `stringProxy` now have an `empty` option, so empty values can be set to `null` or `undefined`.

src/lib/client/index.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import {
3535
type FormPath,
3636
type FormPathLeaves
3737
} from '../stringPath.js';
38-
import { validateField, type Validate } from './validateField.js';
38+
import {
39+
validateField,
40+
type Validate,
41+
validateObjectErrors
42+
} from './validateField.js';
3943
import {
4044
formEnhance,
4145
shouldSyncFlash,
@@ -596,7 +600,7 @@ export function superForm<
596600
options.validationMethod == 'onblur' ||
597601
options.validationMethod == 'submit-only'
598602
) {
599-
return;
603+
return false;
600604
}
601605

602606
let shouldValidate = options.validationMethod === 'oninput';
@@ -630,39 +634,43 @@ export function superForm<
630634

631635
if (shouldValidate) {
632636
await validateField(path, options, Form, Errors, Tainted, { taint });
637+
return true;
638+
} else {
639+
return false;
633640
}
634641
}
635642

636-
function Tainted_update(
643+
async function Tainted_update(
637644
newObj: unknown,
638645
compareAgainst: unknown,
639-
options: TaintOption
646+
taintOptions: TaintOption
640647
) {
641-
if (options === false) {
648+
if (taintOptions === false) {
642649
return;
643-
} else if (options === 'untaint-all') {
650+
} else if (taintOptions === 'untaint-all') {
644651
Tainted.set(undefined);
645652
return;
646653
}
647654

648655
const paths = comparePaths(newObj, compareAgainst);
649656

650-
if (options === true) {
657+
if (taintOptions === true) {
651658
LastChanges.set(paths);
652659
}
653660

654661
if (paths.length) {
655662
Tainted.update((tainted) => {
656663
//console.log('Update tainted:', paths, newObj, compareAgainst);
657664
if (!tainted) tainted = {};
658-
setPaths(tainted, paths, options === true ? true : undefined);
665+
setPaths(tainted, paths, taintOptions === true ? true : undefined);
659666
return tainted;
660667
});
661668

669+
let updated = false;
662670
for (const path of paths) {
663-
//console.log('🚀 ~ file: index.ts:681 ~ path:', path);
664-
Tainted__validate(path, options);
671+
updated = updated || (await Tainted__validate(path, taintOptions));
665672
}
673+
if (!updated) await validateObjectErrors(options, get(Form), Errors);
666674
}
667675
}
668676

src/lib/client/validateField.ts

Lines changed: 85 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import type { z, AnyZodObject, ZodTypeAny, ZodArray, ZodError } from 'zod';
1+
import type { z, AnyZodObject, ZodTypeAny, ZodError } from 'zod';
22
import { get } from 'svelte/store';
33
import type { FormOptions, SuperForm, TaintOption } from './index.js';
44
import {
55
SuperFormError,
66
type FieldPath,
77
type TaintedFields,
8-
type ValidationErrors,
98
type UnwrapEffects,
109
type Validators
1110
} from '../index.js';
@@ -15,8 +14,6 @@ import {
1514
traversePath,
1615
traversePaths
1716
} from '../traversal.js';
18-
import { hasEffects, type ZodTypeInfo } from '../schemaEntity.js';
19-
import { unwrapZodType } from '../schemaEntity.js';
2017
import type { FormPathLeaves } from '../stringPath.js';
2118
import { clearErrors, clone } from '../utils.js';
2219
import { errorShape, mapErrors } from '../errors.js';
@@ -36,7 +33,63 @@ export type Validate<
3633
opts?: ValidateOptions<unknown>
3734
) => Promise<string[] | undefined>;
3835

39-
const effectMapCache = new WeakMap<object, boolean>();
36+
export async function validateObjectErrors<T extends AnyZodObject, M>(
37+
formOptions: FormOptions<T, M>,
38+
data: z.infer<T>,
39+
Errors: SuperForm<T, M>['errors']
40+
) {
41+
if (
42+
typeof formOptions.validators !== 'object' ||
43+
!('safeParseAsync' in formOptions.validators)
44+
) {
45+
return;
46+
}
47+
48+
const validators = formOptions.validators as AnyZodObject;
49+
const result = await validators.safeParseAsync(data);
50+
51+
if (!result.success) {
52+
const newErrors = mapErrors(
53+
result.error.format(),
54+
errorShape(validators as AnyZodObject)
55+
);
56+
57+
Errors.update((currentErrors) => {
58+
// Clear current object-level errors
59+
traversePaths(currentErrors, (pathData) => {
60+
if (pathData.key == '_errors') {
61+
return pathData.set(undefined);
62+
}
63+
});
64+
65+
// Add new object-level errors and tainted field errors
66+
traversePaths(newErrors, (pathData) => {
67+
if (pathData.key == '_errors') {
68+
return setPaths(currentErrors, [pathData.path], pathData.value);
69+
}
70+
/*
71+
if (!Array.isArray(pathData.value)) return;
72+
if (Tainted_isPathTainted(pathData.path, taintedFields)) {
73+
setPaths(currentErrors, [pathData.path], pathData.value);
74+
}
75+
return 'skip';
76+
*/
77+
});
78+
79+
return currentErrors;
80+
});
81+
} else {
82+
Errors.update((currentErrors) => {
83+
// Clear current object-level errors
84+
traversePaths(currentErrors, (pathData) => {
85+
if (pathData.key == '_errors') {
86+
return pathData.set(undefined);
87+
}
88+
});
89+
return currentErrors;
90+
});
91+
}
92+
}
4093

4194
// @DCI-context
4295
export async function validateField<T extends AnyZodObject, M>(
@@ -139,30 +192,6 @@ async function _validateField<T extends AnyZodObject, M>(
139192
return { validated: false, errors: undefined } as const;
140193
}
141194

142-
function extractValidator(
143-
data: ZodTypeInfo,
144-
key: string
145-
): ZodTypeAny | undefined {
146-
if (data.effects) return undefined;
147-
148-
// No effects, check if ZodObject or ZodArray, which are the
149-
// "allowed" objects in the path above the leaf.
150-
const type = data.zodType;
151-
152-
if (type._def.typeName == 'ZodObject') {
153-
const nextType = (type as AnyZodObject)._def.shape()[key];
154-
const unwrapped = unwrapZodType(nextType);
155-
return unwrapped.effects ? undefined : unwrapped.zodType;
156-
} else if (type._def.typeName == 'ZodArray') {
157-
const array = type as ZodArray<ZodTypeAny>;
158-
const unwrapped = unwrapZodType(array.element);
159-
if (unwrapped.effects) return undefined;
160-
return extractValidator(unwrapped, key);
161-
} else {
162-
throw new SuperFormError('Invalid validator');
163-
}
164-
}
165-
166195
///// Roles ///////////////////////////////////////////////////////
167196

168197
function Tainted_isPathTainted(
@@ -175,12 +204,8 @@ async function _validateField<T extends AnyZodObject, M>(
175204
return leaf.value === true;
176205
}
177206

178-
function Errors_get() {
179-
return get(Errors);
180-
}
181-
182-
function Errors_set(newErrors: ValidationErrors<UnwrapEffects<T>>) {
183-
Errors.set(newErrors);
207+
function Errors_update(updater: Parameters<typeof Errors.update>[0]) {
208+
Errors.update(updater);
184209
}
185210

186211
function Errors_clearFormLevelErrors() {
@@ -230,63 +255,6 @@ async function _validateField<T extends AnyZodObject, M>(
230255

231256
if ('safeParseAsync' in validators) {
232257
// Zod validator
233-
// Check if any effects exist for the path, then parse the entire schema.
234-
if (!effectMapCache.has(validators)) {
235-
effectMapCache.set(validators, hasEffects(validators as ZodTypeAny));
236-
}
237-
238-
const effects = effectMapCache.get(validators);
239-
240-
const perFieldValidator = effects
241-
? undefined
242-
: traversePath(
243-
validators,
244-
Context.validationPath as FieldPath<typeof validators>,
245-
(pathData) => {
246-
return extractValidator(
247-
unwrapZodType(pathData.parent),
248-
pathData.key
249-
);
250-
}
251-
);
252-
253-
if (perFieldValidator) {
254-
const validator = extractValidator(
255-
unwrapZodType(perFieldValidator.parent),
256-
perFieldValidator.key
257-
);
258-
if (validator) {
259-
// Check if validator is ZodArray and the path is an array access
260-
// in that case validate the whole array.
261-
if (
262-
Context.currentData &&
263-
validator._def.typeName == 'ZodArray' &&
264-
!isNaN(parseInt(path[path.length - 1]))
265-
) {
266-
const validateArray = traversePath(
267-
Context.currentData,
268-
path.slice(0, -1) as FieldPath<typeof Context.currentData>
269-
);
270-
Context.value = validateArray?.value;
271-
}
272-
273-
//console.log('🚀 ~ file: index.ts:972 ~ no effects:', validator);
274-
const result = await validator.safeParseAsync(Context.value);
275-
if (!result.success) {
276-
const errors = result.error.format();
277-
return {
278-
validated: true,
279-
errors: errors._errors
280-
};
281-
} else {
282-
return { validated: true, errors: undefined };
283-
}
284-
}
285-
}
286-
287-
//console.log('🚀 ~ file: index.ts:983 ~ Effects found, validating all');
288-
289-
// Effects are found, validate entire data, unfortunately
290258
if (!Context.shouldUpdate) {
291259
// If value shouldn't update, clone and set the new value
292260
Context.currentData = clone(Context.currentData ?? get(data));
@@ -298,7 +266,6 @@ async function _validateField<T extends AnyZodObject, M>(
298266
);
299267

300268
if (!result.success) {
301-
let currentErrors: ValidationErrors<UnwrapEffects<T>> = {};
302269
const newErrors = Errors_fromZod(
303270
result.error,
304271
validators as AnyZodObject
@@ -307,28 +274,33 @@ async function _validateField<T extends AnyZodObject, M>(
307274
if (options.update === true || options.update == 'errors') {
308275
// Set errors for other (tainted) fields, that may have been changed
309276
const taintedFields = get(Tainted);
310-
currentErrors = Errors_get();
311-
312-
// Special check for form level errors
313-
if (currentErrors._errors !== newErrors._errors) {
314-
if (
315-
!currentErrors._errors ||
316-
!newErrors._errors ||
317-
currentErrors._errors.join('') != newErrors._errors.join('')
318-
) {
319-
currentErrors._errors = newErrors._errors;
320-
}
321-
}
322277

323-
traversePaths(newErrors, (pathData) => {
324-
if (!Array.isArray(pathData.value)) return;
325-
if (Tainted_isPathTainted(pathData.path, taintedFields)) {
326-
setPaths(currentErrors, [pathData.path], pathData.value);
327-
}
328-
return 'skip';
329-
});
278+
Errors_update((currentErrors) => {
279+
// Clear current object-level errors
280+
traversePaths(currentErrors, (pathData) => {
281+
if (pathData.key == '_errors') {
282+
return pathData.set(undefined);
283+
}
284+
});
285+
286+
// Add new object-level errors and tainted field errors
287+
traversePaths(newErrors, (pathData) => {
288+
if (pathData.key == '_errors') {
289+
return setPaths(
290+
currentErrors,
291+
[pathData.path],
292+
pathData.value
293+
);
294+
}
295+
if (!Array.isArray(pathData.value)) return;
296+
if (Tainted_isPathTainted(pathData.path, taintedFields)) {
297+
setPaths(currentErrors, [pathData.path], pathData.value);
298+
}
299+
return 'skip';
300+
});
330301

331-
Errors_set(currentErrors);
302+
return currentErrors;
303+
});
332304
}
333305

334306
// Finally, set errors for the specific field

src/lib/superValidate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function message<T extends ZodValidation<AnyZodObject>, M>(
5353
form.message = message;
5454
if (options?.status && options.status >= 400) form.valid = false;
5555

56-
return form.valid ? fail(options?.status ?? 400, { form }) : { form };
56+
return !form.valid ? fail(options?.status ?? 400, { form }) : { form };
5757
}
5858

5959
export const setMessage = message;

0 commit comments

Comments
 (0)