zodselect 1.2.2 → 1.2.4

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.
@@ -3,7 +3,8 @@ import { UnionToIntersection } from './refine-schema.js';
3
3
  import { ZodSelect } from './select.js';
4
4
  type Depth = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
5
5
  type FieldSelectAll<T, TDepth extends number> = TDepth extends 0 ? true : Unwrap<T> extends ZodArray<infer TItem> ? ItemSelectAll<TItem, Depth[TDepth]> : ItemSelectAll<T, Depth[TDepth]>;
6
- type ItemSelectAll<T, TDepth extends number> = Unwrap<T> extends ZodObject<infer TShape, any> ? ObjectSelectAll<TShape, TDepth> : Unwrap<T> extends ZodUnion<infer TOptions> ? UnionShape<TOptions> extends infer TShape extends ZodRawShape ? ObjectSelectAll<TShape, TDepth> : true : true;
6
+ type ItemSelectAll<T, TDepth extends number> = Unwrap<T> extends ZodObject<infer TShape, any> ? ObjectSelectAll<TShape, TDepth> : Unwrap<T> extends ZodUnion<infer TOptions> ? [NotObject<TOptions[number]>] extends [never] ? UnionShape<TOptions> extends infer TShape extends ZodRawShape ? ObjectSelectAll<TShape, TDepth> : true : true : true;
7
+ type NotObject<T> = T extends unknown ? Unwrap<T> extends ZodObject<any, any> ? never : T : never;
7
8
  type ObjectSelectAll<TShape extends ZodRawShape, TDepth extends number> = {
8
9
  [K in keyof TShape]-?: FieldSelectAll<TShape[K], TDepth>;
9
10
  };
@@ -11,7 +12,7 @@ type ShapeOf<T> = T extends unknown ? Unwrap<T> extends ZodObject<infer TShape,
11
12
  /** The shapes of a union's object variants, merged; `unknown` when it has none. */
12
13
  type UnionShape<TOptions> = UnionToIntersection<TOptions extends readonly (infer TOption)[] ? ShapeOf<TOption> : never>;
13
14
  type Unwrap<T> = T extends ZodCatch<infer I> | ZodDefault<infer I> | ZodNullable<infer I> | ZodOptional<infer I> ? Unwrap<I> : T extends ZodLazy<infer I> ? Unwrap<I> : T;
14
- /** What {@link buildSelectObject} answers: nested for objects, unions with an object variant, and arrays of either; `true` for anything else. */
15
+ /** What {@link buildSelectObject} answers: nested for objects, unions of objects, and arrays of either; `true` for anything else. */
15
16
  export type ZodSelectAll<T extends ZodType<object> | ZodUnion> = Unwrap<T> extends ZodObject<infer TShape, any> ? ObjectSelectAll<TShape, 10> : {
16
17
  [K in keyof ZodSelect<T, true>]-?: true;
17
18
  };
@@ -20,9 +21,10 @@ export type ZodSelectAll<T extends ZodType<object> | ZodUnion> = Unwrap<T> exten
20
21
  *
21
22
  * Every scalar key is set to `true`. Nested object fields, and arrays of
22
23
  * objects, are recursively expanded into nested select objects. A union field
23
- * with an object variant, such as a file stored as an id and read as the file,
24
- * expands into the merged keys of its object variants. Other arrays and
25
- * records remain `true`.
24
+ * whose every variant is an object expands into their merged keys. A union
25
+ * with any other variant, such as a name that is a string or a structured
26
+ * name, remains `true`: its sub-fields would miss the plain value. Other arrays
27
+ * and records remain `true`.
26
28
  * For union schemas the result is the merged set of keys across all variants.
27
29
  *
28
30
  * @example
@@ -5,9 +5,10 @@ import { getInnerType, isZodArray, isZodObject, isZodUnion } from './utilities.j
5
5
  *
6
6
  * Every scalar key is set to `true`. Nested object fields, and arrays of
7
7
  * objects, are recursively expanded into nested select objects. A union field
