Skip to content

Commit 576a0a3

Browse files
committed
tainted wasn't updated correctly for array data.
added more taint options to form store.
1 parent d8405b8 commit 576a0a3

File tree

8 files changed

+292
-87
lines changed

8 files changed

+292
-87
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
### Fixed
1919

20+
- `tainted` wasn't updated correctly for array data.
2021
- `dataType: 'json'` now handles large (+1Mb) payloads.
2122

2223
### Added
2324

2425
- Added `validate` to `superForm`, which can be used to validate any field, at any time.
25-
- The option `{ taint: boolean }` has been added to `form.set` and `form.update`.
26+
- The option `{ taint: boolean | 'untaint' | 'untaint-all' }` has been added to `form.set` and `form.update`.
2627
- The `resetForm` option can now take an `async () => boolean` function to determine whether the form should be resetted or not.
2728

2829
## [0.7.1] - 2023-04-17

src/compare.test.ts

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -325,28 +325,58 @@ describe('Proxies', () => {
325325
});
326326
});
327327

328-
test('Compare paths', () => {
329-
const obj1 = {
330-
name: 'Obj1',
331-
tags: [{ name: 'tag1' }, { name: 'tag2' }],
332-
deep: {
333-
test: true
334-
}
335-
};
328+
describe('Path comparisons', () => {
329+
test('Basic path comparison', () => {
330+
const obj1 = {
331+
name: 'Obj1',
332+
tags: [{ name: 'tag1' }, { name: 'tag2' }],
333+
deep: {
334+
test: true
335+
}
336+
};
336337

337-
const obj2 = {
338-
name: 'Obj2',
339-
tags: [{ name: 'tag1' }, { name: 'tag4' }]
340-
};
338+
const obj2 = {
339+
name: 'Obj2',
340+
tags: [{ name: 'tag1' }, { name: 'tag4' }]
341+
};
342+
343+
//expect(comparePaths(obj1, obj1)).toStrictEqual([]);
344+
//expect(comparePaths(obj1, structuredClone(obj1))).toStrictEqual([]);
345+
346+
expect(comparePaths(obj1, obj2)).toStrictEqual([
347+
['name'],
348+
['tags', '1', 'name'],
349+
['deep', 'test']
350+
]);
351+
});
352+
353+
test('Paths with empty arrays', () => {
354+
const obj1 = {
355+
flavours: [],
356+
scoops: 1
357+
};
341358

342-
//expect(comparePaths(obj1, obj1)).toStrictEqual([]);
343-
//expect(comparePaths(obj1, structuredClone(obj1))).toStrictEqual([]);
359+
const obj2 = {
360+
flavours: [],
361+
scoops: 1
362+
};
363+
364+
expect(comparePaths(obj1, obj2)).toStrictEqual([]);
365+
});
344366

345-
expect(comparePaths(obj1, obj2)).toStrictEqual([
346-
['name'],
347-
['tags', '1', 'name'],
348-
['deep', 'test']
349-
]);
367+
test('Paths with arrays', () => {
368+
const obj1 = {
369+
flavours: [],
370+
scoops: 1
371+
};
372+
373+
const obj2 = {
374+
flavours: ['Mint choc chip'],
375+
scoops: 1
376+
};
377+
378+
expect(comparePaths(obj1, obj2)).toStrictEqual([['flavours', '0']]);
379+
});
350380
});
351381

352382
test('Set paths', () => {

src/lib/client/index.ts

Lines changed: 76 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,34 @@ type SuperFormEventList<T extends AnyZodObject, M> = {
202202
>[];
203203
};
204204

205+
type ValidateOptions<V> = Partial<{
206+
value: V;
207+
update: boolean;
208+
errors: string | string[];
209+
}>;
210+
211+
type Validate<
212+
T extends AnyZodObject,
213+
P extends FieldPath<z.infer<T>>, // = FieldPath<z.infer<T>>,
214+
Path extends keyof z.infer<T> | P // = keyof z.infer<T> | P
215+
> = (
216+
path: Path,
217+
opts?: ValidateOptions<unknown>
218+
) => Promise<string[] | undefined>;
219+
205220
// eslint-disable-next-line @typescript-eslint/no-explicit-any
206221
export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
207222
form: {
208223
subscribe: Readable<z.infer<T>>['subscribe'];
209-
set(this: void, value: z.infer<T>, options?: { taint?: boolean }): void;
224+
set(
225+
this: void,
226+
value: z.infer<T>,
227+
options?: { taint?: boolean | 'untaint' | 'untaint-all' }
228+
): void;
210229
update(
211230
this: void,
212231
updater: Updater<z.infer<T>>,
213-
options?: { taint?: boolean }
232+
options?: { taint?: boolean | 'untaint' | 'untaint-all' }
214233
): void;
215234
};
216235
formId: Writable<string | undefined>;
@@ -242,13 +261,11 @@ export type SuperForm<T extends ZodValidation<AnyZodObject>, M = any> = {
242261
capture: () => SuperFormSnapshot<UnwrapEffects<T>, M>;
243262
restore: (snapshot: SuperFormSnapshot<UnwrapEffects<T>, M>) => void;
244263

245-
validate: (
246-
path: keyof z.infer<T> | FieldPath<z.infer<T>>,
247-
opts?: {
248-
value?: unknown;
249-
update?: boolean;
250-
}
251-
) => Promise<string[] | undefined>;
264+
validate: Validate<
265+
UnwrapEffects<T>,
266+
FieldPath<z.infer<UnwrapEffects<T>>>,
267+
keyof z.infer<UnwrapEffects<T>> | FieldPath<z.infer<UnwrapEffects<T>>>
268+
>;
252269
};
253270

254271
/**
@@ -402,27 +419,23 @@ export function superForm<
402419
subscribe: _formData.subscribe,
403420
set: (
404421
value: Parameters<typeof _formData.set>[0],
405-
options: { taint?: boolean } = {}
422+
options: { taint?: boolean | 'untaint' | 'untaint-all' } = {}
406423
) => {
407-
if (options.taint !== false && !get(Submitting) && taintedFormState) {
408-
checkTainted(value, taintedFormState);
409-
}
424+
//if (!get(Submitting)) {
425+
checkTainted(value, taintedFormState, options.taint ?? true);
426+
//}
410427
taintedFormState = clone(value);
411428
return _formData.set(value);
412429
},
413430
update: (
414431
updater: Parameters<typeof _formData.update>[0],
415-
options: { taint?: boolean } = {}
432+
options: { taint?: boolean | 'untaint' | 'untaint-all' } = {}
416433
) => {
417434
return _formData.update((value) => {
418435
const output = updater(value);
419-
if (
420-
options.taint !== false &&
421-
!get(Submitting) &&
422-
taintedFormState
423-
) {
424-
checkTainted(output, taintedFormState);
425-
}
436+
//if (!get(Submitting)) {
437+
checkTainted(output, taintedFormState, options.taint ?? true);
438+
//}
426439
taintedFormState = clone(value);
427440
return output;
428441
});
@@ -431,17 +444,29 @@ export function superForm<
431444

432445
const LastChanges = writable<string[][]>([]);
433446

434-
function checkTainted(newObj: unknown, compareAgainst: unknown) {
447+
function checkTainted(
448+
newObj: unknown,
449+
compareAgainst: unknown,
450+
options: boolean | 'untaint' | 'untaint-all'
451+
) {
452+
if (options === false) {
453+
return;
454+
} else if (options === 'untaint-all') {
455+
Tainted.set(undefined);
456+
return;
457+
}
458+
435459
const paths = comparePaths(newObj, compareAgainst);
436-
//console.log('🚀 ~ file: index.ts:449 ~ checkTainted ~ paths:', paths);
437460

438-
LastChanges.set(paths);
461+
if (options === true) {
462+
LastChanges.set(paths);
463+
}
439464

440465
if (paths.length) {
441466
Tainted.update((tainted) => {
442467
//console.log('Update tainted:', paths, newObj, compareAgainst);
443468
if (!tainted) tainted = {};
444-
setPaths(tainted, paths, true);
469+
setPaths(tainted, paths, options === true ? true : undefined);
445470
return tainted;
446471
});
447472
}
@@ -734,20 +759,14 @@ export function superForm<
734759
rebind(snapshot, snapshot.tainted ?? true);
735760
},
736761

737-
validate: (
738-
path: keyof z.infer<T> | FieldPath<z.infer<T>>,
739-
opts: {
740-
value?: unknown;
741-
update?: boolean;
742-
} = {}
743-
) => {
762+
validate: (path, opts) => {
744763
return validateField(
745764
(Array.isArray(path) ? path : [path]) as string[],
746765
options.validators,
747-
get(Form),
748-
opts.update === false ? undefined : Errors,
749766
options.defaultValidator,
750-
opts.value
767+
get(Form),
768+
Errors,
769+
opts
751770
);
752771
},
753772
enhance: (el: HTMLFormElement, events?: SuperFormEvents<T2, M>) => {
@@ -817,25 +836,25 @@ function shouldSyncFlash<T extends AnyZodObject, M>(
817836
async function validateField<T extends AnyZodObject, M>(
818837
path: string[],
819838
validators: FormOptions<T, M>['validators'],
820-
data: z.infer<T>,
821-
errors: SuperForm<T, M>['errors'] | undefined,
822839
defaultValidator: FormOptions<T, M>['defaultValidator'],
823-
value?: unknown
840+
data: z.infer<T>,
841+
errors: SuperForm<T, M>['errors'],
842+
options: ValidateOptions<unknown> = {}
824843
): Promise<string[] | undefined> {
825844
function setError(errorMsgs: null | undefined | string | string[]) {
826845
if (typeof errorMsgs === 'string') errorMsgs = [errorMsgs];
827846

828-
if (errors) {
847+
if (options.update !== false) {
829848
errors.update((errors) => {
830849
const error = traversePath(
831850
errors,
832851
path as FieldPath<typeof errors>,
833-
(data) => {
834-
if (data.value === undefined) {
835-
data.parent[data.key] = {};
836-
return data.parent[data.key];
852+
(node) => {
853+
if (node.value === undefined) {
854+
node.parent[node.key] = {};
855+
return node.parent[node.key];
837856
} else {
838-
return data.value;
857+
return node.value;
839858
}
840859
}
841860
);
@@ -859,16 +878,21 @@ async function validateField<T extends AnyZodObject, M>(
859878
return undefined;
860879
}
861880

881+
let value = options.value;
882+
862883
if (value === undefined) {
863884
const dataToValidate = traversePath(
864885
data,
865886
path as FieldPath<typeof data>
866887
);
867888

868-
if (!dataToValidate)
889+
if (!dataToValidate) {
869890
throw new SuperFormError('Validation data not found: ' + path);
870-
else value = dataToValidate.value;
891+
}
892+
893+
value = dataToValidate.value;
871894
}
895+
//console.log('🚀 ~ file: index.ts:871 ~ validate:', path, value);
872896

873897
if (typeof validators !== 'object') {
874898
return defaultValidate();
@@ -910,7 +934,7 @@ async function validateField<T extends AnyZodObject, M>(
910934

911935
if (!result.success) {
912936
const msgs = mapErrors<T>(result.error.format());
913-
return setError(msgs._errors);
937+
return setError(options.errors ?? msgs._errors);
914938
} else {
915939
return setError(undefined);
916940
}
@@ -932,7 +956,7 @@ async function validateField<T extends AnyZodObject, M>(
932956
return defaultValidate();
933957
} else {
934958
const result = validator.value(value);
935-
return setError(result);
959+
return setError(result ? options.errors ?? result : result);
936960
}
937961
}
938962
}
@@ -983,9 +1007,9 @@ function formEnhance<T extends AnyZodObject, M>(
9831007
validateField(
9841008
change,
9851009
options.validators,
1010+
options.defaultValidator,
9861011
get(data),
987-
errors,
988-
options.defaultValidator
1012+
errors
9891013
);
9901014
}
9911015
}
@@ -1015,9 +1039,9 @@ function formEnhance<T extends AnyZodObject, M>(
10151039
validateField(
10161040
change,
10171041
options.validators,
1042+
options.defaultValidator,
10181043
get(data),
1019-
errors,
1020-
options.defaultValidator
1044+
errors
10211045
);
10221046
}
10231047
}

src/lib/entity.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -214,28 +214,42 @@ export async function traversePathsAsync<
214214
* Compare two objects and return the differences as paths.
215215
*/
216216
export function comparePaths(newObj: unknown, oldObj: unknown) {
217-
const diffPaths: string[][] = [];
218-
traversePaths(newObj as object, (data) => {
217+
const diffPaths = new Map<string, string[]>();
218+
219+
function checkPath(data: FullPathData, compareTo: object) {
219220
if (data.isLeaf) {
220-
const exists = traversePath(
221-
oldObj as object,
222-
data.path as FieldPath<object>
223-
);
221+
const exists = traversePath(compareTo, data.path as FieldPath<object>);
222+
223+
/*
224+
console.log('----------- Compare ------------ ');
225+
console.log(data);
226+
console.log('with');
227+
console.log(exists);
228+
*/
229+
224230
if (!exists) {
225-
diffPaths.push(data.path);
231+
diffPaths.set(data.path.join(' '), data.path);
226232
} else if (
227233
data.value instanceof Date &&
228234
exists.value instanceof Date &&
229235
data.value.getTime() != exists.value.getTime()
230236
) {
231-
diffPaths.push(data.path);
237+
diffPaths.set(data.path.join(' '), data.path);
232238
} else if (data.value !== exists.value) {
233-
diffPaths.push(data.path);
239+
diffPaths.set(data.path.join(' '), data.path);
234240
}
235241
}
236-
});
242+
}
243+
244+
traversePaths(newObj as object, (data) =>
245+
checkPath(data, oldObj as object)
246+
);
247+
248+
traversePaths(oldObj as object, (data) =>
249+
checkPath(data, newObj as object)
250+
);
237251

238-
return diffPaths;
252+
return Array.from(diffPaths.values());
239253
}
240254

241255
export function setPaths(

0 commit comments

Comments
 (0)