zodselect 1.2.7 → 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/refine-schema.d.ts +1 -1
- package/dist/refine-schema.js +7 -7
- package/dist/utilities.d.ts +3 -1
- package/dist/utilities.js +6 -0
- package/package.json +7 -6
- package/dist/build-select-object.spec.d.ts +0 -1
- package/dist/build-select-object.spec.js +0 -211
- package/dist/build-select-schema.spec.d.ts +0 -1
- package/dist/build-select-schema.spec.js +0 -242
- package/dist/refine-schema.spec.d.ts +0 -1
- package/dist/refine-schema.spec.js +0 -401
- package/dist/select.spec.d.ts +0 -1
- package/dist/select.spec.js +0 -108
- package/dist/utilities.spec.d.ts +0 -1
- package/dist/utilities.spec.js +0 -94
package/dist/refine-schema.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ type Push<T extends any[], V> = [...T, V];
|
|
|
12
12
|
type ReapplyOptionalNullable<T, U extends z.ZodType> = T extends ZodOptional<ZodNullable<ZodType>> ? ZodOptional<ZodNullable<U>> : T extends ZodNullable<ZodOptional<ZodType>> ? ZodNullable<ZodOptional<U>> : T extends ZodOptional<ZodType> ? ZodOptional<U> : T extends ZodNullable<ZodType> ? ZodNullable<U> : U;
|
|
13
13
|
export type RefinedSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, TIsSimple>, TIsSimple extends boolean> = T extends ZodUnion ? RefinedUnionSchema<T, TShape> : T extends ZodType<object> ? RefinedTypeSchema<z.infer<T>, TShape> : never;
|
|
14
14
|
export type RefinedTypeSchema<T extends object, TShape, TDepth extends number = 0> = TDepth extends keyof DepthLimit ? ZodObject<{
|
|
15
|
-
[K in keyof T & keyof TShape]: TShape[K] extends (schema: infer
|
|
15
|
+
[K in keyof T & keyof TShape]: TShape[K] extends (schema: infer _S) => ZodType ? ReturnType<TShape[K]> : TShape[K] extends ZodType ? TShape[K] : TShape[K] extends boolean ? ZodifyField<T, K> : TShape[K] extends object ? Exclude<T[K], null | undefined> extends Array<infer U> ? [Extract<U, object>] extends [never] ? Zodify<T[K]> : ApplyOptionalNullable<T[K], z.ZodArray<RefinedVariants<Extract<U, object>, TShape[K], NextDepth<TDepth>>>> : [Extract<Exclude<T[K], null | undefined>, object>] extends [never] ? ZodNever : ApplyOptionalNullable<T[K], RefinedVariants<Extract<Exclude<T[K], null | undefined>, object>, TShape[K], NextDepth<TDepth>>> : ZodNever;
|
|
16
16
|
}, core.$strict> : any;
|
|
17
17
|
export type RefinedUnionSchema<T extends ZodUnion, TShape> = ZodUnion<T['options'] extends Readonly<[infer A, ...infer Rest]> ? Rest extends ZodType[] ? [
|
|
18
18
|
ReapplyOptionalNullable<A, A extends ZodType<object> ? RefinedTypeSchema<z.infer<A>, TShape> : Extract<A, ZodType>>,
|
package/dist/refine-schema.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { isZodArray, isZodCatch, isZodDefault, isZodLazy, isZodNullable, isZodObject, isZodOptional, isZodType, isZodUnion } from './utilities.js';
|
|
3
3
|
// Main function to refine the schema
|
|
4
4
|
export function refineSchema(schema, shape) {
|
|
5
5
|
if (isZodObject(schema)) {
|
|
@@ -45,12 +45,12 @@ function refineSchemaField(fieldSchema, fieldShape) {
|
|
|
45
45
|
else if (isZodNullable(fieldSchema)) {
|
|
46
46
|
return refineSchemaField(fieldSchema.def.innerType, fieldShape).nullable();
|
|
47
47
|
}
|
|
48
|
-
else {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
else if (isZodDefault(fieldSchema) || isZodCatch(fieldSchema)) {
|
|
49
|
+
// A default or catch is a whole value, which a picked part no longer fits, so it gives way to what it wraps.
|
|
50
|
+
return refineSchemaField(fieldSchema.def.innerType, fieldShape);
|
|
51
|
+
}
|
|
52
|
+
else if (isZodLazy(fieldSchema)) {
|
|
53
|
+
return refineSchemaField(fieldSchema.def.getter(), fieldShape);
|
|
54
54
|
}
|
|
55
55
|
return fieldSchema;
|
|
56
56
|
}
|
package/dist/utilities.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ZodArray, ZodLazy, ZodNullable, ZodObject, ZodOptional, ZodRecord, ZodType, ZodUnion, ZodUnknown } from 'zod';
|
|
1
|
+
import { ZodArray, ZodCatch, ZodDefault, ZodLazy, ZodNullable, ZodObject, ZodOptional, ZodRecord, ZodType, ZodUnion, ZodUnknown } from 'zod';
|
|
2
2
|
export declare function getInnerType(schema: ZodType, unwrapLazy?: boolean): ZodType;
|
|
3
3
|
export declare function isZodArray(obj: unknown): obj is ZodArray<ZodType>;
|
|
4
|
+
export declare function isZodCatch(obj: unknown): obj is ZodCatch<ZodType>;
|
|
5
|
+
export declare function isZodDefault(obj: unknown): obj is ZodDefault<ZodType>;
|
|
4
6
|
export declare function isZodLazy(obj: unknown): obj is ZodLazy;
|
|
5
7
|
export declare function isZodNullable(obj: unknown): obj is ZodNullable<ZodType>;
|
|
6
8
|
export declare function isZodObject(obj: unknown, isLoose?: boolean): obj is ZodObject;
|
package/dist/utilities.js
CHANGED
|
@@ -17,6 +17,12 @@ export function getInnerType(schema, unwrapLazy = true) {
|
|
|
17
17
|
export function isZodArray(obj) {
|
|
18
18
|
return isZodType(obj) && (obj.def.type === 'array');
|
|
19
19
|
}
|
|
20
|
+
export function isZodCatch(obj) {
|
|
21
|
+
return isZodType(obj) && (obj.def.type === 'catch');
|
|
22
|
+
}
|
|
23
|
+
export function isZodDefault(obj) {
|
|
24
|
+
return isZodType(obj) && (obj.def.type === 'default');
|
|
25
|
+
}
|
|
20
26
|
export function isZodLazy(obj) {
|
|
21
27
|
return isZodType(obj) && (obj.def.type === 'lazy');
|
|
22
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodselect",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Type-safe selection utility built on top of Zod. Allows for strongly typed field selection, schema merging, and runtime refinement for frontend or backend use.",
|
|
5
5
|
"author": "SecureDataSystems",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@temelyos/
|
|
28
|
+
"@temelyos/oxlint-config": "^1.1.0",
|
|
29
29
|
"@temelyos/tsconfig": "^1.0.6",
|
|
30
30
|
"@types/node": "^26.1.1",
|
|
31
|
+
"oxlint": "^1.88.0",
|
|
32
|
+
"oxlint-tsgolint": "^7.0.2004",
|
|
31
33
|
"tsx": "^4.23.1",
|
|
32
|
-
"typescript": "
|
|
33
|
-
"typescript-native": "npm:typescript@^7.0.2"
|
|
34
|
+
"typescript": "^7.0.2"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"zod": "^4.4.3"
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"build": "node scripts/build.mjs && tsc --project tsconfig.build.json",
|
|
40
41
|
"clean": "node scripts/clean.mjs",
|
|
41
42
|
"deploy": "npm run build && node ./scripts/publish.mjs",
|
|
42
|
-
"lint": "
|
|
43
|
-
"lint:fix": "
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"lint:fix": "temelyos-oxlint-fix",
|
|
44
45
|
"prune": "node scripts/prune.mjs",
|
|
45
46
|
"test": "tsx --tsconfig ./tsconfig.build.json --test ./src/**/*.{spec,test}.ts ./test/**/*.{spec,test}.ts"
|
|
46
47
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { buildSelectObject } from './build-select-object.js';
|
|
5
|
-
describe('.buildSelectObject()', () => {
|
|
6
|
-
it('should return all keys set to true for a flat object', () => {
|
|
7
|
-
const schema = z.object({
|
|
8
|
-
age: z.number(),
|
|
9
|
-
name: z.string()
|
|
10
|
-
});
|
|
11
|
-
assert.deepStrictEqual(buildSelectObject(schema), { age: true, name: true });
|
|
12
|
-
});
|
|
13
|
-
it('should merge keys from all union members', () => {
|
|
14
|
-
const schema = z.union([
|
|
15
|
-
z.object({ type: z.literal('a'), valueA: z.string() }),
|
|
16
|
-
z.object({ type: z.literal('b'), valueB: z.number() })
|
|
17
|
-
]);
|
|
18
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
19
|
-
type: true,
|
|
20
|
-
valueA: true,
|
|
21
|
-
valueB: true
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
it('should merge keys from an intersection', () => {
|
|
25
|
-
const schema = z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() }));
|
|
26
|
-
assert.deepStrictEqual(buildSelectObject(schema), { a: true, b: true });
|
|
27
|
-
});
|
|
28
|
-
it('should unwrap optional/nullable wrappers', () => {
|
|
29
|
-
const inner = z.object({ id: z.string(), value: z.number() });
|
|
30
|
-
const schema = inner.optional();
|
|
31
|
-
assert.deepStrictEqual(buildSelectObject(schema), { id: true, value: true });
|
|
32
|
-
});
|
|
33
|
-
it('should unwrap lazy schemas', () => {
|
|
34
|
-
const schema = z.lazy(() => z.object({ bar: z.number(), foo: z.string() }));
|
|
35
|
-
assert.deepStrictEqual(buildSelectObject(schema), { bar: true, foo: true });
|
|
36
|
-
});
|
|
37
|
-
it('should handle nested unions inside intersection', () => {
|
|
38
|
-
const schema = z.intersection(z.union([
|
|
39
|
-
z.object({ a: z.string() }),
|
|
40
|
-
z.object({ b: z.number() })
|
|
41
|
-
]), z.object({ c: z.boolean() }));
|
|
42
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
43
|
-
a: true,
|
|
44
|
-
b: true,
|
|
45
|
-
c: true
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
it('should recursively expand nested objects', () => {
|
|
49
|
-
const schema = z.object({
|
|
50
|
-
active: z.boolean(),
|
|
51
|
-
user: z.object({
|
|
52
|
-
age: z.number(),
|
|
53
|
-
name: z.string()
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
57
|
-
active: true,
|
|
58
|
-
user: { age: true, name: true }
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
it('should recursively expand deeply nested objects', () => {
|
|
62
|
-
const schema = z.object({
|
|
63
|
-
name: z.string(),
|
|
64
|
-
organization: z.object({
|
|
65
|
-
_id: z.string(),
|
|
66
|
-
address: z.object({
|
|
67
|
-
city: z.string(),
|
|
68
|
-
country: z.string()
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
73
|
-
name: true,
|
|
74
|
-
organization: { _id: true, address: { city: true, country: true } }
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
it('should expand optional nested objects', () => {
|
|
78
|
-
const schema = z.object({
|
|
79
|
-
name: z.string(),
|
|
80
|
-
organization: z.object({
|
|
81
|
-
_id: z.string(),
|
|
82
|
-
name: z.string()
|
|
83
|
-
}).optional()
|
|
84
|
-
});
|
|
85
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
86
|
-
name: true,
|
|
87
|
-
organization: { _id: true, name: true }
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
it('should not expand arrays', () => {
|
|
91
|
-
const schema = z.object({
|
|
92
|
-
name: z.string(),
|
|
93
|
-
tags: z.array(z.string())
|
|
94
|
-
});
|
|
95
|
-
assert.deepStrictEqual(buildSelectObject(schema), { name: true, tags: true });
|
|
96
|
-
});
|
|
97
|
-
it('should expand the items of an array of objects', () => {
|
|
98
|
-
const schema = z.object({
|
|
99
|
-
items: z.array(z.object({ id: z.string(), value: z.number() })),
|
|
100
|
-
name: z.string()
|
|
101
|
-
});
|
|
102
|
-
const select = buildSelectObject(schema);
|
|
103
|
-
const isTyped = true;
|
|
104
|
-
assert.ok(isTyped);
|
|
105
|
-
assert.deepStrictEqual(select, { items: { id: true, value: true }, name: true });
|
|
106
|
-
});
|
|
107
|
-
it('should expand an optional array of optional objects', () => {
|
|
108
|
-
const schema = z.object({
|
|
109
|
-
items: z.array(z.object({ id: z.string() }).optional()).optional()
|
|
110
|
-
});
|
|
111
|
-
const select = buildSelectObject(schema);
|
|
112
|
-
const isTyped = true;
|
|
113
|
-
assert.ok(isTyped);
|
|
114
|
-
assert.deepStrictEqual(select, { items: { id: true } });
|
|
115
|
-
});
|
|
116
|
-
it('should expand arrays of objects inside arrays of objects', () => {
|
|
117
|
-
const schema = z.object({
|
|
118
|
-
attachments: z.array(z.object({
|
|
119
|
-
history: z.array(z.object({ note: z.string(), uploadedBy: z.object({ _id: z.string(), username: z.string() }) })),
|
|
120
|
-
title: z.string()
|
|
121
|
-
}))
|
|
122
|
-
});
|
|
123
|
-
const select = buildSelectObject(schema);
|
|
124
|
-
const isTyped = true;
|
|
125
|
-
assert.ok(isTyped);
|
|
126
|
-
assert.deepStrictEqual(select, {
|
|
127
|
-
attachments: { history: { note: true, uploadedBy: { _id: true, username: true } }, title: true }
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
it('should type nested objects as nested selects', () => {
|
|
131
|
-
const schema = z.object({
|
|
132
|
-
client: z.object({ _id: z.string(), name: z.string() }).optional(),
|
|
133
|
-
title: z.string()
|
|
134
|
-
});
|
|
135
|
-
const select = buildSelectObject(schema);
|
|
136
|
-
const isTyped = true;
|
|
137
|
-
assert.ok(isTyped);
|
|
138
|
-
assert.deepStrictEqual(select, { client: { _id: true, name: true }, title: true });
|
|
139
|
-
});
|
|
140
|
-
it('should not expand records, or arrays of anything but objects', () => {
|
|
141
|
-
const schema = z.object({
|
|
142
|
-
byKey: z.record(z.string(), z.object({ id: z.string() })),
|
|
143
|
-
matrix: z.array(z.array(z.number())),
|
|
144
|
-
tags: z.array(z.string())
|
|
145
|
-
});
|
|
146
|
-
const select = buildSelectObject(schema);
|
|
147
|
-
const isTyped = true;
|
|
148
|
-
assert.ok(isTyped);
|
|
149
|
-
assert.deepStrictEqual(select, { byKey: true, matrix: true, tags: true });
|
|
150
|
-
});
|
|
151
|
-
it('should select a union field whole when a variant is not an object', () => {
|
|
152
|
-
const schema = z.object({
|
|
153
|
-
file: z.union([z.object({ _id: z.string(), filename: z.string() }), z.string()]),
|
|
154
|
-
name: z.union([z.string(), z.object({ first: z.string(), last: z.string() })])
|
|
155
|
-
});
|
|
156
|
-
const select = buildSelectObject(schema);
|
|
157
|
-
const isTyped = true;
|
|
158
|
-
assert.ok(isTyped);
|
|
159
|
-
assert.deepStrictEqual(select, { file: true, name: true });
|
|
160
|
-
});
|
|
161
|
-
it('should merge the keys of every object variant of a union field', () => {
|
|
162
|
-
const schema = z.object({
|
|
163
|
-
party: z.union([z.object({ name: z.string(), type: z.literal('company') }), z.object({ first: z.string(), type: z.literal('person') })]).optional()
|
|
164
|
-
});
|
|
165
|
-
const select = buildSelectObject(schema);
|
|
166
|
-
const isTyped = true;
|
|
167
|
-
assert.ok(isTyped);
|
|
168
|
-
assert.deepStrictEqual(select, { party: { first: true, name: true, type: true } });
|
|
169
|
-
});
|
|
170
|
-
it('should not expand a union field with no object variant', () => {
|
|
171
|
-
const schema = z.object({ value: z.union([z.string(), z.number()]).nullable() });
|
|
172
|
-
const select = buildSelectObject(schema);
|
|
173
|
-
const isTyped = true;
|
|
174
|
-
assert.ok(isTyped);
|
|
175
|
-
assert.deepStrictEqual(select, { value: true });
|
|
176
|
-
});
|
|
177
|
-
it('should expand an array of a union of objects', () => {
|
|
178
|
-
const schema = z.object({
|
|
179
|
-
options: z.array(z.union([z.object({ a: z.string() }), z.object({ b: z.string() })]))
|
|
180
|
-
});
|
|
181
|
-
const select = buildSelectObject(schema);
|
|
182
|
-
const isTyped = true;
|
|
183
|
-
assert.ok(isTyped);
|
|
184
|
-
assert.deepStrictEqual(select, { options: { a: true, b: true } });
|
|
185
|
-
});
|
|
186
|
-
it('should be a select the schema accepts', () => {
|
|
187
|
-
const schema = z.object({
|
|
188
|
-
client: z.object({ _id: z.string(), name: z.string() }),
|
|
189
|
-
items: z.array(z.object({ id: z.string() }))
|
|
190
|
-
});
|
|
191
|
-
const select = buildSelectObject(schema);
|
|
192
|
-
assert.deepStrictEqual(select, { client: { _id: true, name: true }, items: { id: true } });
|
|
193
|
-
});
|
|
194
|
-
it('should handle recursive schemas without infinite loop', () => {
|
|
195
|
-
;
|
|
196
|
-
const node = z.lazy(() => z.object({
|
|
197
|
-
children: z.array(node).optional(),
|
|
198
|
-
name: z.string(),
|
|
199
|
-
parent: node.optional()
|
|
200
|
-
}));
|
|
201
|
-
const schema = z.object({
|
|
202
|
-
name: z.string(),
|
|
203
|
-
root: node
|
|
204
|
-
});
|
|
205
|
-
// Should not throw or hang — recursive objects hit depth limit
|
|
206
|
-
const result = buildSelectObject(schema);
|
|
207
|
-
assert.strictEqual(result.name, true);
|
|
208
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
209
|
-
assert.ok(typeof result.root === 'object' && result.root !== null);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { z, ZodObject } from 'zod';
|
|
4
|
-
import { buildSelectSchema } from './build-select-schema.js';
|
|
5
|
-
import { getInnerType, isZodObjectLoose } from './utilities.js';
|
|
6
|
-
describe('.buildSelectSchema()', () => {
|
|
7
|
-
it('should build select schema for a flat object', () => {
|
|
8
|
-
const schema = z.object({
|
|
9
|
-
age: z.number(),
|
|
10
|
-
name: z.string()
|
|
11
|
-
});
|
|
12
|
-
const selectSchema = buildSelectSchema(schema);
|
|
13
|
-
assert(selectSchema instanceof ZodObject);
|
|
14
|
-
assert.deepEqual(selectSchema.parse({ name: true }), { name: true });
|
|
15
|
-
assert.deepEqual(selectSchema.parse({}), {});
|
|
16
|
-
assert.throws(() => selectSchema.parse({ name: 'yes' }), /expected boolean/);
|
|
17
|
-
assert.throws(() => selectSchema.parse({ unknown: true }), /Unrecognized key/);
|
|
18
|
-
});
|
|
19
|
-
it('should handle nested objects', () => {
|
|
20
|
-
const schema = z.object({
|
|
21
|
-
user: z.object({
|
|
22
|
-
id: z.string(),
|
|
23
|
-
profile: z.object({
|
|
24
|
-
displayName: z.string()
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
});
|
|
28
|
-
const selectSchema = buildSelectSchema(schema);
|
|
29
|
-
const result = selectSchema.parse({
|
|
30
|
-
user: {
|
|
31
|
-
id: true,
|
|
32
|
-
profile: {
|
|
33
|
-
displayName: true
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
assert.deepEqual(result, {
|
|
38
|
-
user: {
|
|
39
|
-
id: true,
|
|
40
|
-
profile: {
|
|
41
|
-
displayName: true
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
it('should build select schema for a union of objects', () => {
|
|
47
|
-
const schema = z.union([
|
|
48
|
-
z.object({ type: z.literal('a'), valueA: z.string() }).strict(),
|
|
49
|
-
z.object({ type: z.literal('b'), valueB: z.number() }).strict()
|
|
50
|
-
]);
|
|
51
|
-
const selectSchema = buildSelectSchema(schema);
|
|
52
|
-
assert(selectSchema instanceof ZodObject);
|
|
53
|
-
const result = selectSchema.parse({
|
|
54
|
-
type: true,
|
|
55
|
-
valueA: true
|
|
56
|
-
});
|
|
57
|
-
assert.deepEqual(result, {
|
|
58
|
-
type: true,
|
|
59
|
-
valueA: true
|
|
60
|
-
});
|
|
61
|
-
assert.throws(() => selectSchema.parse({ valueC: true }), /Unrecognized key/);
|
|
62
|
-
});
|
|
63
|
-
it('z.infer of a union select schema should be a plain select type', () => {
|
|
64
|
-
const schema = z.union([
|
|
65
|
-
z.object({ shared: z.string(), type: z.literal('a'), valueA: z.string() }).strict(),
|
|
66
|
-
z.object({ shared: z.string(), type: z.literal('b'), valueB: z.number() }).strict()
|
|
67
|
-
]);
|
|
68
|
-
const selectSchema = buildSelectSchema(schema);
|
|
69
|
-
// Runtime: the schema correctly parses a plain select object
|
|
70
|
-
assert.deepEqual(selectSchema.parse({ shared: true, type: true, valueA: true }), { shared: true, type: true, valueA: true });
|
|
71
|
-
const plain = {};
|
|
72
|
-
void plain;
|
|
73
|
-
assert.ok(true);
|
|
74
|
-
});
|
|
75
|
-
it('should support intersections of objects', () => {
|
|
76
|
-
const schema = z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() }));
|
|
77
|
-
const select = buildSelectSchema(schema);
|
|
78
|
-
assert.strictEqual(getInnerType(select.shape.a).def.type, 'boolean');
|
|
79
|
-
assert.strictEqual(getInnerType(select.shape.b).def.type, 'boolean');
|
|
80
|
-
});
|
|
81
|
-
it('should support tuples of objects', () => {
|
|
82
|
-
const schema = z.tuple([
|
|
83
|
-
z.object({ foo: z.string() }),
|
|
84
|
-
z.object({ bar: z.number() })
|
|
85
|
-
]);
|
|
86
|
-
const select = buildSelectSchema(schema);
|
|
87
|
-
assert.strictEqual(getInnerType(select.shape.foo).def.type, 'boolean');
|
|
88
|
-
assert.strictEqual(getInnerType(select.shape.bar).def.type, 'boolean');
|
|
89
|
-
});
|
|
90
|
-
it('should return a strict schema if any member is strict', () => {
|
|
91
|
-
const schema = z.union([
|
|
92
|
-
z.object({ strictA: z.string() }).strict(),
|
|
93
|
-
z.object({ strictB: z.number() }).strict()
|
|
94
|
-
]);
|
|
95
|
-
const select = buildSelectSchema(schema);
|
|
96
|
-
assert.strictEqual(isZodObjectLoose(select), false);
|
|
97
|
-
});
|
|
98
|
-
it('should return a strict schema when only one union member is strict', () => {
|
|
99
|
-
const schema = z.union([
|
|
100
|
-
z.object({ strictA: z.string() }).strict(),
|
|
101
|
-
z.looseObject({ looseB: z.number() })
|
|
102
|
-
]);
|
|
103
|
-
const select = buildSelectSchema(schema);
|
|
104
|
-
// Any strict member makes the merged select strict so callers cannot
|
|
105
|
-
// accidentally reference fields outside any member's defined shape.
|
|
106
|
-
assert.strictEqual(isZodObjectLoose(select), false);
|
|
107
|
-
});
|
|
108
|
-
it('should allow optional keys in the select schema', () => {
|
|
109
|
-
const schema = z.object({
|
|
110
|
-
optionalField: z.string().optional()
|
|
111
|
-
});
|
|
112
|
-
const select = buildSelectSchema(schema);
|
|
113
|
-
assert.strictEqual(getInnerType(select.shape.optionalField).def.type, 'boolean');
|
|
114
|
-
});
|
|
115
|
-
it('should handle direct recursion', () => {
|
|
116
|
-
const roleSchema = z.lazy(() => {
|
|
117
|
-
return (z.object({
|
|
118
|
-
name: z.string(),
|
|
119
|
-
roles: z.array(roleSchema)
|
|
120
|
-
}));
|
|
121
|
-
});
|
|
122
|
-
const select = buildSelectSchema(roleSchema);
|
|
123
|
-
assert.deepStrictEqual(Object.keys(select.shape), ['name', 'roles']);
|
|
124
|
-
const nameSelect = select.shape.name;
|
|
125
|
-
assert.strictEqual(nameSelect.def.type, 'optional');
|
|
126
|
-
assert.strictEqual(nameSelect.def.innerType.def.type, 'boolean');
|
|
127
|
-
const rolesSelect = select.shape.roles;
|
|
128
|
-
assert.strictEqual(rolesSelect.def.type, 'lazy');
|
|
129
|
-
const innerUnion = rolesSelect.def.getter().def.innerType;
|
|
130
|
-
assert.strictEqual(innerUnion.def.type, 'union');
|
|
131
|
-
const booleanItem = innerUnion.def.options[0];
|
|
132
|
-
assert.strictEqual(booleanItem.def.type, 'boolean');
|
|
133
|
-
const roleItem = innerUnion.def.options[1];
|
|
134
|
-
assert.strictEqual(roleItem.def.type, 'object');
|
|
135
|
-
});
|
|
136
|
-
it('should support mutual recursion (cross-recursive)', () => {
|
|
137
|
-
const schemaA = z.lazy(() => z.object({
|
|
138
|
-
b: schemaB
|
|
139
|
-
}));
|
|
140
|
-
const schemaB = z.lazy(() => z.object({
|
|
141
|
-
a: schemaA
|
|
142
|
-
}));
|
|
143
|
-
const select = buildSelectSchema(schemaA);
|
|
144
|
-
assert.strictEqual(select.shape.b.def.type, 'lazy');
|
|
145
|
-
const bUnion = select.shape.b.def.getter();
|
|
146
|
-
assert.strictEqual(bUnion.def.innerType.def.type, 'union');
|
|
147
|
-
const bObj = bUnion.def.innerType.def.options[1];
|
|
148
|
-
assert.strictEqual(bObj.shape.a.def.type, 'lazy');
|
|
149
|
-
});
|
|
150
|
-
it('should share the same instance in recursion (no infinite loop)', () => {
|
|
151
|
-
const role = z.lazy(() => z.object({
|
|
152
|
-
name: z.string(),
|
|
153
|
-
roles: z.array(role)
|
|
154
|
-
}));
|
|
155
|
-
const select = buildSelectSchema(role);
|
|
156
|
-
const rolesSelect = select.shape.roles;
|
|
157
|
-
const unionSelect = rolesSelect.def.getter().def.innerType;
|
|
158
|
-
const nestedRoleSelect = unionSelect.def.options[1].shape.roles;
|
|
159
|
-
// Should be the same object (by reference)
|
|
160
|
-
assert.strictEqual(rolesSelect, nestedRoleSelect);
|
|
161
|
-
});
|
|
162
|
-
it('should support record types', () => {
|
|
163
|
-
const schema = z.object({
|
|
164
|
-
tags: z.record(z.string(), z.string())
|
|
165
|
-
});
|
|
166
|
-
const select = buildSelectSchema(schema);
|
|
167
|
-
const tagsSelect = select.shape.tags;
|
|
168
|
-
assert.strictEqual(tagsSelect.type, 'optional');
|
|
169
|
-
const union = tagsSelect.def.innerType;
|
|
170
|
-
assert.strictEqual(union.type, 'union');
|
|
171
|
-
assert.strictEqual(union.def.options[0].type, 'boolean');
|
|
172
|
-
assert.strictEqual(union.def.options[1].type, 'record');
|
|
173
|
-
assert.strictEqual(union.def.options[1].def.keyType.type, 'string');
|
|
174
|
-
assert.strictEqual(union.def.options[1].def.valueType.type, 'optional');
|
|
175
|
-
assert.strictEqual(union.def.options[1].def.valueType.def.innerType.type, 'boolean');
|
|
176
|
-
});
|
|
177
|
-
it('should share the same lazy instance for a recursive schema referenced by multiple union members', () => {
|
|
178
|
-
const nested = z.lazy(() => z.object({ children: z.array(nested) }));
|
|
179
|
-
const schema = z.union([
|
|
180
|
-
z.object({ a: nested }),
|
|
181
|
-
z.object({ b: nested })
|
|
182
|
-
]);
|
|
183
|
-
const select = buildSelectSchema(schema);
|
|
184
|
-
// Both 'a' and 'b' reference the same `nested` schema.
|
|
185
|
-
// Their select schemas must be the same lazy instance (shared cache).
|
|
186
|
-
const aSelect = select.shape.a;
|
|
187
|
-
const bSelect = select.shape.b;
|
|
188
|
-
assert.strictEqual(aSelect, bSelect);
|
|
189
|
-
});
|
|
190
|
-
it('should support unknown types', () => {
|
|
191
|
-
const schema = z.object({
|
|
192
|
-
tags: z.unknown()
|
|
193
|
-
});
|
|
194
|
-
const select = buildSelectSchema(schema);
|
|
195
|
-
const result = select.safeParse({
|
|
196
|
-
tags: {
|
|
197
|
-
any: {
|
|
198
|
-
damn: true
|
|
199
|
-
},
|
|
200
|
-
thing: true
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
assert.strictEqual(result.success, true);
|
|
204
|
-
assert.deepStrictEqual(result.data, {
|
|
205
|
-
tags: {
|
|
206
|
-
any: {
|
|
207
|
-
damn: true
|
|
208
|
-
},
|
|
209
|
-
thing: true
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
it('should allow nested object sub-selection in union members', () => {
|
|
214
|
-
const account = z.object({
|
|
215
|
-
_id: z.string(),
|
|
216
|
-
name: z.string()
|
|
217
|
-
});
|
|
218
|
-
const individual = z.object({
|
|
219
|
-
account: account.optional(),
|
|
220
|
-
name: z.object({ first: z.string(), last: z.string() }),
|
|
221
|
-
type: z.literal('individual')
|
|
222
|
-
});
|
|
223
|
-
const organization = z.object({
|
|
224
|
-
account: account.optional(),
|
|
225
|
-
name: z.string(),
|
|
226
|
-
type: z.literal('organization')
|
|
227
|
-
});
|
|
228
|
-
const contact = z.union([individual, organization]);
|
|
229
|
-
const selectSchema = buildSelectSchema(contact);
|
|
230
|
-
// Runtime: nested sub-selection on a shared nested object field should parse
|
|
231
|
-
const result = selectSchema.parse({
|
|
232
|
-
account: { _id: true, name: true },
|
|
233
|
-
type: true
|
|
234
|
-
});
|
|
235
|
-
assert.deepStrictEqual(result, {
|
|
236
|
-
account: { _id: true, name: true },
|
|
237
|
-
type: true
|
|
238
|
-
});
|
|
239
|
-
const nested = { account: { _id: true, name: true }, type: true };
|
|
240
|
-
void nested;
|
|
241
|
-
});
|
|
242
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import { strict as assert, deepStrictEqual, throws } from 'assert';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { z, ZodError } from 'zod';
|
|
4
|
-
import { refineSchema } from './refine-schema.js';
|
|
5
|
-
describe('.refineSchema()', () => {
|
|
6
|
-
it('should retain required fields', () => {
|
|
7
|
-
const userSchema = z.object({
|
|
8
|
-
age: z.number().optional(),
|
|
9
|
-
firstName: z.string(),
|
|
10
|
-
lastName: z.string()
|
|
11
|
-
});
|
|
12
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
13
|
-
firstName: true,
|
|
14
|
-
lastName: true
|
|
15
|
-
});
|
|
16
|
-
const parsedData = refinedSchema.parse({
|
|
17
|
-
firstName: 'John',
|
|
18
|
-
lastName: 'Doe'
|
|
19
|
-
});
|
|
20
|
-
assert.equal(parsedData.firstName, 'John');
|
|
21
|
-
assert.equal(parsedData.lastName, 'Doe');
|
|
22
|
-
});
|
|
23
|
-
it('should handle optional fields', () => {
|
|
24
|
-
const userSchema = z.object({
|
|
25
|
-
age: z.number().optional(),
|
|
26
|
-
firstName: z.string(),
|
|
27
|
-
lastName: z.string()
|
|
28
|
-
});
|
|
29
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
30
|
-
age: true
|
|
31
|
-
});
|
|
32
|
-
const parsedData = refinedSchema.parse({ age: 25 });
|
|
33
|
-
assert.equal(parsedData.age, 25);
|
|
34
|
-
const parsedWithoutAge = refinedSchema.parse({});
|
|
35
|
-
assert.equal(parsedWithoutAge.age, undefined);
|
|
36
|
-
});
|
|
37
|
-
it('should handle nullable fields', () => {
|
|
38
|
-
const roleSchema = z.object({
|
|
39
|
-
name: z.string()
|
|
40
|
-
});
|
|
41
|
-
const userSchema = z.object({
|
|
42
|
-
firstName: z.string(),
|
|
43
|
-
role: roleSchema.nullable()
|
|
44
|
-
});
|
|
45
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
46
|
-
role: true
|
|
47
|
-
});
|
|
48
|
-
const parsedWithNullRole = refinedSchema.parse({
|
|
49
|
-
firstName: 'Alice',
|
|
50
|
-
role: null
|
|
51
|
-
});
|
|
52
|
-
assert.equal(parsedWithNullRole.role, null);
|
|
53
|
-
});
|
|
54
|
-
it('should handle both optional and nullable fields', () => {
|
|
55
|
-
const roleSchema = z.object({
|
|
56
|
-
name: z.string()
|
|
57
|
-
});
|
|
58
|
-
const userSchema = z.object({
|
|
59
|
-
firstName: z.string(),
|
|
60
|
-
role: roleSchema.nullable().optional()
|
|
61
|
-
});
|
|
62
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
63
|
-
role: {
|
|
64
|
-
name: true
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
const parsedWithRole = refinedSchema.parse({
|
|
68
|
-
firstName: 'Alice',
|
|
69
|
-
role: { name: 'Admin' }
|
|
70
|
-
});
|
|
71
|
-
assert.deepStrictEqual(parsedWithRole.role, { name: 'Admin' });
|
|
72
|
-
const parsedWithNullRole = refinedSchema.parse({
|
|
73
|
-
firstName: 'Alice',
|
|
74
|
-
role: null
|
|
75
|
-
});
|
|
76
|
-
assert.strictEqual(parsedWithNullRole.role, null);
|
|
77
|
-
const parsedWithoutRole = refinedSchema.parse({ firstName: 'Alice' });
|
|
78
|
-
assert.strictEqual(parsedWithoutRole.role, undefined);
|
|
79
|
-
});
|
|
80
|
-
it('should apply custom schema transformations', () => {
|
|
81
|
-
const roleSchema = z.object({
|
|
82
|
-
name: z.string()
|
|
83
|
-
});
|
|
84
|
-
const userSchema = z.object({
|
|
85
|
-
firstName: z.string(),
|
|
86
|
-
role: roleSchema
|
|
87
|
-
});
|
|
88
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
89
|
-
role: schema => schema.extend({ permissions: z.array(z.string()) })
|
|
90
|
-
});
|
|
91
|
-
const parsedData = refinedSchema.parse({
|
|
92
|
-
firstName: 'Bob',
|
|
93
|
-
role: { name: 'User', permissions: ['read', 'write'] }
|
|
94
|
-
});
|
|
95
|
-
assert.deepEqual(parsedData.role, {
|
|
96
|
-
name: 'User',
|
|
97
|
-
permissions: ['read', 'write']
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
it('should handle nested arrays', () => {
|
|
101
|
-
const roleSchema = z.object({
|
|
102
|
-
level: z.number(),
|
|
103
|
-
title: z.string()
|
|
104
|
-
});
|
|
105
|
-
const userSchema = z.object({
|
|
106
|
-
age: z.number().optional(),
|
|
107
|
-
firstName: z.string(),
|
|
108
|
-
lastName: z.string(),
|
|
109
|
-
nestedArray: z.array(z.object({ name: z.string(), value: z.number() })).optional().nullable(),
|
|
110
|
-
role: roleSchema.nullable().optional(),
|
|
111
|
-
tags: z.array(z.string()).optional()
|
|
112
|
-
});
|
|
113
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
114
|
-
nestedArray: {
|
|
115
|
-
name: true
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
const parsedData = refinedSchema.parse({
|
|
119
|
-
firstName: 'Alice',
|
|
120
|
-
lastName: 'Smith',
|
|
121
|
-
nestedArray: [{ name: 'Feature1', value: 10 }, { name: 'Feature2', value: 20 }]
|
|
122
|
-
});
|
|
123
|
-
assert.deepStrictEqual(parsedData.nestedArray, [{ name: 'Feature1' }, { name: 'Feature2' }]);
|
|
124
|
-
// Test optional array
|
|
125
|
-
const parsedWithoutNestedArray = refinedSchema.parse({
|
|
126
|
-
firstName: 'Alice',
|
|
127
|
-
lastName: 'Smith'
|
|
128
|
-
});
|
|
129
|
-
assert.equal(parsedWithoutNestedArray.nestedArray, undefined);
|
|
130
|
-
});
|
|
131
|
-
it('should handle custom fields', () => {
|
|
132
|
-
const userSchema = z.object({
|
|
133
|
-
address: z.custom().optional(),
|
|
134
|
-
firstName: z.string(),
|
|
135
|
-
lastName: z.string()
|
|
136
|
-
});
|
|
137
|
-
const refinedSchema = refineSchema(userSchema, {
|
|
138
|
-
address: true
|
|
139
|
-
});
|
|
140
|
-
const parsedData = refinedSchema.parse({
|
|
141
|
-
address: {
|
|
142
|
-
city: 'Austin',
|
|
143
|
-
state: 'TX',
|
|
144
|
-
street: '123 Main Street'
|
|
145
|
-
},
|
|
146
|
-
firstName: 'Alice',
|
|
147
|
-
lastName: 'Smith'
|
|
148
|
-
});
|
|
149
|
-
assert.deepStrictEqual(parsedData, {
|
|
150
|
-
address: {
|
|
151
|
-
city: 'Austin',
|
|
152
|
-
state: 'TX',
|
|
153
|
-
street: '123 Main Street'
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
it('should support unions of objects', () => {
|
|
158
|
-
const person = z.object({
|
|
159
|
-
firstName: z.string(),
|
|
160
|
-
lastName: z.string()
|
|
161
|
-
});
|
|
162
|
-
const address = z.object({
|
|
163
|
-
address: z.string(),
|
|
164
|
-
city: z.string()
|
|
165
|
-
});
|
|
166
|
-
const schema = refineSchema(z.union([person, address]), {
|
|
167
|
-
address: true,
|
|
168
|
-
firstName: true
|
|
169
|
-
});
|
|
170
|
-
let parseResult = schema.parse({
|
|
171
|
-
address: '123 Main Street',
|
|
172
|
-
firstName: 'Jerry',
|
|
173
|
-
lastName: 'Springer'
|
|
174
|
-
});
|
|
175
|
-
deepStrictEqual(parseResult, { firstName: 'Jerry' });
|
|
176
|
-
parseResult = schema.parse({
|
|
177
|
-
address: '123 Main Street',
|
|
178
|
-
city: 'Austin'
|
|
179
|
-
});
|
|
180
|
-
deepStrictEqual(parseResult, { address: '123 Main Street' });
|
|
181
|
-
});
|
|
182
|
-
it('should error if none of the types are matched', () => {
|
|
183
|
-
const person = z.object({
|
|
184
|
-
firstName: z.string(),
|
|
185
|
-
lastName: z.string()
|
|
186
|
-
});
|
|
187
|
-
const address = z.object({
|
|
188
|
-
address: z.string(),
|
|
189
|
-
city: z.string()
|
|
190
|
-
});
|
|
191
|
-
const schema = refineSchema(z.union([person, address]), {
|
|
192
|
-
address: true,
|
|
193
|
-
firstName: true
|
|
194
|
-
});
|
|
195
|
-
throws(() => {
|
|
196
|
-
schema.parse({
|
|
197
|
-
lastName: 'Springer'
|
|
198
|
-
});
|
|
199
|
-
}, (err) => {
|
|
200
|
-
if (!(err instanceof ZodError) || err.issues[0].code !== 'invalid_union') {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
return true;
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
it('should exclude fields specified as false in the shape', () => {
|
|
207
|
-
const schema = z.object({
|
|
208
|
-
firstName: z.string(),
|
|
209
|
-
lastName: z.string()
|
|
210
|
-
});
|
|
211
|
-
const refined = refineSchema(schema, {
|
|
212
|
-
firstName: true,
|
|
213
|
-
lastName: false
|
|
214
|
-
});
|
|
215
|
-
// lastName was specified as false — it should be excluded from the refined schema
|
|
216
|
-
const result = refined.parse({ firstName: 'Alice', lastName: 'Smith' });
|
|
217
|
-
assert.deepStrictEqual(result, { firstName: 'Alice' });
|
|
218
|
-
});
|
|
219
|
-
it('should support unions of objects and simple types', () => {
|
|
220
|
-
const person = z.object({
|
|
221
|
-
firstName: z.string(),
|
|
222
|
-
lastName: z.string()
|
|
223
|
-
});
|
|
224
|
-
const schema = refineSchema(z.union([person, z.string()]), {
|
|
225
|
-
address: true,
|
|
226
|
-
firstName: true
|
|
227
|
-
});
|
|
228
|
-
let parseResult = schema.parse({
|
|
229
|
-
firstName: 'Jerry',
|
|
230
|
-
lastName: 'Springer'
|
|
231
|
-
});
|
|
232
|
-
deepStrictEqual(parseResult, { firstName: 'Jerry' });
|
|
233
|
-
parseResult = schema.parse('123 Main Street');
|
|
234
|
-
deepStrictEqual(parseResult, '123 Main Street');
|
|
235
|
-
});
|
|
236
|
-
it('should accept selecting a subfield of a union member with an optional array-of-objects field', () => {
|
|
237
|
-
// Regression test for a bug in `UnionFieldType` (refine-schema.ts).
|
|
238
|
-
//
|
|
239
|
-
// `UnionFieldType` chose a union member field's refine shape with
|
|
240
|
-
// `z.infer<T> extends Array<infer U>` but, unlike `RefineType`, it did not
|
|
241
|
-
// strip `| undefined` first. For an OPTIONAL array-of-objects member
|
|
242
|
-
// (`field: Obj.array().optional()`), `z.infer<T>` is `Obj[] | undefined`,
|
|
243
|
-
// which is NOT assignable to `Array<infer U>`, so the field's refine shape
|
|
244
|
-
// collapsed to `RefineObject<Obj[]>` (the array's own keys) instead of
|
|
245
|
-
// `RefineObject<Obj>`. The runtime was unaffected, so this only broke the
|
|
246
|
-
// type-check (`tsc`), not `tsx --test`.
|
|
247
|
-
const accessLevel = z.object({
|
|
248
|
-
_id: z.string(),
|
|
249
|
-
name: z.string()
|
|
250
|
-
});
|
|
251
|
-
const organizationInvitation = z.object({
|
|
252
|
-
accessLevels: accessLevel.array().optional(),
|
|
253
|
-
organizationId: z.string(),
|
|
254
|
-
type: z.literal('organization')
|
|
255
|
-
});
|
|
256
|
-
const platformInvitation = z.object({
|
|
257
|
-
accessLevels: accessLevel.array().optional(),
|
|
258
|
-
type: z.literal('platform')
|
|
259
|
-
});
|
|
260
|
-
const invitation = z.discriminatedUnion('type', [organizationInvitation, platformInvitation]);
|
|
261
|
-
// Type-level assertion: drilling into a subfield of the optional array
|
|
262
|
-
// member is a valid select and MUST be assignable to the refine shape.
|
|
263
|
-
// With the bug present, this `satisfies` fails to compile.
|
|
264
|
-
const select = {
|
|
265
|
-
accessLevels: { name: true },
|
|
266
|
-
type: true
|
|
267
|
-
};
|
|
268
|
-
const refined = refineSchema(invitation, select);
|
|
269
|
-
const parsed = refined.parse({
|
|
270
|
-
accessLevels: [{ name: 'Admin' }],
|
|
271
|
-
type: 'organization'
|
|
272
|
-
});
|
|
273
|
-
assert.equal(parsed.type, 'organization');
|
|
274
|
-
assert.deepEqual(parsed.accessLevels, [{ name: 'Admin' }]);
|
|
275
|
-
});
|
|
276
|
-
it('allows selecting a nested subfield of a declared looseObject field', () => {
|
|
277
|
-
// Regression test for `RefineObject` collapsing a `z.looseObject`'s declared fields.
|
|
278
|
-
//
|
|
279
|
-
// A `z.looseObject` carries a catchall index signature (`[k: string]: unknown`).
|
|
280
|
-
// `RefineObject` maps over `keyof T`, and when `T` has an index signature `keyof T`
|
|
281
|
-
// collapses to `string | number`, so every declared field's value type came from the
|
|
282
|
-
// catchall (`unknown` -> scalar `boolean`). Declared fields could be marked `true` but
|
|
283
|
-
// could NOT be drilled into. The runtime was unaffected, so this only broke the
|
|
284
|
-
// type-check (`tsc`), not `tsx --test`.
|
|
285
|
-
const personName = z.object({
|
|
286
|
-
first: z.string().optional(),
|
|
287
|
-
last: z.string().optional()
|
|
288
|
-
});
|
|
289
|
-
const user = z.looseObject({
|
|
290
|
-
_id: z.string(),
|
|
291
|
-
name: personName.optional(),
|
|
292
|
-
username: z.string()
|
|
293
|
-
});
|
|
294
|
-
const root = z.object({
|
|
295
|
-
invitedBy: user
|
|
296
|
-
});
|
|
297
|
-
// Type-level assertion: drilling into `invitedBy.name.first` is valid and MUST be
|
|
298
|
-
// assignable to the refine shape. With the bug present this `satisfies` fails to
|
|
299
|
-
// compile because `invitedBy`'s declared fields collapse to an index signature with
|
|
300
|
-
// `boolean` values.
|
|
301
|
-
const select = {
|
|
302
|
-
invitedBy: { name: { first: true } }
|
|
303
|
-
};
|
|
304
|
-
const refined = refineSchema(root, select);
|
|
305
|
-
const parsed = refined.parse({
|
|
306
|
-
invitedBy: { name: { first: 'Alice' } }
|
|
307
|
-
});
|
|
308
|
-
assert.equal(parsed.invitedBy.name?.first, 'Alice');
|
|
309
|
-
});
|
|
310
|
-
it('keeps every option of a small enum in the refined type (control)', () => {
|
|
311
|
-
const stage = z.enum(['one', 'two', 'three']);
|
|
312
|
-
const project = z.object({ name: z.string(), stage });
|
|
313
|
-
const refined = refineSchema(project, { stage: true });
|
|
314
|
-
// Type-level assertion: every option must remain assignable to the refined type.
|
|
315
|
-
const selected = [
|
|
316
|
-
{ stage: 'one' },
|
|
317
|
-
{ stage: 'two' },
|
|
318
|
-
{ stage: 'three' }
|
|
319
|
-
];
|
|
320
|
-
deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
|
|
321
|
-
});
|
|
322
|
-
it('keeps every option of an enum wider than nine options in the refined type', () => {
|
|
323
|
-
// Regression test for `TuplifyUnion` (refine-schema.ts) truncating wide enums.
|
|
324
|
-
//
|
|
325
|
-
// `TuplifyUnion` spends one recursion level per union member and stops at its
|
|
326
|
-
// depth cap, so an enum field refined with `true` silently dropped every option
|
|
327
|
-
// past nine from the refined TYPE — and which options vanished varied with
|
|
328
|
-
// TypeScript's internal union ordering. Fixed by the ZodLiteral shortcut in
|
|
329
|
-
// `Zodify`. The runtime was unaffected, so this only broke the type-check
|
|
330
|
-
// (`tsc`), not `tsx --test`.
|
|
331
|
-
const stage = z.enum([
|
|
332
|
-
'draft', 'interviewing', 'interview-complete',
|
|
333
|
-
'previewing', 'preview-review',
|
|
334
|
-
'plan-review', 'plan-approved', 'local',
|
|
335
|
-
'spec-review', 'spec-approved',
|
|
336
|
-
'implementing', 'module-ready'
|
|
337
|
-
]);
|
|
338
|
-
const project = z.object({ name: z.string(), stage });
|
|
339
|
-
const refined = refineSchema(project, { stage: true });
|
|
340
|
-
// Type-level assertion: each of the twelve options must remain assignable to
|
|
341
|
-
// the refined type. With the bug present, three of these assignments fail TS2322.
|
|
342
|
-
const selected = [
|
|
343
|
-
{ stage: 'draft' },
|
|
344
|
-
{ stage: 'interviewing' },
|
|
345
|
-
{ stage: 'interview-complete' },
|
|
346
|
-
{ stage: 'previewing' },
|
|
347
|
-
{ stage: 'preview-review' },
|
|
348
|
-
{ stage: 'plan-review' },
|
|
349
|
-
{ stage: 'plan-approved' },
|
|
350
|
-
{ stage: 'local' },
|
|
351
|
-
{ stage: 'spec-review' },
|
|
352
|
-
{ stage: 'spec-approved' },
|
|
353
|
-
{ stage: 'implementing' },
|
|
354
|
-
{ stage: 'module-ready' }
|
|
355
|
-
];
|
|
356
|
-
deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
describe('.refineSchema() literal and unknown field types', () => {
|
|
360
|
-
// Regression tests for `Zodify` (refine-schema.ts) when a schema's fields are listed
|
|
361
|
-
// (e.g. via `buildSelectObject`) instead of selected whole. Before the fix a numeric
|
|
362
|
-
// literal union widened to `number` and a required `z.unknown()` field became
|
|
363
|
-
// optional, so the selected type no longer fit the source type. Runtime was
|
|
364
|
-
// unaffected; these assignments fail the type-check (`tsc`).
|
|
365
|
-
const question = z.object({
|
|
366
|
-
answer: z.object({ value: z.unknown() }),
|
|
367
|
-
flag: z.literal(true),
|
|
368
|
-
note: z.unknown().optional(),
|
|
369
|
-
scale: z.union([z.literal(5), z.literal(10)]),
|
|
370
|
-
steps: z.literal([3, 7]),
|
|
371
|
-
value: z.unknown()
|
|
372
|
-
});
|
|
373
|
-
const survey = z.object({
|
|
374
|
-
name: z.string(),
|
|
375
|
-
pages: z.array(z.object({ questions: z.array(question) }))
|
|
376
|
-
});
|
|
377
|
-
it('keeps a numeric literal union instead of widening it to number', () => {
|
|
378
|
-
const refined = refineSchema(survey, { pages: { questions: { scale: true, steps: true } } });
|
|
379
|
-
// With the bug these resolve to `number`, so the source type can't take them.
|
|
380
|
-
const scale = {}.scale;
|
|
381
|
-
const steps = {}.steps;
|
|
382
|
-
void scale;
|
|
383
|
-
void steps;
|
|
384
|
-
const parsed = refined.parse({ pages: [{ questions: [{ scale: 10, steps: 3 }] }] });
|
|
385
|
-
assert.equal(parsed.pages[0]?.questions[0]?.scale, 10);
|
|
386
|
-
throws(() => refined.parse({ pages: [{ questions: [{ scale: 7, steps: 3 }] }] }), ZodError);
|
|
387
|
-
});
|
|
388
|
-
it('keeps a boolean literal instead of widening it to boolean', () => {
|
|
389
|
-
const refined = refineSchema(survey, { pages: { questions: { flag: true } } });
|
|
390
|
-
const isFlag = {}.flag;
|
|
391
|
-
void isFlag;
|
|
392
|
-
throws(() => refined.parse({ pages: [{ questions: [{ flag: false }] }] }), ZodError);
|
|
393
|
-
});
|
|
394
|
-
it('keeps a required unknown field required', () => {
|
|
395
|
-
const refined = refineSchema(survey, { pages: { questions: { answer: true, note: true, value: true } } });
|
|
396
|
-
// With the bug `value` became an optional key; `note` must stay optional.
|
|
397
|
-
const selected = { answer: { value: 1 }, value: 'x' };
|
|
398
|
-
const questions = [selected];
|
|
399
|
-
deepStrictEqual(refined.parse({ pages: [{ questions }] }).pages[0]?.questions, questions);
|
|
400
|
-
});
|
|
401
|
-
});
|
package/dist/select.spec.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/select.spec.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { strict as assert } from 'assert';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { buildSelectObject } from './build-select-object.js';
|
|
5
|
-
import { refineSchema } from './refine-schema.js';
|
|
6
|
-
const internalService = z.object({
|
|
7
|
-
name: z.string(),
|
|
8
|
-
type: z.literal('internal')
|
|
9
|
-
}).strict();
|
|
10
|
-
const externalService = z.object({
|
|
11
|
-
iconUrl: z.string().optional(),
|
|
12
|
-
name: z.string(),
|
|
13
|
-
type: z.literal('external')
|
|
14
|
-
}).strict();
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
16
|
-
const serviceType = z.union([internalService, externalService]);
|
|
17
|
-
describe('InferMergedType', () => {
|
|
18
|
-
it('should resolve to z.infer<T> when used in a generic function with TSelect = undefined', () => {
|
|
19
|
-
function getServices(options) {
|
|
20
|
-
void options;
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
// When called without select, TSelect defaults to undefined
|
|
24
|
-
// InferMergedType<typeof Service, undefined> should resolve to z.infer<typeof Service>
|
|
25
|
-
const services = getServices({});
|
|
26
|
-
const service = Object.values(services)[0];
|
|
27
|
-
// This is a compile-time test: the assertion below must type-check (all
|
|
28
|
-
// union members have `name: string`). The impl returns `{}`, so guard the
|
|
29
|
-
// dereference — the block never runs but TS still checks it.
|
|
30
|
-
if (service) {
|
|
31
|
-
const name = service.name;
|
|
32
|
-
void name;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
it('should resolve to projected type when used in a generic function with a select', () => {
|
|
36
|
-
function getServices(options) {
|
|
37
|
-
void options;
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
// When called with a select, the return type should be the projected type
|
|
41
|
-
const services = getServices({ select: { name: true } });
|
|
42
|
-
const service = Object.values(services)[0];
|
|
43
|
-
// This is a compile-time test: the assertion below must type-check (name
|
|
44
|
-
// was selected). The impl returns `{}`, so guard the dereference — the
|
|
45
|
-
// block never runs but TS still checks it.
|
|
46
|
-
if (service) {
|
|
47
|
-
const name = service.name;
|
|
48
|
-
void name;
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
describe('a field whose value is a union of objects', () => {
|
|
53
|
-
const channel = z.object({ _id: z.string(), key: z.string(), name: z.string() });
|
|
54
|
-
const effect = z.discriminatedUnion('kind', [
|
|
55
|
-
z.object({ channel, kind: z.literal('route') }),
|
|
56
|
-
z.object({ field: z.string(), kind: z.literal('require') })
|
|
57
|
-
]);
|
|
58
|
-
const policy = z.object({
|
|
59
|
-
_id: z.string(),
|
|
60
|
-
effect,
|
|
61
|
-
effects: z.array(effect).optional(),
|
|
62
|
-
name: z.union([z.string(), z.object({ en: z.string() })])
|
|
63
|
-
});
|
|
64
|
-
it('accepts a nested select into the variants, and refines each variant by it', () => {
|
|
65
|
-
const select = { effect: { channel: { _id: true, name: true }, field: true, kind: true } };
|
|
66
|
-
const parsed = refineSchema(policy, select).parse({ effect: { channel: { _id: 'c1', key: 'rpo', name: 'RPO' }, kind: 'route' } });
|
|
67
|
-
assert.deepEqual(parsed, { effect: { channel: { _id: 'c1', name: 'RPO' }, kind: 'route' } });
|
|
68
|
-
});
|
|
69
|
-
it('types the result variant by variant, so checking the kind narrows to what that variant selected', () => {
|
|
70
|
-
const select = { effect: { channel: { _id: true, name: true }, field: true, kind: true } };
|
|
71
|
-
const isTyped = true;
|
|
72
|
-
assert.ok(isTyped);
|
|
73
|
-
assert.deepEqual(refineSchema(policy, select).parse({ effect: { field: 'rationale', kind: 'require' } }), { effect: { field: 'rationale', kind: 'require' } });
|
|
74
|
-
});
|
|
75
|
-
it('does the same for an array of such a union', () => {
|
|
76
|
-
const select = { effects: { channel: { name: true }, kind: true } };
|
|
77
|
-
const isTyped = true;
|
|
78
|
-
const effects = [{ channel: { _id: 'c1', key: 'rpo', name: 'RPO' }, kind: 'route' }, { field: 'rationale', kind: 'require' }];
|
|
79
|
-
assert.ok(isTyped);
|
|
80
|
-
assert.deepEqual(refineSchema(policy, select).parse({ effects }), { effects: [{ channel: { name: 'RPO' }, kind: 'route' }, { kind: 'require' }] });
|
|
81
|
-
});
|
|
82
|
-
it('accepts the select buildSelectObject builds for it', () => {
|
|
83
|
-
const select = buildSelectObject(policy);
|
|
84
|
-
assert.deepEqual(select, {
|
|
85
|
-
_id: true,
|
|
86
|
-
effect: { channel: { _id: true, key: true, name: true }, field: true, kind: true },
|
|
87
|
-
effects: { channel: { _id: true, key: true, name: true }, field: true, kind: true },
|
|
88
|
-
name: true
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
it('accepts a nested select into the object variants of a union with a plain one, as a join read as its record is', () => {
|
|
92
|
-
const select = { name: { en: true } };
|
|
93
|
-
const isTyped = true;
|
|
94
|
-
assert.ok(isTyped);
|
|
95
|
-
assert.deepEqual(refineSchema(policy, select).parse({ name: { en: 'Software' } }), { name: { en: 'Software' } });
|
|
96
|
-
});
|
|
97
|
-
it('accepts the select a screen builds by declaring a joined id as its record', () => {
|
|
98
|
-
const file = z.object({ _id: z.string(), filename: z.string() });
|
|
99
|
-
const attachment = z.object({ file: z.union([file, z.string()]), title: z.string().optional() });
|
|
100
|
-
const stored = z.object({ attachments: z.array(attachment).optional() });
|
|
101
|
-
const read = z.object({ attachments: z.array(attachment.extend({ file })).optional() });
|
|
102
|
-
const select = buildSelectObject(read);
|
|
103
|
-
const isTyped = true;
|
|
104
|
-
assert.ok(isTyped);
|
|
105
|
-
assert.deepEqual(select, { attachments: { file: { _id: true, filename: true }, title: true } });
|
|
106
|
-
assert.deepEqual(refineSchema(stored, select).parse({ attachments: [{ file: { _id: 'f1', filename: 'a.pdf' } }] }), { attachments: [{ file: { _id: 'f1', filename: 'a.pdf' } }] });
|
|
107
|
-
});
|
|
108
|
-
});
|
package/dist/utilities.spec.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/utilities.spec.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert';
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { getInnerType, isZodObject, isZodOptional, isZodRecord, isZodUnion, isZodUnknown } from './utilities.js';
|
|
5
|
-
describe('.utilities()', () => {
|
|
6
|
-
describe('.getInnerType()', () => {
|
|
7
|
-
it('getInnerType identifies inner types', () => {
|
|
8
|
-
const optionalString = z.string().optional();
|
|
9
|
-
const nullableString = z.string().nullable();
|
|
10
|
-
const defaultString = z.string().default('default');
|
|
11
|
-
const catchString = z.string().catch('fallback');
|
|
12
|
-
const lazyString = z.lazy(() => z.string());
|
|
13
|
-
const arrayString = z.array(z.string());
|
|
14
|
-
assert.strictEqual(getInnerType(optionalString).def.type, optionalString.def.innerType.def.type);
|
|
15
|
-
assert.strictEqual(getInnerType(nullableString).def.type, nullableString.def.innerType.def.type);
|
|
16
|
-
assert.strictEqual(getInnerType(defaultString).def.type, defaultString.def.innerType.def.type);
|
|
17
|
-
assert.strictEqual(getInnerType(catchString).def.type, catchString.def.innerType.def.type);
|
|
18
|
-
assert.strictEqual(getInnerType(lazyString).def.type, lazyString.def.getter().def.type);
|
|
19
|
-
assert.strictEqual(getInnerType(arrayString).def.type, arrayString.element.def.type);
|
|
20
|
-
assert.strictEqual(getInnerType(z.string()).def.type, 'string'); // Not a wrapper type
|
|
21
|
-
});
|
|
22
|
-
it('should return base type from array', () => {
|
|
23
|
-
const schema = z.array(z.number());
|
|
24
|
-
const inner = getInnerType(schema);
|
|
25
|
-
assert.strictEqual(inner.def.type, 'number');
|
|
26
|
-
});
|
|
27
|
-
it('should unwrap nested optional -> nullable -> default -> string', () => {
|
|
28
|
-
const schema = z.string().default('x').nullable().optional();
|
|
29
|
-
const inner = getInnerType(schema);
|
|
30
|
-
assert.strictEqual(inner.def.type, 'string');
|
|
31
|
-
});
|
|
32
|
-
it('should unwrap lazy inside nested wrappers', () => {
|
|
33
|
-
const lazySchema = z.lazy(() => z.string());
|
|
34
|
-
const wrapped = lazySchema.optional().nullable().default('abc');
|
|
35
|
-
const inner = getInnerType(wrapped);
|
|
36
|
-
assert.strictEqual(inner.def.type, 'string');
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
it('isZodObject detects ZodObject', () => {
|
|
40
|
-
const objectSchema = z.object({ name: z.string() });
|
|
41
|
-
const stringSchema = z.string();
|
|
42
|
-
assert.ok(isZodObject(objectSchema));
|
|
43
|
-
assert.ok(!isZodObject(stringSchema));
|
|
44
|
-
});
|
|
45
|
-
it('isZodObject should detect loose objects when isLoose is passed', () => {
|
|
46
|
-
assert.ok(isZodObject(z.looseObject({}), true));
|
|
47
|
-
assert.ok(!isZodObject(z.looseObject({}), false));
|
|
48
|
-
assert.ok(!isZodObject(z.strictObject({}), true));
|
|
49
|
-
assert.ok(isZodObject(z.strictObject({}), false));
|
|
50
|
-
});
|
|
51
|
-
it('isZodOptional detects ZodOptional', () => {
|
|
52
|
-
const optionalSchema = z.string().optional();
|
|
53
|
-
const stringSchema = z.string();
|
|
54
|
-
assert.ok(isZodOptional(optionalSchema));
|
|
55
|
-
assert.ok(!isZodOptional(stringSchema));
|
|
56
|
-
});
|
|
57
|
-
it('isZodRecord detects complex record schemas with optional fields', () => {
|
|
58
|
-
// Create a sample RecordAttachmentSchema
|
|
59
|
-
const recordAttachmentSchema = z.object({
|
|
60
|
-
name: z.string(),
|
|
61
|
-
size: z.number(),
|
|
62
|
-
type: z.string()
|
|
63
|
-
});
|
|
64
|
-
// Create a record schema with optional field
|
|
65
|
-
const attachmentsSchema = z.record(z.string(), recordAttachmentSchema).optional();
|
|
66
|
-
const nonRecordSchema = z.object({ field: z.string() });
|
|
67
|
-
// Test record detection
|
|
68
|
-
assert.ok(isZodRecord(z.record(z.string(), recordAttachmentSchema)), 'Should detect record schema');
|
|
69
|
-
assert.ok(isZodRecord(attachmentsSchema.unwrap()), 'Should detect optional record schema when unwrapped');
|
|
70
|
-
assert.ok(!isZodRecord(nonRecordSchema), 'Should not detect object schema as record');
|
|
71
|
-
// Test with actual record data
|
|
72
|
-
const validRecord = z.record(z.string(), recordAttachmentSchema).parse({
|
|
73
|
-
'file1': { name: 'test.pdf', size: 1024, type: 'application/pdf' },
|
|
74
|
-
'file2': { name: 'image.jpg', size: 2048, type: 'image/jpeg' }
|
|
75
|
-
});
|
|
76
|
-
assert.ok(validRecord, 'Should successfully parse valid record data');
|
|
77
|
-
});
|
|
78
|
-
it('isZodRecord detects ZodRecord', () => {
|
|
79
|
-
const recordSchema = z.record(z.string(), z.string());
|
|
80
|
-
const stringSchema = z.string();
|
|
81
|
-
assert.ok(isZodRecord(recordSchema));
|
|
82
|
-
assert.ok(!isZodRecord(stringSchema));
|
|
83
|
-
});
|
|
84
|
-
it('isZodUnion detects ZodUnion', () => {
|
|
85
|
-
const unionSchema = z.union([z.string(), z.number()]);
|
|
86
|
-
const stringSchema = z.string();
|
|
87
|
-
assert.ok(isZodUnion(unionSchema));
|
|
88
|
-
assert.ok(!isZodUnion(stringSchema));
|
|
89
|
-
});
|
|
90
|
-
it('isZodUnknown detects ZodUnknown', () => {
|
|
91
|
-
assert.ok(isZodUnknown(z.unknown()));
|
|
92
|
-
assert.ok(!isZodUnknown(z.string()));
|
|
93
|
-
});
|
|
94
|
-
});
|