8
- * with an object variant, such as a file stored as an id and read as the file,
9
- * expands into the merged keys of its object variants. Other arrays and
10
- * records remain `true`.
8
+ * whose every variant is an object expands into their merged keys. A union
9
+ * with any other variant, such as a name that is a string or a structured
10
+ * name, remains `true`: its sub-fields would miss the plain value. Other arrays
11
+ * and records remain `true`.
11
12
  * For union schemas the result is the merged set of keys across all variants.
12
13
  *
13
14
  * @example
@@ -46,7 +47,7 @@ function collectKeys(schema, result, depth = 0) {
46
47
  for (const key in schema.shape) {
47
48
  const field = unwrapField(schema.shape[key]);
48
49
  const fieldInner = isZodArray(field) ? unwrapField(field.def.element) : field;
49
- if (isZodObject(fieldInner) || hasObjectVariant(fieldInner)) {
50
+ if (isZodObject(fieldInner) || isUnionOfObjects(fieldInner)) {
50
51
  const nested = {};
51
52
  collectKeys(fieldInner, nested, depth + 1);
52
53
  result[key] = nested;
@@ -66,6 +67,6 @@ function collectKeys(schema, result, depth = 0) {
66
67
  collectKeys(schema.def.right, result, depth);
67
68
  }
68
69
  }
69
- function hasObjectVariant(schema) {
70
- return isZodUnion(schema) && schema.options.some(option => isZodObject(unwrapField(option)));
70
+ function isUnionOfObjects(schema) {
71
+ return isZodUnion(schema) && schema.options.every(option => isZodObject(unwrapField(option)));
71
72
  }
@@ -148,15 +148,15 @@ describe('.buildSelectObject()', () => {
148
148
  assert.ok(isTyped);
149
149
  assert.deepStrictEqual(select, { byKey: true, matrix: true, tags: true });
150
150
  });
151
- it('should expand a union field into the keys of its object variants', () => {
151
+ it('should select a union field whole when a variant is not an object', () => {
152
152
  const schema = z.object({
153
153
  file: z.union([z.object({ _id: z.string(), filename: z.string() }), z.string()]),
154
- name: z.string()
154
+ name: z.union([z.string(), z.object({ first: z.string(), last: z.string() })])
155
155
  });
156
156
  const select = buildSelectObject(schema);
157
157
  const isTyped = true;
158
158
  assert.ok(isTyped);
159
- assert.deepStrictEqual(select, { file: { _id: true, filename: true }, name: true });
159
+ assert.deepStrictEqual(select, { file: true, name: true });
160
160
  });
161
161
  it('should merge the keys of every object variant of a union field', () => {
162
162
  const schema = z.object({
@@ -183,16 +183,6 @@ describe('.buildSelectObject()', () => {
183
183
  assert.ok(isTyped);
184
184
  assert.deepStrictEqual(select, { options: { a: true, b: true } });
185
185
  });
186
- it('should expand an attachment whose file is stored as an id and read as the file', () => {
187
- const file = z.object({ _id: z.string(), contentType: z.string().optional(), filename: z.string() });
188
- const revision = z.object({ file: z.union([file, z.string()]), uploadedBy: z.object({ _id: z.string(), username: z.string() }) });
189
- const schema = z.object({ attachments: z.array(revision.extend({ history: z.array(revision).optional(), title: z.string().optional() })).optional() });
190
- const select = buildSelectObject(schema);
191
- const revisionSelect = { file: { _id: true, contentType: true, filename: true }, uploadedBy: { _id: true, username: true } };
192
- const isTyped = true;
193
- assert.ok(isTyped);
194
- assert.deepStrictEqual(select, { attachments: { ...revisionSelect, history: revisionSelect, title: true } });
195
- });
196
186
  it('should be a select the schema accepts', () => {
197
187
  const schema = z.object({
198
188
  client: z.object({ _id: z.string(), name: z.string() }),
@@ -1,8 +1,8 @@
1
1
  import { core, z, ZodAny, ZodArray, ZodBoolean, ZodDate, ZodLiteral, ZodNever, ZodNullable, ZodNumber, ZodObject, ZodOptional, ZodRawShape, ZodString, ZodTuple, ZodType, ZodUnion, ZodUnknown } from 'zod';
2
2
  import { IsAny, IsJsonType } from './types.js';
3
- type ApplyOptionalNullable<T, U extends ZodType> = IsAny<T, U, undefined extends T ? null extends T ? ZodOptional<ZodNullable<U>> : ZodOptional<U> : null extends T ? ZodNullable<U> : U>;
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
- 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 ? ZodNumber : T extends boolean ? ZodBoolean : 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<{
5
+ 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
6
  [K in keyof T]-?: Zodify<T[K], NextDepth<TDepth>>;
7
7
  }, core.$strict>, ZodType<T>> : never>;
8
8
  type IsTuple<T, TTrue = true, TFalse = false> = T extends [infer TFirst, infer TSecond, ...infer TRest] ? TTrue : TFalse;
@@ -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 S) => ZodType ? ReturnType<TShape[K]> : TShape[K] extends ZodType ? TShape[K] : TShape[K] extends boolean ? Zodify<T[K]> : TShape[K] extends object ? Exclude<T[K], null | undefined> extends Array<infer U> ? U extends object ? ApplyOptionalNullable<T[K], z.ZodArray<RefinedTypeSchema<U, TShape[K], NextDepth<TDepth>>>> : Zodify<T[K]> : Exclude<T[K], null | undefined> extends object ? ApplyOptionalNullable<T[K], RefinedTypeSchema<Exclude<T[K], null | undefined>, TShape[K], NextDepth<TDepth>>> : ZodNever : ZodNever;
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> ? U extends object ? ApplyOptionalNullable<T[K], z.ZodArray<RefinedTypeSchema<U, TShape[K], NextDepth<TDepth>>>> : Zodify<T[K]> : Exclude<T[K], null | undefined> extends object ? ApplyOptionalNullable<T[K], RefinedTypeSchema<Exclude<T[K], null | undefined>, TShape[K], NextDepth<TDepth>>> : ZodNever : 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>>,
@@ -36,8 +36,10 @@ export type TuplifyUnion<T, TDepth extends number = 0, L = LastOf<T>, N = [T] ex
36
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
- export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T, IsAny<T, ZodAny, [
40
- Exclude<T, null | undefined>
41
- ] extends [string] ? string extends Exclude<T, null | undefined> ? ZodString : ZodLiteral<Exclude<T, null | undefined>> : IsTuple<TuplifyUnion<Exclude<T, null | undefined>, TDepth>, ZodUnion<TuplifyUnion<Exclude<T, null | undefined>, TDepth>>, InternalZodify<Exclude<T, null | undefined>, TDepth>>>>;
39
+ export type Zodify<T, TDepth extends number = 0> = ApplyOptionalNullable<T, IsAny<T, ZodAny, ZodifyLiteral<Exclude<T, null | undefined>, IsTuple<TuplifyUnion<Exclude<T, null | undefined>, TDepth>, ZodUnion<TuplifyUnion<Exclude<T, null | undefined>, TDepth>>, InternalZodify<Exclude<T, null | undefined>, TDepth>>>>>;
40
+ type ZodifyField<T, TKey extends keyof T> = unknown extends T[TKey] ? Partial<Pick<T, TKey>> extends Pick<T, TKey> ? ZodOptional<Zodify<T[TKey]>> : Zodify<T[TKey]> : Zodify<T[TKey]>;
41
+ type ZodifyLiteral<T, TElse> = [
42
+ T
43
+ ] 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;
42
44
  export declare function refineSchema<T extends ZodType<object> | ZodUnion, TShape extends RefineSchema<T, false>>(schema: T, shape: TShape): RefinedSchema<T, TShape, false>;
43
45
  export {};
@@ -356,3 +356,46 @@ describe('.refineSchema()', () => {
356
356
  deepStrictEqual(selected.map(s => refined.parse(s).stage), stage.options);
357
357
  });
358
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zodselect",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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",