zodselect 1.2.8 โ†’ 1.3.0

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/README.md CHANGED
@@ -139,6 +139,14 @@ const schema = refineSchema(UserSchema, {
139
139
  }
140
140
  ```
141
141
 
142
+ A field the schema doesn't have is a type error, at any depth and even beside valid fields:
143
+
144
+ ```ts
145
+ refineSchema(UserSchema, { firstName: true, lastNmae: true }); // โœ– 'lastNmae' is not a field of UserSchema
146
+ ```
147
+
148
+ Code that builds a selection generically, whose type the check can't evaluate, calls `refineSchemaUnchecked` instead: the same refinement, with a field the schema doesn't have skipped.
149
+
142
150
  ---
143
151
 
144
152
  ## ๐Ÿงช Refine Field Types Inline
@@ -2,6 +2,18 @@ import { core, z, ZodAny, ZodArray, ZodBoolean, ZodDate, ZodLiteral, ZodNever, Z
2
2
  import { IsAny, IsJsonType } from './types.js';
3
3
  type ApplyOptionalNullable<T, U extends ZodType> = IsAny<T, U, unknown extends T ? U : undefined extends T ? null extends T ? ZodOptional<ZodNullable<U>> : ZodOptional<U> : null extends T ? ZodNullable<U> : U>;
4
4
  type DepthLimit = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
5
+ /** `TShape` with every key `T` doesn't have, at any depth, as `never`, so a misspelt select is a type error even beside valid keys. */
6
+ type Exact<TShape, T> = {
7
+ [K in keyof TShape]: K extends FieldKeys<T> ? TShape[K] extends ((schema: never) => unknown) | boolean | ZodType ? TShape[K] : TShape[K] extends object ? Exact<TShape[K], FieldObjects<FieldAt<T, K>>> : TShape[K] : never;
8
+ };
9
+ /** What the field `TKey` holds in any object of `T`. */
10
+ type FieldAt<T, TKey> = T extends unknown ? TKey extends keyof T ? T[TKey] : never : never;
11
+ /** The declared keys of every object in `T`, each of a union's own. */
12
+ type FieldKeys<T> = T extends unknown ? Extract<keyof {
13
+ [K in keyof T as string extends K ? never : number extends K ? never : K]: unknown;
14
+ }, string> : never;
15
+ /** The objects a field of type `T` holds, through arrays: what a nested select picks within. */
16
+ type FieldObjects<T> = T extends readonly (infer E)[] ? FieldObjects<E> : T extends Date ? never : T extends object ? T : never;
5
17
  type InternalZodify<T, TDepth extends number = 0> = TDepth extends 9 ? ZodType : IsAny<T, ZodAny, T extends string ? string extends T ? ZodString : ZodLiteral<T> : T extends number ? number extends T ? ZodNumber : ZodLiteral<T> : T extends boolean ? boolean extends T ? ZodBoolean : ZodLiteral<T> : T extends Date ? ZodDate : unknown extends T ? ZodUnknown : T extends Array<infer U> ? ZodArray<Zodify<U, NextDepth<TDepth>>> : T extends object ? IsJsonType<T, true, ZodObject<{
6
18
  [K in keyof T]-?: Zodify<T[K], NextDepth<TDepth>>;
7
19
  }, core.$strict>, ZodType<T>> : never>;
@@ -12,8 +24,8 @@ type Push<T extends any[], V> = [...T, V];
12
24
  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
25
  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
26
  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 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
- }, core.$strict> : any;
27
+ [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;
28
+ }, core.$strip> : any;
17
29
  export type RefinedUnionSchema<T extends ZodUnion, TShape> = ZodUnion<T['options'] extends Readonly<[infer A, ...infer Rest]> ? Rest extends ZodType[] ? [
18
30
  ReapplyOptionalNullable<A, A extends ZodType<object> ? RefinedTypeSchema<z.infer<A>, TShape> : Extract<A, ZodType>>,
19
31
  ...{
@@ -43,5 +55,11 @@ type ZodifyField<T, TKey extends keyof T> = unknown extends T[TKey] ? Partial<Pi
43
55
  type ZodifyLiteral<T, TElse> = [
44
56
  T
45
57
  ] extends [boolean | number | string] ? [T] extends [boolean] ? boolean extends T ? ZodBoolean : ZodLiteral<T> : string extends T ? [T] extends [string] ? ZodString : TElse : number extends T ? [T] extends [number] ? ZodNumber : TElse : ZodLiteral<T> : TElse;
46
- export declare function refineSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, false>>(schema: T, shape: TShape): RefinedSchema<T, TShape, false>;
58
+ /** `shape`'s fields of `schema`. A key the schema doesn't have, at any depth, is a type error. */
59
+ export declare function refineSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, false>>(schema: T, shape: TShape & Exact<TShape, z.infer<T>>): RefinedSchema<T, TShape, false>;
60
+ /**
61
+ * {@link refineSchema} for a shape whose type is still generic, which the exact key check can't evaluate.
62
+ * Prefer {@link refineSchema}: a key the schema doesn't have is skipped here, not refused.
63
+ */
64
+ export declare function refineSchemaUnchecked<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, false>>(schema: T, shape: TShape): RefinedSchema<T, TShape, false>;
47
65
  export {};
@@ -1,22 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { isZodArray, isZodCatch, isZodDefault, isZodLazy, isZodNullable, isZodObject, isZodOptional, isZodType, isZodUnion } from './utilities.js';
3
- // Main function to refine the schema
3
+ /** `shape`'s fields of `schema`. A key the schema doesn't have, at any depth, is a type error. */
4
4
  export function refineSchema(schema, shape) {
5
- if (isZodObject(schema)) {
6
- const refinedShape = {};
7
- const schemaShape = schema.shape;
8
- for (const key in shape) {
9
- if (key in schemaShape && shape[key] !== false) {
10
- refinedShape[key] = refineSchemaField(schemaShape[key], shape[key]);
11
- }
12
- }
13
- return z.object(refinedShape);
14
- }
15
- else if (isZodUnion(schema)) {
16
- const refinedOptions = schema.options.map(option => refineSchemaField(option, shape));
17
- return z.union(refinedOptions);
18
- }
19
- throw new Error('Unsupported schema type for refinement');
5
+ return refineSchemaUnchecked(schema, shape);
20
6
  }
21
7
  // Helper function to refine a single schema field
22
8
  function refineSchemaField(fieldSchema, fieldShape) {
@@ -30,10 +16,10 @@ function refineSchemaField(fieldSchema, fieldShape) {
30
16
  return fieldShape; // Use redefined type
31
17
  }
32
18
  else if (isZodObject(fieldSchema)) {
33
- return refineSchema(fieldSchema, fieldShape); // Recursively refine nested objects
19
+ return refineSchemaUnchecked(fieldSchema, fieldShape); // Recursively refine nested objects
34
20
  }
35
21
  else if (isZodUnion(fieldSchema)) {
36
- return refineSchema(fieldSchema, fieldShape);
22
+ return refineSchemaUnchecked(fieldSchema, fieldShape);
37
23
  }
38
24
  else if (isZodArray(fieldSchema)) {
39
25
  // Recursively refine array items
@@ -54,3 +40,24 @@ function refineSchemaField(fieldSchema, fieldShape) {
54
40
  }
55
41
  return fieldSchema;
56
42
  }
43
+ /**
44
+ * {@link refineSchema} for a shape whose type is still generic, which the exact key check can't evaluate.
45
+ * Prefer {@link refineSchema}: a key the schema doesn't have is skipped here, not refused.
46
+ */
47
+ export function refineSchemaUnchecked(schema, shape) {
48
+ if (isZodObject(schema)) {
49
+ const refinedShape = {};
50
+ const schemaShape = schema.shape;
51
+ for (const key in shape) {
52
+ if (key in schemaShape && shape[key] !== false) {
53
+ refinedShape[key] = refineSchemaField(schemaShape[key], shape[key]);
54
+ }
55
+ }
56
+ return z.object(refinedShape);
57
+ }
58
+ else if (isZodUnion(schema)) {
59
+ const refinedOptions = schema.options.map(option => refineSchemaField(option, shape));
60
+ return z.union(refinedOptions);
61
+ }
62
+ throw new Error('Unsupported schema type for refinement');
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zodselect",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
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/eslint-config": "^1.0.38",
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": "npm:@typescript/typescript6@^6.0.2",
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": "eslint . --c eslint.config.mjs",
43
- "lint:fix": "eslint . --c eslint.config.mjs --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,414 +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('keeps an array under a default, a catch or a lazy an array of the picked fields', () => {
132
- const line = z.object({ id: z.string(), secret: z.string() });
133
- const order = z.object({
134
- caught: z.array(line).catch([]),
135
- defaulted: z.array(line).default([]),
136
- lazy: z.lazy(() => z.array(line))
137
- });
138
- const refined = refineSchema(order, { caught: { id: true }, defaulted: { id: true }, lazy: { id: true } });
139
- const lines = [{ id: 'a', secret: 's' }];
140
- deepStrictEqual(refined.parse({ caught: lines, defaulted: lines, lazy: lines }), { caught: [{ id: 'a' }], defaulted: [{ id: 'a' }], lazy: [{ id: 'a' }] });
141
- assert.ok(refined.shape.defaulted instanceof z.ZodArray);
142
- assert.ok(refined.shape.caught instanceof z.ZodArray);
143
- });
144
- it('should handle custom fields', () => {
145
- const userSchema = z.object({
146
- address: z.custom().optional(),
147
- firstName: z.string(),
148
- lastName: z.string()
149
- });
150
- const refinedSchema = refineSchema(userSchema, {
151
- address: true
152
- });
153
- const parsedData = refinedSchema.parse({
154
- address: {
155
- city: 'Austin',
156
- state: 'TX',
157
- street: '123 Main Street'
158
- },
159
- firstName: 'Alice',
160
- lastName: 'Smith'
161
- });
162
- assert.deepStrictEqual(parsedData, {
163
- address: {
164
- city: 'Austin',
165
- state: 'TX',
166
- street: '123 Main Street'
167
- }
168
- });
169
- });
170
- it('should support unions of objects', () => {
171
- const person = z.object({
172
- firstName: z.string(),
173
- lastName: z.string()
174
- });
175
- const address = z.object({
176
- address: z.string(),
177
- city: z.string()
178
- });
179
- const schema = refineSchema(z.union([person, address]), {
180
- address: true,
181
- firstName: true
182
- });
183
- let parseResult = schema.parse({
184
- address: '123 Main Street',
185
- firstName: 'Jerry',
186
- lastName: 'Springer'
187
- });
188
- deepStrictEqual(parseResult, { firstName: 'Jerry' });
189
- parseResult = schema.parse({
190
- address: '123 Main Street',
191
- city: 'Austin'
192
- });
193
- deepStrictEqual(parseResult, { address: '123 Main Street' });
194
- });
195
- it('should error if none of the types are matched', () => {
196
- const person = z.object({
197
- firstName: z.string(),
198
- lastName: z.string()
199
- });
200
- const address = z.object({
201
- address: z.string(),
202
- city: z.string()
203
- });
204
- const schema = refineSchema(z.union([person, address]), {
205
- address: true,
206
- firstName: true
207
- });
208
- throws(() => {
209
- schema.parse({
210
- lastName: 'Springer'
211
- });
212
- }, (err) => {
213
- if (!(err instanceof ZodError) || err.issues[0].code !== 'invalid_union') {
214
- return false;
215
- }
216
- return true;
217
- });
218
- });
219
- it('should exclude fields specified as false in the shape', () => {
220
- const schema = z.object({
221
- firstName: z.string(),
222
- lastName: z.string()
223
- });
224
- const refined = refineSchema(schema, {
225
- firstName: true,
226
- lastName: false
227
- });
228
- // lastName was specified as false โ€” it should be excluded from the refined schema
229
- const result = refined.parse({ firstName: 'Alice', lastName: 'Smith' });
230
- assert.deepStrictEqual(result, { firstName: 'Alice' });
231
- });
232
- it('should support unions of objects and simple types', () => {
233
- const person = z.object({
234
- firstName: z.string(),
235
- lastName: z.string()
236
- });
237
- const schema = refineSchema(z.union([person, z.string()]), {
238
- address: true,
239
- firstName: true
240
- });
241
- let parseResult = schema.parse({
242
- firstName: 'Jerry',
243
- lastName: 'Springer'
244
- });
245
- deepStrictEqual(parseResult, { firstName: 'Jerry' });
246
- parseResult = schema.parse('123 Main Street');
247
- deepStrictEqual(parseResult, '123 Main Street');
248
- });
249
- it('should accept selecting a subfield of a union member with an optional array-of-objects field', () => {
250
- // Regression test for a bug in `UnionFieldType` (refine-schema.ts).
251
- //
252
- // `UnionFieldType` chose a union member field's refine shape with
253
- // `z.infer<T> extends Array<infer U>` but, unlike `RefineType`, it did not
254
- // strip `| undefined` first. For an OPTIONAL array-of-objects member
255
- // (`field: Obj.array().optional()`), `z.infer<T>` is `Obj[] | undefined`,
256
- // which is NOT assignable to `Array<infer U>`, so the field's refine shape
257
- // collapsed to `RefineObject<Obj[]>` (the array's own keys) instead of
258
- // `RefineObject<Obj>`. The runtime was unaffected, so this only broke the
259
- // type-check (`tsc`), not `tsx --test`.
260
- const accessLevel = z.object({
261
- _id: z.string(),
262
- name: z.string()
263
- });
264
- const organizationInvitation = z.object({
265
- accessLevels: accessLevel.array().optional(),
266
- organizationId: z.string(),
267
- type: z.literal('organization')
268
- });
269
- const platformInvitation = z.object({
270
- accessLevels: accessLevel.array().optional(),
271
- type: z.literal('platform')
272
- });
273
- const invitation = z.discriminatedUnion('type', [organizationInvitation, platformInvitation]);
274
- // Type-level assertion: drilling into a subfield of the optional array
275
- // member is a valid select and MUST be assignable to the refine shape.
276
- // With the bug present, this `satisfies` fails to compile.
277
- const select = {
278
- accessLevels: { name: true },
279
- type: true
280
- };
281
- const refined = refineSchema(invitation, select);
282
- const parsed = refined.parse({
283
- accessLevels: [{ name: 'Admin' }],
284
- type: 'organization'
285
- });
286
- assert.equal(parsed.type, 'organization');
287
- assert.deepEqual(parsed.accessLevels, [{ name: 'Admin' }]);
288
- });
289
- it('allows selecting a nested subfield of a declared looseObject field', () => {
290
- // Regression test for `RefineObject` collapsing a `z.looseObject`'s declared fields.
291
- //
292
- // A `z.looseObject` carries a catchall index signature (`[k: string]: unknown`).
293
- // `RefineObject` maps over `keyof T`, and when `T` has an index signature `keyof T`
294
- // collapses to `string | number`, so every declared field's value type came from the
295
- // catchall (`unknown` -> scalar `boolean`). Declared fields could be marked `true` but
296
- // could NOT be drilled into. The runtime was unaffected, so this only broke the
297
- // type-check (`tsc`), not `tsx --test`.
298
- const personName = z.object({
299
- first: z.string().optional(),
300
- last: z.string().optional()
301
- });
302
- const user = z.looseObject({
303
- _id: z.string(),
304
- name: personName.optional(),
305
- username: z.string()
306
- });
307
- const root = z.object({
308
- invitedBy: user
309
- });
310
- // Type-level assertion: drilling into `invitedBy.name.first` is valid and MUST be
311
- // assignable to the refine shape. With the bug present this `satisfies` fails to
312
- // compile because `invitedBy`'s declared fields collapse to an index signature with
313
- // `boolean` values.
314
- const select = {
315
- invitedBy: { name: { first: true } }
316
- };
317
- const refined = refineSchema(root, select);
318
- const parsed = refined.parse({
319
- invitedBy: { name: { first: 'Alice' } }
320
- });
321
- assert.equal(parsed.invitedBy.name?.first, 'Alice');
322
- });
323
- it('keeps every option of a small enum in the refined type (control)', () => {
324
- const stage = z.enum(['one', 'two', 'three']);
325
- const project = z.object({ name: z.string(), stage });
326
- const refined = refineSchema(project, { stage: true });
327
- // Type-level assertion: every option must remain assignable to the refined type.
328
- const selected = [
329
- { stage: 'one' },
330
- { stage: 'two' },
331
- { stage: 'three' }
332
- ];
333
- deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
334
- });
335
- it('keeps every option of an enum wider than nine options in the refined type', () => {
336
- // Regression test for `TuplifyUnion` (refine-schema.ts) truncating wide enums.
337
- //
338
- // `TuplifyUnion` spends one recursion level per union member and stops at its
339
- // depth cap, so an enum field refined with `true` silently dropped every option
340
- // past nine from the refined TYPE โ€” and which options vanished varied with
341
- // TypeScript's internal union ordering. Fixed by the ZodLiteral shortcut in
342
- // `Zodify`. The runtime was unaffected, so this only broke the type-check
343
- // (`tsc`), not `tsx --test`.
344
- const stage = z.enum([
345
- 'draft', 'interviewing', 'interview-complete',
346
- 'previewing', 'preview-review',
347
- 'plan-review', 'plan-approved', 'local',
348
- 'spec-review', 'spec-approved',
349
- 'implementing', 'module-ready'
350
- ]);
351
- const project = z.object({ name: z.string(), stage });
352
- const refined = refineSchema(project, { stage: true });
353
- // Type-level assertion: each of the twelve options must remain assignable to
354
- // the refined type. With the bug present, three of these assignments fail TS2322.
355
- const selected = [
356
- { stage: 'draft' },
357
- { stage: 'interviewing' },
358
- { stage: 'interview-complete' },
359
- { stage: 'previewing' },
360
- { stage: 'preview-review' },
361
- { stage: 'plan-review' },
362
- { stage: 'plan-approved' },
363
- { stage: 'local' },
364
- { stage: 'spec-review' },
365
- { stage: 'spec-approved' },
366
- { stage: 'implementing' },
367
- { stage: 'module-ready' }
368
- ];
369
- deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
370
- });
371
- });
372
- describe('.refineSchema() literal and unknown field types', () => {
373
- // Regression tests for `Zodify` (refine-schema.ts) when a schema's fields are listed
374
- // (e.g. via `buildSelectObject`) instead of selected whole. Before the fix a numeric
375
- // literal union widened to `number` and a required `z.unknown()` field became
376
- // optional, so the selected type no longer fit the source type. Runtime was
377
- // unaffected; these assignments fail the type-check (`tsc`).
378
- const question = z.object({
379
- answer: z.object({ value: z.unknown() }),
380
- flag: z.literal(true),
381
- note: z.unknown().optional(),
382
- scale: z.union([z.literal(5), z.literal(10)]),
383
- steps: z.literal([3, 7]),
384
- value: z.unknown()
385
- });
386
- const survey = z.object({
387
- name: z.string(),
388
- pages: z.array(z.object({ questions: z.array(question) }))
389
- });
390
- it('keeps a numeric literal union instead of widening it to number', () => {
391
- const refined = refineSchema(survey, { pages: { questions: { scale: true, steps: true } } });
392
- // With the bug these resolve to `number`, so the source type can't take them.
393
- const scale = {}.scale;
394
- const steps = {}.steps;
395
- void scale;
396
- void steps;
397
- const parsed = refined.parse({ pages: [{ questions: [{ scale: 10, steps: 3 }] }] });
398
- assert.equal(parsed.pages[0]?.questions[0]?.scale, 10);
399
- throws(() => refined.parse({ pages: [{ questions: [{ scale: 7, steps: 3 }] }] }), ZodError);
400
- });
401
- it('keeps a boolean literal instead of widening it to boolean', () => {
402
- const refined = refineSchema(survey, { pages: { questions: { flag: true } } });
403
- const isFlag = {}.flag;
404
- void isFlag;
405
- throws(() => refined.parse({ pages: [{ questions: [{ flag: false }] }] }), ZodError);
406
- });
407
- it('keeps a required unknown field required', () => {
408
- const refined = refineSchema(survey, { pages: { questions: { answer: true, note: true, value: true } } });
409
- // With the bug `value` became an optional key; `note` must stay optional.
410
- const selected = { answer: { value: 1 }, value: 'x' };
411
- const questions = [selected];
412
- deepStrictEqual(refined.parse({ pages: [{ questions }] }).pages[0]?.questions, questions);
413
- });
414
- });
@@ -1 +0,0 @@
1
- export {};
@@ -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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -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
- });