zodvex 0.7.5 → 0.7.6
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 +1 -0
- package/dist/internal/db.d.ts +1 -1
- package/dist/internal/db.d.ts.map +1 -1
- package/dist/internal/stream.d.ts +110 -0
- package/dist/internal/stream.d.ts.map +1 -0
- package/dist/mini/server/index.js +54 -2
- package/dist/mini/server/index.js.map +1 -1
- package/dist/public/server/index.d.ts +1 -0
- package/dist/public/server/index.d.ts.map +1 -1
- package/dist/server/index.js +54 -2
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/internal/db.ts +1 -1
- package/src/internal/stream.ts +242 -0
- package/src/public/server/index.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zodvex",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Codec-first Zod v4 integration for Convex -- type-safe validation, encoding, DB wrapping, and codegen.",
|
|
5
5
|
"keywords": ["zod", "convex", "validators", "codec", "mapping", "schema", "validation"],
|
|
6
6
|
"homepage": "https://github.com/panzacoder/zodvex#readme",
|
package/src/internal/db.ts
CHANGED
|
@@ -408,7 +408,7 @@ export class ZodvexQueryChain<TableInfo extends GenericTableInfo, Doc = Document
|
|
|
408
408
|
* If the table has a decoded type in DecodedDocs, use it.
|
|
409
409
|
* Otherwise fall back to DocumentByInfo (wire types = runtime types for tables without codecs).
|
|
410
410
|
*/
|
|
411
|
-
type ResolveDecodedDoc<
|
|
411
|
+
export type ResolveDecodedDoc<
|
|
412
412
|
DataModel extends GenericDataModel,
|
|
413
413
|
DecodedDocs extends Record<string, any>,
|
|
414
414
|
TableName extends TableNamesInDataModel<DataModel>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataModelFromSchemaDefinition,
|
|
3
|
+
DocumentByInfo,
|
|
4
|
+
GenericDatabaseReader,
|
|
5
|
+
GenericTableInfo,
|
|
6
|
+
IndexNames,
|
|
7
|
+
IndexRange,
|
|
8
|
+
NamedIndex,
|
|
9
|
+
NamedTableInfo,
|
|
10
|
+
SchemaDefinition,
|
|
11
|
+
TableNamesInDataModel
|
|
12
|
+
} from 'convex/server'
|
|
13
|
+
import { mergedStream, type QueryStream, stream } from 'convex-helpers/server/stream'
|
|
14
|
+
import type { ResolveDecodedDoc, ZodvexDatabaseReader, ZodvexIndexRangeBuilder } from './db'
|
|
15
|
+
import type { ZodTableMap } from './schema'
|
|
16
|
+
import {
|
|
17
|
+
$ZodCodec,
|
|
18
|
+
$ZodDefault,
|
|
19
|
+
$ZodNullable,
|
|
20
|
+
$ZodObject,
|
|
21
|
+
$ZodOptional,
|
|
22
|
+
$ZodType,
|
|
23
|
+
$ZodUnion
|
|
24
|
+
} from './zod-core'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Typed convex-helpers stream interop for the secure DatabaseReader (#78).
|
|
28
|
+
*
|
|
29
|
+
* convex-helpers' `stream()` / `mergedStream()` enable honest pagination over
|
|
30
|
+
* set-valued equality predicates (`roomId IN {a, b, c}`): fan out one
|
|
31
|
+
* substream per value over an index and k-way merge them with index-key
|
|
32
|
+
* cursors that stay valid across all substreams.
|
|
33
|
+
*
|
|
34
|
+
* `stream()` nominally requires a `GenericDatabaseReader`, which the zodvex
|
|
35
|
+
* secure reader doesn't implement (its `query()` returns the decoded chain).
|
|
36
|
+
* This module provides the typed entry point so call sites never cast:
|
|
37
|
+
*
|
|
38
|
+
* - The unavoidable cast lives in ONE audited place (`zodvexStream`), pinned
|
|
39
|
+
* by tests that exercise the duck-typed surface stream() relies on
|
|
40
|
+
* (`db.query(table).withIndex(...).order(...)` + async iteration), so a
|
|
41
|
+
* chain-surface change fails loudly in zodvex CI rather than silently
|
|
42
|
+
* downstream.
|
|
43
|
+
* - Item/page types are the DECODED doc types (codec outputs applied), which
|
|
44
|
+
* is what the zodvex chain actually yields — not convex-helpers' raw
|
|
45
|
+
* `DocumentByName` types.
|
|
46
|
+
*
|
|
47
|
+
* Rules semantics: streams are rules-preserving. Every streamed row flows
|
|
48
|
+
* through the secure chain (decode + read rules evaluated per row
|
|
49
|
+
* mid-stream). If a read rule denies a row inside a substream, the row is
|
|
50
|
+
* simply never yielded — the merged index-key cursor never includes it, so
|
|
51
|
+
* there are no holes and no stuck cursors. Rules act as a backstop; fan-out
|
|
52
|
+
* queries should already narrow to authorized index ranges.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Convex stream items must be non-nullish (mirrors convex-helpers' unexported GenericStreamItem). */
|
|
56
|
+
type StreamItem = NonNullable<unknown>
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A stream of DECODED documents. Alias of convex-helpers' `QueryStream` with
|
|
60
|
+
* the item type corrected to what the zodvex chain actually yields, so
|
|
61
|
+
* `paginate()` / `collect()` / iteration return decoded docs.
|
|
62
|
+
*/
|
|
63
|
+
export type ZodvexQueryStream<T extends StreamItem> = QueryStream<T>
|
|
64
|
+
|
|
65
|
+
/** Ordered stream over decoded documents — mergeable and paginatable. */
|
|
66
|
+
export interface ZodvexOrderedStreamQuery<T extends StreamItem> extends QueryStream<T> {}
|
|
67
|
+
|
|
68
|
+
/** Stream query with an index applied; `.order()` yields the mergeable stream. */
|
|
69
|
+
export interface ZodvexStreamQuery<T extends StreamItem> extends QueryStream<T> {
|
|
70
|
+
order(order: 'asc' | 'desc'): ZodvexOrderedStreamQuery<T>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Entry point for a single table's stream. Mirrors convex-helpers'
|
|
75
|
+
* `StreamQueryInitializer` surface, but `withIndex` uses zodvex's
|
|
76
|
+
* decoded-aware index range builder (codec fields accept decoded values,
|
|
77
|
+
* e.g. Date) and terminal types are decoded docs.
|
|
78
|
+
*/
|
|
79
|
+
export interface ZodvexStreamQueryInitializer<
|
|
80
|
+
TableInfo extends GenericTableInfo,
|
|
81
|
+
T extends StreamItem
|
|
82
|
+
> extends ZodvexStreamQuery<T> {
|
|
83
|
+
fullTableScan(): ZodvexStreamQuery<T>
|
|
84
|
+
withIndex<IndexName extends IndexNames<TableInfo>>(
|
|
85
|
+
indexName: IndexName,
|
|
86
|
+
indexRange?: (
|
|
87
|
+
q: ZodvexIndexRangeBuilder<DocumentByInfo<TableInfo>, T, NamedIndex<TableInfo, IndexName>>
|
|
88
|
+
) => IndexRange
|
|
89
|
+
): ZodvexStreamQuery<T>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A defineZodSchema() result — a Convex schema carrying the zod table map. */
|
|
93
|
+
export type ZodvexStreamableSchema = SchemaDefinition<any, boolean> & {
|
|
94
|
+
__zodTableMap: ZodTableMap
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Extracts the phantom decoded-doc map carried by defineZodSchema results. */
|
|
98
|
+
type DecodedDocsOf<Schema> = Schema extends { __decodedDocs: infer DD extends Record<string, any> }
|
|
99
|
+
? DD
|
|
100
|
+
: Record<string, any>
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The stream-flavored secure reader: same fluent surface as convex-helpers'
|
|
104
|
+
* `stream(db, schema)`, typed against decoded documents.
|
|
105
|
+
*/
|
|
106
|
+
export interface ZodvexStreamDatabaseReader<Schema extends ZodvexStreamableSchema> {
|
|
107
|
+
query<TableName extends TableNamesInDataModel<DataModelFromSchemaDefinition<Schema>>>(
|
|
108
|
+
tableName: TableName
|
|
109
|
+
): ZodvexStreamQueryInitializer<
|
|
110
|
+
NamedTableInfo<DataModelFromSchemaDefinition<Schema>, TableName>,
|
|
111
|
+
NonNullable<
|
|
112
|
+
ResolveDecodedDoc<DataModelFromSchemaDefinition<Schema>, DecodedDocsOf<Schema>, TableName>
|
|
113
|
+
>
|
|
114
|
+
>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a typed convex-helpers stream over the zodvex secure reader —
|
|
119
|
+
* no cast at call sites.
|
|
120
|
+
*
|
|
121
|
+
* Every streamed row flows through the secure chain: codec decode plus any
|
|
122
|
+
* read rules / audit wrappers attached to the reader. Use with
|
|
123
|
+
* `zodvexMergedStream` to paginate fan-out queries over set-valued equality
|
|
124
|
+
* predicates.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const substreams = rooms.map(roomId =>
|
|
129
|
+
* zodvexStream(ctx.db, schema)
|
|
130
|
+
* .query('visits')
|
|
131
|
+
* .withIndex('tenantId_roomId_status', q => q.eq('tenantId', tenantId).eq('roomId', roomId))
|
|
132
|
+
* .order('asc')
|
|
133
|
+
* )
|
|
134
|
+
* return zodvexMergedStream(substreams, ['status', '_creationTime']).paginate(opts)
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function zodvexStream<Schema extends ZodvexStreamableSchema>(
|
|
138
|
+
db: ZodvexDatabaseReader<DataModelFromSchemaDefinition<Schema>, any>,
|
|
139
|
+
schema: Schema
|
|
140
|
+
): ZodvexStreamDatabaseReader<Schema> {
|
|
141
|
+
// THE one audited cast (#78). The secure reader doesn't nominally implement
|
|
142
|
+
// GenericDatabaseReader, but it is duck-type compatible with the surface
|
|
143
|
+
// stream() uses: db.query(table).withIndex(index, range).order(order) plus
|
|
144
|
+
// async iteration. That surface is pinned by __tests__/stream.test.ts.
|
|
145
|
+
const rawDb = db as unknown as GenericDatabaseReader<DataModelFromSchemaDefinition<Schema>>
|
|
146
|
+
return stream(rawDb, schema) as unknown as ZodvexStreamDatabaseReader<Schema>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Merge multiple zodvex streams into a single stream ordered by
|
|
151
|
+
* `orderByIndexFields`, suitable for `.paginate()` with cursors that stay
|
|
152
|
+
* valid across all substreams. Thin wrapper over convex-helpers'
|
|
153
|
+
* `mergedStream` that keeps decoded item types and rejects codec-backed
|
|
154
|
+
* merge keys.
|
|
155
|
+
*
|
|
156
|
+
* Codec-backed fields are FORBIDDEN in `orderByIndexFields`: the merge
|
|
157
|
+
* comparator reads index-key values off the yielded (decoded) documents,
|
|
158
|
+
* while the underlying Convex index is ordered by wire values — decoded
|
|
159
|
+
* comparisons can mis-order the merge and produce invalid cursors. Pin codec
|
|
160
|
+
* fields with `.eq()` inside each substream and order by non-codec fields
|
|
161
|
+
* (e.g. '_creationTime') instead.
|
|
162
|
+
*/
|
|
163
|
+
export function zodvexMergedStream<T extends StreamItem>(
|
|
164
|
+
streams: ZodvexQueryStream<T>[],
|
|
165
|
+
orderByIndexFields: string[]
|
|
166
|
+
): ZodvexQueryStream<T> {
|
|
167
|
+
assertNoCodecOrderFields(streams, orderByIndexFields)
|
|
168
|
+
return mergedStream(streams, orderByIndexFields)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Unwraps Optional/Nullable/Default wrappers to reach the structural type. */
|
|
172
|
+
function unwrapOuter(schema: $ZodType): $ZodType {
|
|
173
|
+
let current: $ZodType = schema
|
|
174
|
+
for (let i = 0; i < 10; i++) {
|
|
175
|
+
if (
|
|
176
|
+
current instanceof $ZodOptional ||
|
|
177
|
+
current instanceof $ZodNullable ||
|
|
178
|
+
current instanceof $ZodDefault
|
|
179
|
+
) {
|
|
180
|
+
current = current._zod.def.innerType
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
return current
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Walks a (possibly dot-separated) field path through a doc schema and
|
|
190
|
+
* reports whether it crosses a codec boundary — i.e. whether the decoded
|
|
191
|
+
* value at that path differs from the wire value the index is ordered by.
|
|
192
|
+
* Union doc schemas (union tables) flag the field if ANY variant is
|
|
193
|
+
* codec-backed.
|
|
194
|
+
*/
|
|
195
|
+
function fieldIsCodecBacked(schema: $ZodType, segments: string[]): boolean {
|
|
196
|
+
const current = unwrapOuter(schema)
|
|
197
|
+
if (current instanceof $ZodCodec) return true
|
|
198
|
+
if (segments.length === 0) return false
|
|
199
|
+
if (current instanceof $ZodObject) {
|
|
200
|
+
const fieldSchema = (current._zod.def.shape as Record<string, $ZodType | undefined>)[
|
|
201
|
+
segments[0]
|
|
202
|
+
]
|
|
203
|
+
return fieldSchema ? fieldIsCodecBacked(fieldSchema, segments.slice(1)) : false
|
|
204
|
+
}
|
|
205
|
+
if (current instanceof $ZodUnion) {
|
|
206
|
+
const options = current._zod.def.options as readonly $ZodType[]
|
|
207
|
+
return options.some(option => fieldIsCodecBacked(option, segments))
|
|
208
|
+
}
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Best-effort guard: for every stream that exposes `reflect()` (any stream
|
|
214
|
+
* built via zodvexStream(...).query(...)), look up the table's doc schema in
|
|
215
|
+
* the zodvex table map and reject codec-backed `orderByIndexFields`.
|
|
216
|
+
* Derived streams (filterWith/map) don't reflect and pass through unchecked.
|
|
217
|
+
*/
|
|
218
|
+
function assertNoCodecOrderFields(
|
|
219
|
+
streams: ZodvexQueryStream<any>[],
|
|
220
|
+
orderByIndexFields: string[]
|
|
221
|
+
): void {
|
|
222
|
+
for (const s of streams) {
|
|
223
|
+
const reflect = (s as { reflect?: () => { schema?: unknown; table?: string } }).reflect
|
|
224
|
+
if (typeof reflect !== 'function') continue
|
|
225
|
+
const { schema, table } = reflect.call(s) ?? {}
|
|
226
|
+
const tableMap = (schema as { __zodTableMap?: ZodTableMap } | undefined)?.__zodTableMap
|
|
227
|
+
const docSchema = table ? tableMap?.[table]?.doc : undefined
|
|
228
|
+
if (!docSchema) continue
|
|
229
|
+
for (const field of orderByIndexFields) {
|
|
230
|
+
if (field === '_creationTime' || field === '_id') continue
|
|
231
|
+
if (fieldIsCodecBacked(docSchema, field.split('.'))) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`zodvexMergedStream: orderByIndexFields contains codec-backed field '${field}' ` +
|
|
234
|
+
`(table '${table}'). The merge comparator reads decoded values off yielded docs, ` +
|
|
235
|
+
`but the underlying index is ordered by wire values — codec-backed merge keys can ` +
|
|
236
|
+
`mis-order results and produce invalid cursors. Pin codec fields with .eq() inside ` +
|
|
237
|
+
`each substream and order by non-codec fields (e.g. '_creationTime') instead.`
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -77,5 +77,16 @@ export type {
|
|
|
77
77
|
export * from '../../internal/schema'
|
|
78
78
|
// Schema helpers (pure Zod, no server deps)
|
|
79
79
|
export { addSystemFields } from '../../internal/schemaHelpers'
|
|
80
|
+
// Typed convex-helpers stream interop for the secure reader (#78)
|
|
81
|
+
export {
|
|
82
|
+
type ZodvexOrderedStreamQuery,
|
|
83
|
+
type ZodvexQueryStream,
|
|
84
|
+
type ZodvexStreamableSchema,
|
|
85
|
+
type ZodvexStreamDatabaseReader,
|
|
86
|
+
type ZodvexStreamQuery,
|
|
87
|
+
type ZodvexStreamQueryInitializer,
|
|
88
|
+
zodvexMergedStream,
|
|
89
|
+
zodvexStream
|
|
90
|
+
} from '../../internal/stream'
|
|
80
91
|
// Table creation and helpers (named — hide union internals)
|
|
81
92
|
export { zodDoc, zodDocOrNull, zodTable } from '../../legacy/tables'
|