zodselect 1.0.14 → 1.0.16
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/build-select-object.d.ts +3 -3
- package/dist/build-select-object.js +36 -8
- package/dist/build-select-object.spec.js +65 -2
- package/dist/refine-schema.d.ts +1 -1
- package/dist/refine-schema.spec.js +40 -0
- package/dist/select.spec.d.ts +1 -0
- package/dist/select.spec.js +48 -0
- package/package.json +1 -1
|
@@ -6,9 +6,9 @@ export type ZodSelectAll<T extends ZodType<object> | ZodUnion> = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Produces a runtime select-all object for the given Zod schema.
|
|
8
8
|
*
|
|
9
|
-
* Every
|
|
10
|
-
*
|
|
11
|
-
* set of keys across all variants.
|
|
9
|
+
* Every scalar key is set to `true`. Nested object fields are recursively
|
|
10
|
+
* expanded into nested select objects. Array fields remain `true`.
|
|
11
|
+
* For union schemas the result is the merged set of keys across all variants.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
14
|
* ```ts
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { ZodCatch, ZodDefault, ZodLazy, ZodNullable, ZodOptional } from 'zod';
|
|
1
2
|
import { getInnerType, isZodObject, isZodUnion } from './utilities.js';
|
|
2
3
|
/**
|
|
3
4
|
* Produces a runtime select-all object for the given Zod schema.
|
|
4
5
|
*
|
|
5
|
-
* Every
|
|
6
|
-
*
|
|
7
|
-
* set of keys across all variants.
|
|
6
|
+
* Every scalar key is set to `true`. Nested object fields are recursively
|
|
7
|
+
* expanded into nested select objects. Array fields remain `true`.
|
|
8
|
+
* For union schemas the result is the merged set of keys across all variants.
|
|
8
9
|
*
|
|
9
10
|
* @example
|
|
10
11
|
* ```ts
|
|
@@ -17,20 +18,47 @@ export function buildSelectObject(schema) {
|
|
|
17
18
|
collectKeys(schema, result);
|
|
18
19
|
return result;
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Unwraps optional/nullable/default/catch/lazy wrappers but NOT arrays.
|
|
23
|
+
*/
|
|
24
|
+
function unwrapField(schema) {
|
|
25
|
+
if (schema instanceof ZodOptional
|
|
26
|
+
|| schema instanceof ZodNullable
|
|
27
|
+
|| schema instanceof ZodDefault
|
|
28
|
+
|| schema instanceof ZodCatch) {
|
|
29
|
+
return unwrapField(schema.def.innerType);
|
|
30
|
+
}
|
|
31
|
+
if (schema instanceof ZodLazy) {
|
|
32
|
+
return unwrapField(schema.def.getter());
|
|
33
|
+
}
|
|
34
|
+
return schema;
|
|
35
|
+
}
|
|
36
|
+
const MAX_DEPTH = 10;
|
|
37
|
+
function collectKeys(schema, result, depth = 0) {
|
|
38
|
+
if (depth >= MAX_DEPTH) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
21
41
|
schema = getInnerType(schema);
|
|
22
42
|
if (isZodObject(schema)) {
|
|
23
43
|
for (const key in schema.shape) {
|
|
24
|
-
|
|
44
|
+
const fieldInner = unwrapField(schema.shape[key]);
|
|
45
|
+
if (isZodObject(fieldInner)) {
|
|
46
|
+
const nested = {};
|
|
47
|
+
collectKeys(fieldInner, nested, depth + 1);
|
|
48
|
+
result[key] = nested;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
result[key] = true;
|
|
52
|
+
}
|
|
25
53
|
}
|
|
26
54
|
}
|
|
27
55
|
else if (isZodUnion(schema)) {
|
|
28
56
|
for (const option of schema.options) {
|
|
29
|
-
collectKeys(option, result);
|
|
57
|
+
collectKeys(option, result, depth);
|
|
30
58
|
}
|
|
31
59
|
}
|
|
32
60
|
else if (schema.def.type === 'intersection') {
|
|
33
|
-
collectKeys(schema.def.left, result);
|
|
34
|
-
collectKeys(schema.def.right, result);
|
|
61
|
+
collectKeys(schema.def.left, result, depth);
|
|
62
|
+
collectKeys(schema.def.right, result, depth);
|
|
35
63
|
}
|
|
36
64
|
}
|
|
@@ -45,7 +45,7 @@ describe('.buildSelectObject()', () => {
|
|
|
45
45
|
c: true
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
-
it('should
|
|
48
|
+
it('should recursively expand nested objects', () => {
|
|
49
49
|
const schema = z.object({
|
|
50
50
|
active: z.boolean(),
|
|
51
51
|
user: z.object({
|
|
@@ -53,6 +53,69 @@ describe('.buildSelectObject()', () => {
|
|
|
53
53
|
name: z.string()
|
|
54
54
|
})
|
|
55
55
|
});
|
|
56
|
-
assert.deepStrictEqual(buildSelectObject(schema), {
|
|
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 not expand arrays 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
|
+
assert.deepStrictEqual(buildSelectObject(schema), { items: true, name: true });
|
|
103
|
+
});
|
|
104
|
+
it('should handle recursive schemas without infinite loop', () => {
|
|
105
|
+
;
|
|
106
|
+
const node = z.lazy(() => z.object({
|
|
107
|
+
children: z.array(node).optional(),
|
|
108
|
+
name: z.string(),
|
|
109
|
+
parent: node.optional()
|
|
110
|
+
}));
|
|
111
|
+
const schema = z.object({
|
|
112
|
+
name: z.string(),
|
|
113
|
+
root: node
|
|
114
|
+
});
|
|
115
|
+
// Should not throw or hang — recursive objects hit depth limit
|
|
116
|
+
const result = buildSelectObject(schema);
|
|
117
|
+
assert.strictEqual(result.name, true);
|
|
118
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
119
|
+
assert.ok(typeof result.root === 'object' && result.root !== null);
|
|
57
120
|
});
|
|
58
121
|
});
|
package/dist/refine-schema.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type RefineZodUnion<T extends ZodUnion> = {
|
|
|
33
33
|
[K in keyof UnionMergedShape<T>]?: UnionMergedShape<T>[K] extends ZodType ? UnionFieldType<UnionMergedShape<T>[K]> : boolean;
|
|
34
34
|
};
|
|
35
35
|
export type TuplifyUnion<T, TDepth extends number = 0, L = LastOf<T>, N = [T] extends [never] ? true : false> = TDepth extends 9 ? [] : true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>, NextDepth<TDepth>>, InternalZodify<L>>;
|
|
36
|
-
type UnionFieldType<T extends ZodType> = z.infer<T> extends Array<infer U> ? Exclude<U, null | undefined> extends object ? boolean | RefineObject<Extract<U, object>, true> : boolean : Exclude<z.infer<T>, null | undefined> extends object ? boolean | RefineObject<Exclude<z.infer<T>, null | undefined>, true> : boolean;
|
|
36
|
+
type UnionFieldType<T extends ZodType> = Exclude<z.infer<T>, null | undefined> extends Array<infer U> ? Exclude<U, null | undefined> extends object ? boolean | RefineObject<Extract<U, object>, true> : boolean : Exclude<z.infer<T>, null | undefined> extends object ? boolean | RefineObject<Exclude<z.infer<T>, null | undefined>, true> : boolean;
|
|
37
37
|
type UnionMergedShape<T extends ZodUnion> = UnionToIntersection<T['options'][number] extends ZodObject<infer Shape extends ZodRawShape> ? Shape : object>;
|
|
38
38
|
export type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
39
39
|
export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T, IsTuple<TuplifyUnion<Exclude<T, null | undefined>, TDepth>, ZodUnion<TuplifyUnion<Exclude<T, null | undefined>, TDepth>>, InternalZodify<Exclude<T, null | undefined>, TDepth>>>;
|
|
@@ -233,4 +233,44 @@ describe('.refineSchema()', () => {
|
|
|
233
233
|
parseResult = schema.parse('123 Main Street');
|
|
234
234
|
deepStrictEqual(parseResult, '123 Main Street');
|
|
235
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
|
+
});
|
|
236
276
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const internalService = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
type: z.literal('internal')
|
|
6
|
+
}).strict();
|
|
7
|
+
const externalService = z.object({
|
|
8
|
+
iconUrl: z.string().optional(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
type: z.literal('external')
|
|
11
|
+
}).strict();
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13
|
+
const serviceType = z.union([internalService, externalService]);
|
|
14
|
+
describe('InferMergedType', () => {
|
|
15
|
+
it('should resolve to z.infer<T> when used in a generic function with TSelect = undefined', () => {
|
|
16
|
+
function getServices(options) {
|
|
17
|
+
void options;
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
// When called without select, TSelect defaults to undefined
|
|
21
|
+
// InferMergedType<typeof Service, undefined> should resolve to z.infer<typeof Service>
|
|
22
|
+
const services = getServices({});
|
|
23
|
+
const service = Object.values(services)[0];
|
|
24
|
+
// This is a compile-time test: the assertion below must type-check (all
|
|
25
|
+
// union members have `name: string`). The impl returns `{}`, so guard the
|
|
26
|
+
// dereference — the block never runs but TS still checks it.
|
|
27
|
+
if (service) {
|
|
28
|
+
const name = service.name;
|
|
29
|
+
void name;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
it('should resolve to projected type when used in a generic function with a select', () => {
|
|
33
|
+
function getServices(options) {
|
|
34
|
+
void options;
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
// When called with a select, the return type should be the projected type
|
|
38
|
+
const services = getServices({ select: { name: true } });
|
|
39
|
+
const service = Object.values(services)[0];
|
|
40
|
+
// This is a compile-time test: the assertion below must type-check (name
|
|
41
|
+
// was selected). The impl returns `{}`, so guard the dereference — the
|
|
42
|
+
// block never runs but TS still checks it.
|
|
43
|
+
if (service) {
|
|
44
|
+
const name = service.name;
|
|
45
|
+
void name;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodselect",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
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",
|