Skip to content

Commit 8b59774

Browse files
committed
Checking if whole object has side-effects.
1 parent b48872a commit 8b59774

File tree

3 files changed

+56
-23
lines changed

3 files changed

+56
-23
lines changed

src/compare.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { pathExists, traversePath, traversePathAsync } from '$lib/entity';
1818
import { get, writable } from 'svelte/store';
1919
import { mapErrors } from '$lib/entity';
20-
import { unwrapZodType } from '$lib/server/entity';
20+
import { hasEffects, unwrapZodType } from '$lib/server/entity';
2121
import { superValidate } from '$lib/server';
2222
import {
2323
booleanProxy,
@@ -457,15 +457,18 @@ test('Check path existence', () => {
457457
});
458458
});
459459

460-
/*
461-
test('String paths', () => {
462-
type Social = z.infer<typeof social>;
463-
const i = 5;
464-
const i2 = '5';
465-
466-
const path1: StringPath<Social> = `user.tags.${i}.name`;
467-
const path2: StringPath<Social> = `user.tags.${i2}.name`;
460+
const refined = z.object({
461+
id: z.number().int().positive(),
462+
name: z.string().min(2),
463+
email: z.string().email().nullable(),
464+
tags: z
465+
.object({ name: z.string().min(1) })
466+
.refine((data) => data.name.length > 5)
467+
.array()
468+
.optional()
469+
});
468470

469-
expect(path1).toEqual(path2);
471+
test.only('Checking side effects', () => {
472+
expect(hasEffects(social)).toStrictEqual(false);
473+
expect(hasEffects(refined)).toStrictEqual(true);
470474
});
471-
*/

src/lib/client/index.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import {
5252
} from '../entity.js';
5353
import { fieldProxy } from './proxies.js';
5454
import { clone } from '../utils.js';
55-
import type { Entity } from '../schemaEntity.js';
55+
import { hasEffects, type Entity } from '../schemaEntity.js';
5656
import { unwrapZodType } from '../schemaEntity.js';
5757

5858
enum FetchStatus {
@@ -843,6 +843,8 @@ function shouldSyncFlash<T extends AnyZodObject, M>(
843843
return options.syncFlashMessage;
844844
}
845845

846+
const effectMapCache = new WeakMap<object, boolean>();
847+
846848
async function validateField<T extends AnyZodObject, M>(
847849
path: string[],
848850
validators: FormOptions<T, M>['validators'],
@@ -940,7 +942,7 @@ async function validateField<T extends AnyZodObject, M>(
940942
const unwrapped = unwrapZodType(nextType);
941943
return unwrapped.effects ? undefined : unwrapped.zodType;
942944
} else if (type._def.typeName == 'ZodArray') {
943-
const array = type as ZodArray<ZodAny>;
945+
const array = type as ZodArray<ZodTypeAny>;
944946
const unwrapped = unwrapZodType(array.element);
945947
if (unwrapped.effects) return undefined;
946948
return extractValidator(unwrapped, key);
@@ -952,16 +954,24 @@ async function validateField<T extends AnyZodObject, M>(
952954
if ('safeParseAsync' in validators) {
953955
// Zod validator
954956
// Check if any effects exist for the path, then parse the entire schema.
955-
const noEffects = traversePath(
956-
validators,
957-
validationPath as FieldPath<typeof validators>,
958-
(pathData) => {
959-
return extractValidator(
960-
unwrapZodType(pathData.parent),
961-
pathData.key
957+
if (!effectMapCache.has(validators)) {
958+
effectMapCache.set(validators, hasEffects(validators as ZodTypeAny));
959+
}
960+
961+
const effects = effectMapCache.get(validators);
962+
963+
const noEffects = effects
964+
? undefined
965+
: traversePath(
966+
validators,
967+
validationPath as FieldPath<typeof validators>,
968+
(pathData) => {
969+
return extractValidator(
970+
unwrapZodType(pathData.parent),
971+
pathData.key
972+
);
973+
}
962974
);
963-
}
964-
);
965975

966976
if (noEffects) {
967977
const validator = extractValidator(

src/lib/schemaEntity.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
ZodBigInt,
2323
ZodObject,
2424
ZodSymbol,
25-
ZodRecord
25+
ZodRecord,
26+
ZodAny
2627
} from 'zod';
2728

2829
export type UnwrappedEntity<T> = T extends ZodOptional<infer U>
@@ -52,6 +53,25 @@ export type Entity<T extends AnyZodObject> = {
5253
keys: string[];
5354
};
5455

56+
export function hasEffects(zodType: ZodTypeAny): boolean {
57+
const type = unwrapZodType(zodType);
58+
if (type.effects) return true;
59+
60+
const name = type.zodType._def.typeName;
61+
62+
if (name == 'ZodObject') {
63+
const obj = type.zodType as AnyZodObject;
64+
for (const field of Object.values(obj._def.shape())) {
65+
if (hasEffects(field as ZodTypeAny)) return true;
66+
}
67+
} else if (name == 'ZodArray') {
68+
const array = type.zodType as ZodArray<ZodTypeAny>;
69+
return hasEffects(array.element);
70+
}
71+
72+
return false;
73+
}
74+
5575
export function unwrapZodType(zodType: ZodTypeAny): ZodTypeInfo {
5676
let _wrapped = true;
5777
let isNullable = false;

0 commit comments

Comments
 (0)