zodvex 0.2.5 → 0.3.2
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 +319 -7
- package/dist/index.d.ts +177 -66
- package/dist/index.js +122 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -16
- package/src/__type-tests__/infer-returns.ts +154 -0
- package/src/__type-tests__/zodTable-inference.ts +476 -0
- package/src/custom.ts +15 -6
- package/src/ids.ts +8 -8
- package/src/registry.ts +171 -0
- package/src/tables.ts +238 -30
- package/src/types.ts +10 -22
- package/src/wrappers.ts +2 -2
package/package.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodvex",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Zod <=> Convex integration, supporting Zod 4",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"zod",
|
|
7
|
-
"convex",
|
|
8
|
-
"validators",
|
|
9
|
-
"codec",
|
|
10
|
-
"mapping",
|
|
11
|
-
"schema",
|
|
12
|
-
"validation"
|
|
13
|
-
],
|
|
5
|
+
"keywords": ["zod", "convex", "validators", "codec", "mapping", "schema", "validation"],
|
|
14
6
|
"homepage": "https://github.com/panzacoder/zodvex#readme",
|
|
15
7
|
"bugs": {
|
|
16
8
|
"url": "https://github.com/panzacoder/zodvex/issues"
|
|
@@ -31,12 +23,7 @@
|
|
|
31
23
|
"default": "./dist/index.js"
|
|
32
24
|
}
|
|
33
25
|
},
|
|
34
|
-
"files": [
|
|
35
|
-
"dist",
|
|
36
|
-
"src",
|
|
37
|
-
"README.md",
|
|
38
|
-
"LICENSE"
|
|
39
|
-
],
|
|
26
|
+
"files": ["dist", "src", "README.md", "LICENSE"],
|
|
40
27
|
"scripts": {
|
|
41
28
|
"build": "tsup",
|
|
42
29
|
"dev": "bun run tsup --watch",
|
|
@@ -57,6 +44,7 @@
|
|
|
57
44
|
"@biomejs/biome": "^2.2.6",
|
|
58
45
|
"@types/bun": "^1.3.0",
|
|
59
46
|
"@types/node": "^24.8.0",
|
|
47
|
+
"ai": "^6.0.6",
|
|
60
48
|
"tsup": "^8.5.0",
|
|
61
49
|
"typescript": "^5.9.3"
|
|
62
50
|
},
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type tests for InferReturns (Issue #19)
|
|
3
|
+
*
|
|
4
|
+
* These assertions cause TypeScript errors if types don't match expectations.
|
|
5
|
+
* This file is type-checked but not included in the bundle.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import type { InferReturns } from '../types'
|
|
9
|
+
|
|
10
|
+
// Test helper: causes TS error if assigned `any`
|
|
11
|
+
declare function expectNotAny<T>(value: 0 extends 1 & T ? never : T): void
|
|
12
|
+
|
|
13
|
+
// --- Simple schemas should infer correctly ---
|
|
14
|
+
|
|
15
|
+
declare const stringResult: InferReturns<z.ZodString>
|
|
16
|
+
expectNotAny(stringResult)
|
|
17
|
+
|
|
18
|
+
declare const objectResult: InferReturns<ReturnType<typeof z.object<{ name: z.ZodString }>>>
|
|
19
|
+
expectNotAny(objectResult)
|
|
20
|
+
|
|
21
|
+
// --- Union schemas: this is the Issue #19 bug ---
|
|
22
|
+
// If InferReturns bails to `any` for unions, these will fail
|
|
23
|
+
|
|
24
|
+
declare const unionSchema: z.ZodUnion<[z.ZodString, z.ZodNumber]>
|
|
25
|
+
declare const unionResult: InferReturns<typeof unionSchema>
|
|
26
|
+
// @ts-expect-error - If this errors with "unused directive", Result is `any` (the bug)
|
|
27
|
+
const _unionInvalid: typeof unionResult = { notStringOrNumber: true }
|
|
28
|
+
|
|
29
|
+
declare const literalUnionSchema: z.ZodUnion<[z.ZodLiteral<'a'>, z.ZodLiteral<'b'>]>
|
|
30
|
+
declare const literalUnionResult: InferReturns<typeof literalUnionSchema>
|
|
31
|
+
// @ts-expect-error - If this errors with "unused directive", Result is `any` (the bug)
|
|
32
|
+
const _literalInvalid: typeof literalUnionResult = { notAOrB: true }
|
|
33
|
+
|
|
34
|
+
// --- Complex/deeply nested schemas (stress tests for TS depth limits) ---
|
|
35
|
+
// These tests ensure removing bailouts doesn't cause "Type instantiation is
|
|
36
|
+
// excessively deep and possibly infinite" errors.
|
|
37
|
+
|
|
38
|
+
// Deeply nested object (5 levels)
|
|
39
|
+
const deeplyNestedSchema = z.object({
|
|
40
|
+
level1: z.object({
|
|
41
|
+
level2: z.object({
|
|
42
|
+
level3: z.object({
|
|
43
|
+
level4: z.object({
|
|
44
|
+
level5: z.object({
|
|
45
|
+
value: z.string()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
declare const deeplyNestedResult: InferReturns<typeof deeplyNestedSchema>
|
|
53
|
+
expectNotAny(deeplyNestedResult)
|
|
54
|
+
|
|
55
|
+
// Large discriminated union (10 variants) - common in real apps
|
|
56
|
+
const largeDiscriminatedUnion = z.discriminatedUnion('type', [
|
|
57
|
+
z.object({ type: z.literal('variant1'), data: z.string() }),
|
|
58
|
+
z.object({ type: z.literal('variant2'), count: z.number() }),
|
|
59
|
+
z.object({ type: z.literal('variant3'), flag: z.boolean() }),
|
|
60
|
+
z.object({ type: z.literal('variant4'), items: z.array(z.string()) }),
|
|
61
|
+
z.object({ type: z.literal('variant5'), nested: z.object({ a: z.number() }) }),
|
|
62
|
+
z.object({ type: z.literal('variant6'), opt: z.string().optional() }),
|
|
63
|
+
z.object({ type: z.literal('variant7'), nullable: z.string().nullable() }),
|
|
64
|
+
z.object({ type: z.literal('variant8'), tuple: z.tuple([z.string(), z.number()]) }),
|
|
65
|
+
z.object({ type: z.literal('variant9'), record: z.record(z.string(), z.number()) }),
|
|
66
|
+
z.object({ type: z.literal('variant10'), union: z.union([z.string(), z.number()]) })
|
|
67
|
+
])
|
|
68
|
+
declare const largeUnionResult: InferReturns<typeof largeDiscriminatedUnion>
|
|
69
|
+
expectNotAny(largeUnionResult)
|
|
70
|
+
|
|
71
|
+
// Nested unions (union containing objects with union fields)
|
|
72
|
+
const nestedUnionSchema = z.object({
|
|
73
|
+
outer: z.union([
|
|
74
|
+
z.object({
|
|
75
|
+
kind: z.literal('a'),
|
|
76
|
+
inner: z.union([z.literal('x'), z.literal('y')])
|
|
77
|
+
}),
|
|
78
|
+
z.object({
|
|
79
|
+
kind: z.literal('b'),
|
|
80
|
+
inner: z.union([z.literal('p'), z.literal('q')])
|
|
81
|
+
})
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
declare const nestedUnionResult: InferReturns<typeof nestedUnionSchema>
|
|
85
|
+
expectNotAny(nestedUnionResult)
|
|
86
|
+
|
|
87
|
+
// Array of unions
|
|
88
|
+
const arrayOfUnionsSchema = z.array(
|
|
89
|
+
z.union([
|
|
90
|
+
z.object({ type: z.literal('item'), value: z.string() }),
|
|
91
|
+
z.object({ type: z.literal('group'), children: z.array(z.string()) })
|
|
92
|
+
])
|
|
93
|
+
)
|
|
94
|
+
declare const arrayOfUnionsResult: InferReturns<typeof arrayOfUnionsSchema>
|
|
95
|
+
expectNotAny(arrayOfUnionsResult)
|
|
96
|
+
|
|
97
|
+
// Real-world pattern: API response with polymorphic data
|
|
98
|
+
const apiResponseSchema = z.object({
|
|
99
|
+
success: z.boolean(),
|
|
100
|
+
data: z
|
|
101
|
+
.union([
|
|
102
|
+
z.object({
|
|
103
|
+
type: z.literal('user'),
|
|
104
|
+
id: z.string(),
|
|
105
|
+
name: z.string(),
|
|
106
|
+
email: z.string().email(),
|
|
107
|
+
roles: z.array(z.enum(['admin', 'user', 'guest']))
|
|
108
|
+
}),
|
|
109
|
+
z.object({
|
|
110
|
+
type: z.literal('organization'),
|
|
111
|
+
id: z.string(),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
members: z.array(
|
|
114
|
+
z.object({
|
|
115
|
+
userId: z.string(),
|
|
116
|
+
role: z.enum(['owner', 'admin', 'member'])
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
}),
|
|
120
|
+
z.object({
|
|
121
|
+
type: z.literal('error'),
|
|
122
|
+
code: z.number(),
|
|
123
|
+
message: z.string(),
|
|
124
|
+
details: z.record(z.string(), z.unknown()).optional()
|
|
125
|
+
})
|
|
126
|
+
])
|
|
127
|
+
.nullable(),
|
|
128
|
+
meta: z.object({
|
|
129
|
+
timestamp: z.number(),
|
|
130
|
+
requestId: z.string()
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
declare const apiResponseResult: InferReturns<typeof apiResponseSchema>
|
|
134
|
+
expectNotAny(apiResponseResult)
|
|
135
|
+
|
|
136
|
+
// Recursive-like pattern (not truly recursive, but deep)
|
|
137
|
+
const treeNodeSchema = z.object({
|
|
138
|
+
id: z.string(),
|
|
139
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
140
|
+
children: z.array(
|
|
141
|
+
z.object({
|
|
142
|
+
id: z.string(),
|
|
143
|
+
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
144
|
+
children: z.array(
|
|
145
|
+
z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
value: z.union([z.string(), z.number(), z.boolean()])
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
declare const treeNodeResult: InferReturns<typeof treeNodeSchema>
|
|
154
|
+
expectNotAny(treeNodeResult)
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type tests for zodTable inference (Type Regression Fix)
|
|
3
|
+
*
|
|
4
|
+
* These assertions cause TypeScript errors if zodTable returns `any` or
|
|
5
|
+
* fails to preserve proper type information.
|
|
6
|
+
* This file is type-checked but not included in the bundle.
|
|
7
|
+
*/
|
|
8
|
+
import type { GenericId } from 'convex/values'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import { zid } from '../ids'
|
|
11
|
+
import { zodTable } from '../tables'
|
|
12
|
+
|
|
13
|
+
// Test helper: causes TS error if assigned `any`
|
|
14
|
+
declare function expectNotAny<T>(value: 0 extends 1 & T ? never : T): void
|
|
15
|
+
|
|
16
|
+
// Test helper: causes TS error if types don't match
|
|
17
|
+
declare function expectType<T>(value: T): void
|
|
18
|
+
|
|
19
|
+
// --- Test 1: Basic table is not `any` ---
|
|
20
|
+
|
|
21
|
+
const BasicTable = zodTable('basic', { name: z.string() })
|
|
22
|
+
expectNotAny(BasicTable)
|
|
23
|
+
expectNotAny(BasicTable.table)
|
|
24
|
+
expectNotAny(BasicTable.shape)
|
|
25
|
+
expectNotAny(BasicTable.zDoc)
|
|
26
|
+
expectNotAny(BasicTable.docArray)
|
|
27
|
+
|
|
28
|
+
// --- Test 2: Shape preserves field types ---
|
|
29
|
+
|
|
30
|
+
type BasicShape = typeof BasicTable.shape
|
|
31
|
+
// Shape['name'] should be z.ZodString, not any
|
|
32
|
+
declare const nameField: BasicShape['name']
|
|
33
|
+
expectNotAny(nameField)
|
|
34
|
+
|
|
35
|
+
// Verify the type is actually ZodString
|
|
36
|
+
declare function expectZodString(v: z.ZodString): void
|
|
37
|
+
expectZodString(BasicTable.shape.name)
|
|
38
|
+
|
|
39
|
+
// --- Test 3: zDoc includes system fields ---
|
|
40
|
+
|
|
41
|
+
type BasicDocShape = typeof BasicTable.zDoc extends z.ZodObject<infer S> ? S : never
|
|
42
|
+
// Must have _id and _creationTime keys
|
|
43
|
+
declare const hasId: BasicDocShape['_id']
|
|
44
|
+
declare const hasCreationTime: BasicDocShape['_creationTime']
|
|
45
|
+
expectNotAny(hasId)
|
|
46
|
+
expectNotAny(hasCreationTime)
|
|
47
|
+
|
|
48
|
+
// --- Test 4: zDoc._output rejects invalid data ---
|
|
49
|
+
|
|
50
|
+
type BasicDoc = z.infer<typeof BasicTable.zDoc>
|
|
51
|
+
// @ts-expect-error - should fail if Doc is `any`
|
|
52
|
+
const _invalidDoc: BasicDoc = { completelyWrong: true }
|
|
53
|
+
|
|
54
|
+
// Valid doc should work
|
|
55
|
+
declare const validDoc: BasicDoc
|
|
56
|
+
expectNotAny(validDoc.name)
|
|
57
|
+
expectNotAny(validDoc._id)
|
|
58
|
+
expectNotAny(validDoc._creationTime)
|
|
59
|
+
|
|
60
|
+
// --- Test 5: Complex schema with optionals, arrays, nested objects ---
|
|
61
|
+
|
|
62
|
+
const ComplexTable = zodTable('complex', {
|
|
63
|
+
required: z.string(),
|
|
64
|
+
optional: z.string().optional(),
|
|
65
|
+
nullable: z.string().nullable(),
|
|
66
|
+
optionalNullable: z.string().optional().nullable(),
|
|
67
|
+
array: z.array(z.number()),
|
|
68
|
+
nested: z.object({
|
|
69
|
+
inner: z.string(),
|
|
70
|
+
deepNested: z.object({
|
|
71
|
+
value: z.boolean()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expectNotAny(ComplexTable)
|
|
77
|
+
expectNotAny(ComplexTable.shape)
|
|
78
|
+
expectNotAny(ComplexTable.zDoc)
|
|
79
|
+
|
|
80
|
+
type ComplexDoc = z.infer<typeof ComplexTable.zDoc>
|
|
81
|
+
declare const complexDoc: ComplexDoc
|
|
82
|
+
|
|
83
|
+
// Verify each field type is preserved
|
|
84
|
+
expectNotAny(complexDoc.required)
|
|
85
|
+
expectNotAny(complexDoc.optional)
|
|
86
|
+
expectNotAny(complexDoc.nullable)
|
|
87
|
+
expectNotAny(complexDoc.optionalNullable)
|
|
88
|
+
expectNotAny(complexDoc.array)
|
|
89
|
+
expectNotAny(complexDoc.nested)
|
|
90
|
+
expectNotAny(complexDoc.nested.inner)
|
|
91
|
+
expectNotAny(complexDoc.nested.deepNested)
|
|
92
|
+
expectNotAny(complexDoc.nested.deepNested.value)
|
|
93
|
+
|
|
94
|
+
// @ts-expect-error - should fail: required is string, not number
|
|
95
|
+
const _complexInvalid: ComplexDoc['required'] = 123
|
|
96
|
+
|
|
97
|
+
// --- Test 6: Table with zid references (common Convex pattern) ---
|
|
98
|
+
|
|
99
|
+
const _UsersTable = zodTable('users', {
|
|
100
|
+
name: z.string(),
|
|
101
|
+
email: z.string().email()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const PostsTable = zodTable('posts', {
|
|
105
|
+
title: z.string(),
|
|
106
|
+
content: z.string(),
|
|
107
|
+
authorId: zid('users'),
|
|
108
|
+
categoryId: zid('categories').optional()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expectNotAny(PostsTable)
|
|
112
|
+
expectNotAny(PostsTable.shape)
|
|
113
|
+
expectNotAny(PostsTable.zDoc)
|
|
114
|
+
|
|
115
|
+
type PostDoc = z.infer<typeof PostsTable.zDoc>
|
|
116
|
+
declare const postDoc: PostDoc
|
|
117
|
+
|
|
118
|
+
expectNotAny(postDoc.title)
|
|
119
|
+
expectNotAny(postDoc.authorId)
|
|
120
|
+
expectNotAny(postDoc.categoryId)
|
|
121
|
+
|
|
122
|
+
// @ts-expect-error - authorId should be GenericId<'users'>, not any
|
|
123
|
+
const _postInvalid: PostDoc = { title: 'test', content: 'test', authorId: 123 }
|
|
124
|
+
|
|
125
|
+
// --- Test 7: Spread operator preserves types (the actual symptom) ---
|
|
126
|
+
|
|
127
|
+
declare const docToSpread: z.infer<typeof BasicTable.zDoc>
|
|
128
|
+
const spread = { ...docToSpread, extra: 'field' }
|
|
129
|
+
expectNotAny(spread._id)
|
|
130
|
+
expectNotAny(spread.name)
|
|
131
|
+
expectNotAny(spread._creationTime)
|
|
132
|
+
|
|
133
|
+
// The spread should have proper types
|
|
134
|
+
// @ts-expect-error - _id should be GenericId<'basic'>, not accept random object
|
|
135
|
+
const _spreadInvalid: typeof spread._id = { notAnId: true }
|
|
136
|
+
|
|
137
|
+
// --- Test 8: docArray preserves document types ---
|
|
138
|
+
|
|
139
|
+
type BasicDocArray = z.infer<typeof BasicTable.docArray>
|
|
140
|
+
declare const docArray: BasicDocArray
|
|
141
|
+
|
|
142
|
+
expectNotAny(docArray)
|
|
143
|
+
expectNotAny(docArray[0])
|
|
144
|
+
expectNotAny(docArray[0].name)
|
|
145
|
+
expectNotAny(docArray[0]._id)
|
|
146
|
+
|
|
147
|
+
// @ts-expect-error - array element should have proper type
|
|
148
|
+
const _arrayInvalid: BasicDocArray = [{ notValid: true }]
|
|
149
|
+
|
|
150
|
+
// --- Test 9: Multiple tables don't interfere with each other ---
|
|
151
|
+
|
|
152
|
+
const TableA = zodTable('tableA', { fieldA: z.string() })
|
|
153
|
+
const TableB = zodTable('tableB', { fieldB: z.number() })
|
|
154
|
+
|
|
155
|
+
expectNotAny(TableA.shape.fieldA)
|
|
156
|
+
expectNotAny(TableB.shape.fieldB)
|
|
157
|
+
|
|
158
|
+
// Each table's zDoc should be properly typed
|
|
159
|
+
type DocA = z.infer<typeof TableA.zDoc>
|
|
160
|
+
type DocB = z.infer<typeof TableB.zDoc>
|
|
161
|
+
|
|
162
|
+
declare const docA: DocA
|
|
163
|
+
declare const docB: DocB
|
|
164
|
+
|
|
165
|
+
// @ts-expect-error - docA should not have fieldB
|
|
166
|
+
const _aHasB: typeof docA.fieldB = 'test'
|
|
167
|
+
|
|
168
|
+
// @ts-expect-error - docB should not have fieldA
|
|
169
|
+
const _bHasA: typeof docB.fieldA = 123
|
|
170
|
+
|
|
171
|
+
// --- Test 10: GenericId types are preserved correctly ---
|
|
172
|
+
|
|
173
|
+
type BasicDocId = z.infer<typeof BasicTable.zDoc>['_id']
|
|
174
|
+
// Should be GenericId<'basic'>
|
|
175
|
+
declare function expectGenericId<T extends string>(id: GenericId<T>): void
|
|
176
|
+
declare const basicId: BasicDocId
|
|
177
|
+
expectGenericId(basicId)
|
|
178
|
+
|
|
179
|
+
// --- Test 11: Union schema support still works ---
|
|
180
|
+
|
|
181
|
+
const UnionTable = zodTable(
|
|
182
|
+
'shapes',
|
|
183
|
+
z.union([
|
|
184
|
+
z.object({ kind: z.literal('circle'), radius: z.number() }),
|
|
185
|
+
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() })
|
|
186
|
+
])
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
expectNotAny(UnionTable)
|
|
190
|
+
expectNotAny(UnionTable.table)
|
|
191
|
+
expectNotAny(UnionTable.schema)
|
|
192
|
+
expectNotAny(UnionTable.docArray)
|
|
193
|
+
|
|
194
|
+
// --- Test 12: Table.doc validator returns properly typed document, not any ---
|
|
195
|
+
// This is the ACTUAL bug: ReturnType<typeof Table<any, TableName>> causes Table properties to be any
|
|
196
|
+
|
|
197
|
+
// The .doc property is a Convex validator - check its inferred type
|
|
198
|
+
type DocValidatorType = typeof BasicTable.doc
|
|
199
|
+
// If Table<any, ...> is used, DocValidatorType will have `any` in its type params
|
|
200
|
+
expectNotAny({} as DocValidatorType)
|
|
201
|
+
|
|
202
|
+
// --- Test 13: Table.withoutSystemFields should preserve field types ---
|
|
203
|
+
|
|
204
|
+
type WithoutSystemFields = typeof BasicTable.withoutSystemFields
|
|
205
|
+
expectNotAny({} as WithoutSystemFields)
|
|
206
|
+
|
|
207
|
+
// --- Test 14: Table.withSystemFields should not be any ---
|
|
208
|
+
// Check that withSystemFields preserves proper field structure
|
|
209
|
+
|
|
210
|
+
type WithSystemFields = typeof BasicTable.withSystemFields
|
|
211
|
+
expectNotAny({} as WithSystemFields)
|
|
212
|
+
|
|
213
|
+
// --- Test 15: doc validator should have properly typed fields, not index signature any ---
|
|
214
|
+
// The bug causes VObject<{ [x: string]: any; ... }> instead of VObject<{ name: ... }>
|
|
215
|
+
|
|
216
|
+
type DocValidator = typeof BasicTable.doc
|
|
217
|
+
// Check that the doc validator type doesn't have any in its structure
|
|
218
|
+
// This is a compile-time check - if any propagates, this file fails to compile
|
|
219
|
+
declare const docValidator: DocValidator
|
|
220
|
+
expectNotAny(docValidator)
|
|
221
|
+
|
|
222
|
+
// --- Test 16: Discriminated union schema support ---
|
|
223
|
+
|
|
224
|
+
const DiscriminatedUnionTable = zodTable(
|
|
225
|
+
'events',
|
|
226
|
+
z.discriminatedUnion('type', [
|
|
227
|
+
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
|
|
228
|
+
z.object({ type: z.literal('scroll'), offset: z.number() }),
|
|
229
|
+
z.object({ type: z.literal('keypress'), key: z.string() })
|
|
230
|
+
])
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
expectNotAny(DiscriminatedUnionTable)
|
|
234
|
+
expectNotAny(DiscriminatedUnionTable.table)
|
|
235
|
+
expectNotAny(DiscriminatedUnionTable.schema)
|
|
236
|
+
expectNotAny(DiscriminatedUnionTable.docArray)
|
|
237
|
+
|
|
238
|
+
// --- Test 17: Enum fields preserve literal types ---
|
|
239
|
+
|
|
240
|
+
const EnumTable = zodTable('statuses', {
|
|
241
|
+
status: z.enum(['pending', 'active', 'completed', 'archived']),
|
|
242
|
+
priority: z.enum(['low', 'medium', 'high'])
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expectNotAny(EnumTable)
|
|
246
|
+
expectNotAny(EnumTable.shape)
|
|
247
|
+
expectNotAny(EnumTable.shape.status)
|
|
248
|
+
expectNotAny(EnumTable.shape.priority)
|
|
249
|
+
|
|
250
|
+
type EnumDoc = z.infer<typeof EnumTable.zDoc>
|
|
251
|
+
declare const enumDoc: EnumDoc
|
|
252
|
+
expectNotAny(enumDoc.status)
|
|
253
|
+
expectNotAny(enumDoc.priority)
|
|
254
|
+
|
|
255
|
+
// @ts-expect-error - status should only accept enum values, not arbitrary strings
|
|
256
|
+
const _enumInvalid: EnumDoc['status'] = 'invalid_status'
|
|
257
|
+
|
|
258
|
+
// --- Test 18: Default values don't break inference ---
|
|
259
|
+
|
|
260
|
+
const DefaultsTable = zodTable('defaults', {
|
|
261
|
+
name: z.string().default('unnamed'),
|
|
262
|
+
count: z.number().default(0),
|
|
263
|
+
active: z.boolean().default(true)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
expectNotAny(DefaultsTable)
|
|
267
|
+
expectNotAny(DefaultsTable.shape)
|
|
268
|
+
expectNotAny(DefaultsTable.zDoc)
|
|
269
|
+
|
|
270
|
+
type DefaultsDoc = z.infer<typeof DefaultsTable.zDoc>
|
|
271
|
+
declare const defaultsDoc: DefaultsDoc
|
|
272
|
+
expectNotAny(defaultsDoc.name)
|
|
273
|
+
expectNotAny(defaultsDoc.count)
|
|
274
|
+
expectNotAny(defaultsDoc.active)
|
|
275
|
+
|
|
276
|
+
// --- Test 19: Empty shape edge case ---
|
|
277
|
+
|
|
278
|
+
const EmptyTable = zodTable('empty', {})
|
|
279
|
+
expectNotAny(EmptyTable)
|
|
280
|
+
expectNotAny(EmptyTable.table)
|
|
281
|
+
expectNotAny(EmptyTable.zDoc)
|
|
282
|
+
|
|
283
|
+
// Empty table should still have system fields
|
|
284
|
+
type EmptyDoc = z.infer<typeof EmptyTable.zDoc>
|
|
285
|
+
declare const emptyDoc: EmptyDoc
|
|
286
|
+
expectNotAny(emptyDoc._id)
|
|
287
|
+
expectNotAny(emptyDoc._creationTime)
|
|
288
|
+
|
|
289
|
+
// =============================================================================
|
|
290
|
+
// UNION TABLE TYPE TESTS
|
|
291
|
+
// These tests check for type preservation in the union schema overload
|
|
292
|
+
// =============================================================================
|
|
293
|
+
|
|
294
|
+
// --- Test 20: Union table docArray should preserve variant types ---
|
|
295
|
+
|
|
296
|
+
const ShapesTable = zodTable(
|
|
297
|
+
'shapes',
|
|
298
|
+
z.union([
|
|
299
|
+
z.object({ kind: z.literal('circle'), radius: z.number() }),
|
|
300
|
+
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() })
|
|
301
|
+
])
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// The docArray should infer proper document types with system fields
|
|
305
|
+
type ShapeDocArray = z.infer<typeof ShapesTable.docArray>
|
|
306
|
+
declare const shapeDocArray: ShapeDocArray
|
|
307
|
+
|
|
308
|
+
// TODO: This test currently passes but the inferred type is very loose (ZodTypeAny)
|
|
309
|
+
// Ideally, shapeDocArray[0] should have discriminated union fields
|
|
310
|
+
expectNotAny(shapeDocArray)
|
|
311
|
+
|
|
312
|
+
// --- Test 21: Union table withSystemFields() should preserve variant types ---
|
|
313
|
+
// BUG: addSystemFields returns z.ZodTypeAny, causing type loss
|
|
314
|
+
|
|
315
|
+
const shapeWithFields = ShapesTable.withSystemFields()
|
|
316
|
+
|
|
317
|
+
// BUG DETECTION: withSystemFields returns ZodTypeAny instead of preserving the union type
|
|
318
|
+
// z.infer<z.ZodTypeAny> = any, which breaks type safety
|
|
319
|
+
type ShapeDocFromWithFields = z.infer<typeof shapeWithFields>
|
|
320
|
+
|
|
321
|
+
// This should detect if the type is any
|
|
322
|
+
expectNotAny({} as ShapeDocFromWithFields)
|
|
323
|
+
|
|
324
|
+
// More specific test: the document should have the union variant fields + system fields
|
|
325
|
+
// If withSystemFields worked correctly, we should be able to access kind, radius, etc.
|
|
326
|
+
declare const shapeDoc: ShapeDocFromWithFields
|
|
327
|
+
|
|
328
|
+
// BUG: These should error with "Property does not exist" if type is any
|
|
329
|
+
// because any accepts all property accesses without error
|
|
330
|
+
// @ts-expect-error - if this is unused, shapeDoc is any (the bug)
|
|
331
|
+
const _testAnyAccess: typeof shapeDoc.thisPropertyShouldNotExist = 'test'
|
|
332
|
+
|
|
333
|
+
// Direct test: what is z.infer<z.ZodTypeAny>?
|
|
334
|
+
type DirectZodTypeAnyInfer = z.infer<z.ZodTypeAny>
|
|
335
|
+
// If this is any, the next line will fail
|
|
336
|
+
expectNotAny({} as DirectZodTypeAnyInfer)
|
|
337
|
+
|
|
338
|
+
// Test: check if addSystemFields return type loses union info
|
|
339
|
+
type AddSystemFieldsReturn = ReturnType<typeof ShapesTable.withSystemFields>
|
|
340
|
+
// @ts-expect-error - if unused, the return type accepts all assignments (is any or unknown)
|
|
341
|
+
const _addSystemFieldsTest: AddSystemFieldsReturn = 'this should not be assignable to a Zod schema'
|
|
342
|
+
|
|
343
|
+
// Debug types show:
|
|
344
|
+
// - DirectZodTypeAnyInfer = unknown (not any!)
|
|
345
|
+
// - ShapeDocFromWithFields = unknown
|
|
346
|
+
// - AddSystemFieldsReturn = ZodType<unknown, ...>
|
|
347
|
+
// In Zod v4, z.infer<z.ZodTypeAny> = unknown, which still loses type info
|
|
348
|
+
|
|
349
|
+
// Test helper to detect unknown type (different from any)
|
|
350
|
+
declare function expectNotUnknown<T>(value: unknown extends T ? never : T): void
|
|
351
|
+
|
|
352
|
+
// BUG DETECTION: These fail because types degrade to `unknown`
|
|
353
|
+
// When fixed, these should pass (types should be the actual union)
|
|
354
|
+
expectNotUnknown({} as ShapeDocFromWithFields)
|
|
355
|
+
|
|
356
|
+
// Test: Does the docArray lose type info?
|
|
357
|
+
type DocArrayElement = ShapeDocArray[number]
|
|
358
|
+
// BUG: DocArrayElement is `unknown`, should be the union type with system fields
|
|
359
|
+
expectNotUnknown({} as DocArrayElement)
|
|
360
|
+
|
|
361
|
+
// --- Test 22: Union table schema property preserves original schema type ---
|
|
362
|
+
// This should work - schema property is directly typed as Schema
|
|
363
|
+
|
|
364
|
+
type ShapesSchema = typeof ShapesTable.schema
|
|
365
|
+
expectNotAny({} as ShapesSchema)
|
|
366
|
+
|
|
367
|
+
// The schema should be the original union, not any
|
|
368
|
+
type ShapesSchemaOutput = z.infer<ShapesSchema>
|
|
369
|
+
declare const shapesOutput: ShapesSchemaOutput
|
|
370
|
+
|
|
371
|
+
// This SHOULD error because shapesOutput is a union type, not any
|
|
372
|
+
// @ts-expect-error - should fail if schema output is any
|
|
373
|
+
const _shapesOutputInvalid: ShapesSchemaOutput = { notAShape: true }
|
|
374
|
+
|
|
375
|
+
// --- Test 23: Discriminated union table type preservation ---
|
|
376
|
+
|
|
377
|
+
const EventsTable = zodTable(
|
|
378
|
+
'events',
|
|
379
|
+
z.discriminatedUnion('type', [
|
|
380
|
+
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
|
|
381
|
+
z.object({ type: z.literal('scroll'), offset: z.number() })
|
|
382
|
+
])
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
// The schema should preserve discriminated union type
|
|
386
|
+
type EventsSchema = typeof EventsTable.schema
|
|
387
|
+
expectNotAny({} as EventsSchema)
|
|
388
|
+
|
|
389
|
+
type EventsOutput = z.infer<EventsSchema>
|
|
390
|
+
declare const eventsOutput: EventsOutput
|
|
391
|
+
|
|
392
|
+
// This SHOULD error because eventsOutput is a discriminated union, not any
|
|
393
|
+
// @ts-expect-error - should fail if output is any
|
|
394
|
+
const _eventsInvalid: EventsOutput = { notAnEvent: 123 }
|
|
395
|
+
|
|
396
|
+
// The discriminator should work
|
|
397
|
+
declare const clickEvent: EventsOutput & { type: 'click' }
|
|
398
|
+
expectNotAny(clickEvent.x)
|
|
399
|
+
expectNotAny(clickEvent.y)
|
|
400
|
+
|
|
401
|
+
// =============================================================================
|
|
402
|
+
// STRUCTURAL TESTS FOR UNION/DU TYPE INFERENCE
|
|
403
|
+
// These verify the actual structure is correct, not just "not any/unknown"
|
|
404
|
+
// =============================================================================
|
|
405
|
+
|
|
406
|
+
// --- Test 24: Union docArray elements have system fields ---
|
|
407
|
+
|
|
408
|
+
type ShapeDocElement = z.infer<typeof ShapesTable.docArray>[number]
|
|
409
|
+
declare const shapeElement: ShapeDocElement
|
|
410
|
+
|
|
411
|
+
// System fields should exist on union doc elements
|
|
412
|
+
expectNotAny(shapeElement._id)
|
|
413
|
+
expectNotAny(shapeElement._creationTime)
|
|
414
|
+
|
|
415
|
+
// --- Test 25: Union variant fields are accessible ---
|
|
416
|
+
|
|
417
|
+
// For a union, we should be able to access the common discriminator
|
|
418
|
+
// Note: 'kind' exists on both variants
|
|
419
|
+
expectNotAny(shapeElement.kind)
|
|
420
|
+
|
|
421
|
+
// --- Test 26: Discriminated union narrowing works ---
|
|
422
|
+
|
|
423
|
+
type EventDoc = z.infer<typeof EventsTable.docArray>[number]
|
|
424
|
+
declare const eventDoc: EventDoc
|
|
425
|
+
|
|
426
|
+
// Before narrowing, variant-specific fields should not be directly accessible
|
|
427
|
+
// (they exist on some variants but not all)
|
|
428
|
+
|
|
429
|
+
// After narrowing by discriminator, variant fields should be accessible
|
|
430
|
+
function _handleEvent(event: EventDoc) {
|
|
431
|
+
if (event.type === 'click') {
|
|
432
|
+
// After narrowing, x and y should be accessible
|
|
433
|
+
const x: number = event.x
|
|
434
|
+
const y: number = event.y
|
|
435
|
+
return { x, y }
|
|
436
|
+
} else if (event.type === 'scroll') {
|
|
437
|
+
// After narrowing, offset should be accessible
|
|
438
|
+
const offset: number = event.offset
|
|
439
|
+
return { offset }
|
|
440
|
+
}
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- Test 27: Union docs reject invalid variants ---
|
|
445
|
+
|
|
446
|
+
// @ts-expect-error - missing required 'kind' discriminator
|
|
447
|
+
const _invalidShapeDoc1: ShapeDocElement = { _id: '' as any, _creationTime: 0 }
|
|
448
|
+
|
|
449
|
+
// @ts-expect-error - 'kind' value doesn't match any variant
|
|
450
|
+
const _invalidShapeDoc2: ShapeDocElement = {
|
|
451
|
+
kind: 'triangle' as any, // not a valid variant
|
|
452
|
+
_id: '' as any,
|
|
453
|
+
_creationTime: 0
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// --- Test 28: Discriminated union docs have proper system field types ---
|
|
457
|
+
|
|
458
|
+
// _id should be GenericId<'events'>, not just any string
|
|
459
|
+
type EventDocId = EventDoc['_id']
|
|
460
|
+
declare const eventId: EventDocId
|
|
461
|
+
expectNotAny(eventId)
|
|
462
|
+
declare function expectGenericIdEvents(id: GenericId<'events'>): void
|
|
463
|
+
expectGenericIdEvents(eventId)
|
|
464
|
+
|
|
465
|
+
// --- Test 29: withSystemFields() result has variant fields accessible ---
|
|
466
|
+
|
|
467
|
+
const eventsWithFields = EventsTable.withSystemFields()
|
|
468
|
+
type EventWithFields = z.infer<typeof eventsWithFields>
|
|
469
|
+
declare const eventWithFields: EventWithFields
|
|
470
|
+
|
|
471
|
+
// Should have system fields
|
|
472
|
+
expectNotAny(eventWithFields._id)
|
|
473
|
+
expectNotAny(eventWithFields._creationTime)
|
|
474
|
+
|
|
475
|
+
// Should have discriminator
|
|
476
|
+
expectNotAny(eventWithFields.type)
|