zodvex 0.7.6 → 0.7.7-beta.1

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.
Files changed (47) hide show
  1. package/README.md +55 -107
  2. package/dist/cli/index.d.ts +1 -1
  3. package/dist/cli/index.js +34 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/client/index.js +3 -5
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/codegen/index.js +33 -3
  8. package/dist/codegen/index.js.map +1 -1
  9. package/dist/core/index.js +38 -8
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/index.js +38 -8
  12. package/dist/index.js.map +1 -1
  13. package/dist/internal/boundaryHelpers.d.ts.map +1 -1
  14. package/dist/internal/codec.d.ts +4 -0
  15. package/dist/internal/codec.d.ts.map +1 -1
  16. package/dist/internal/custom.d.ts.map +1 -1
  17. package/dist/internal/registry.d.ts +7 -2
  18. package/dist/internal/registry.d.ts.map +1 -1
  19. package/dist/legacy/index.js +6 -2
  20. package/dist/legacy/index.js.map +1 -1
  21. package/dist/mini/client/index.js +3 -5
  22. package/dist/mini/client/index.js.map +1 -1
  23. package/dist/mini/index.js +38 -8
  24. package/dist/mini/index.js.map +1 -1
  25. package/dist/mini/react/index.js +3 -5
  26. package/dist/mini/react/index.js.map +1 -1
  27. package/dist/mini/server/index.js +20 -9
  28. package/dist/mini/server/index.js.map +1 -1
  29. package/dist/public/cli/index.d.ts +1 -1
  30. package/dist/public/codegen/discover.d.ts.map +1 -1
  31. package/dist/public/codegen/discovery-hooks.d.ts.map +1 -1
  32. package/dist/react/index.js +3 -5
  33. package/dist/react/index.js.map +1 -1
  34. package/dist/server/index.js +20 -9
  35. package/dist/server/index.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/cli/index.ts +1 -1
  38. package/src/internal/boundaryHelpers.ts +4 -6
  39. package/src/internal/codec.ts +35 -2
  40. package/src/internal/custom.ts +11 -2
  41. package/src/internal/db.ts +1 -1
  42. package/src/internal/mapping/types.ts +2 -2
  43. package/src/internal/registry.ts +47 -3
  44. package/src/internal/types.ts +2 -2
  45. package/src/public/cli/index.ts +1 -1
  46. package/src/public/codegen/discover.ts +14 -0
  47. package/src/public/codegen/discovery-hooks.ts +31 -5
package/README.md CHANGED
@@ -2,43 +2,33 @@
2
2
 
3
3
  #### [Zod](https://zod.dev/) + [Convex](https://www.convex.dev/)
4
4
 
5
- Type-safe Convex functions with Zod v4 schemas. Codecs in the schema`zx.date()`, custom transformations plus typed IDs, all wired up through `initZodvex`.
6
-
7
- > Built on top of [convex-helpers](https://github.com/get-convex/convex-helpers)
8
-
9
- ## Table of Contents
10
-
11
- - [Why zodvex?](#why-zodvex)
12
- - [Installation](#installation)
13
- - [Quick Start](#quick-start)
14
- - [Import Paths](#import-paths)
15
- - [Features](#features)
16
- - [Codec-Aware Database](#codec-aware-database)
17
- - [Codegen & Client-Side Schema Sharing](#codegen--client-side-schema-sharing)
18
- - [Using zodvex Without Codecs](#using-zodvex-without-codecs)
19
- - [Supported Types](#supported-types)
20
- - [Upgrading?](#upgrading)
21
- - [API Reference](#api-reference)
22
- - [Roadmap](#roadmap)
23
- - [License](#license)
5
+ Use Zod v4 as your schema language for Convex define your data once and use it end to end, with automatic validation and codecs at every boundary.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/zodvex)](https://www.npmjs.com/package/zodvex)
8
+ [![CI](https://github.com/panzacoder/zodvex/actions/workflows/ci.yml/badge.svg)](https://github.com/panzacoder/zodvex/actions/workflows/ci.yml)
9
+ [![license](https://img.shields.io/npm/l/zodvex)](./LICENSE)
24
10
 
25
11
  ## Why zodvex?
26
12
 
27
- - **Codec-aware database** — `zx.date()`, `zx.codec()` encode/decode automatically at `ctx.db` boundaries
28
- - **Correct optional/nullable semantics** — preserves Convex's distinction (`.optional()` → `v.optional(T)`, `.nullable()` → `v.union(T, v.null())`)
29
- - **Client-safe models** — `defineZodModel` is importable in React
30
- - **End-to-end type safety** — same schema from database to frontend forms
13
+ **Your Zod schemas are the source of truth** — tables, function arguments, and return types defined once, used database to frontend. Two things make that real:
14
+
15
+ - **Functions run full Zod pipelines.** Your argument and return schemas execute as real Zod at runtime refinements like `.min()` and `.email()`, transformations, codecs — not erased down to structural checks.
16
+
17
+ - **The database is Zod-validated, automatically.** `ctx.db` parses every read and encodes every write through your schemas: `.email()` holds at the row level where Convex's structural checks stop, and codecs live in the schema itself — handlers see `Date` objects and branded IDs while Convex stores plain values.
31
18
 
32
- | Feature | zodvex | convex-helpers/zod4 |
33
- |---------|--------|---------------------|
34
- | Codec-aware DB | `initZodvex` wraps `ctx.db` automatically | Manual |
35
- | Date handling | `zx.date()` — automatic Date ↔ timestamp | Manual `z.codec()` |
36
- | Typed IDs | `zx.id('table')` with type branding | Manual |
37
- | Custom codecs | `zx.codec()` with auto-detection | Not provided |
38
- | Client-safe models | `defineZodModel` (importable in React) | Not provided |
39
- | Codegen | Optional typed hooks, boundary helpers | Not provided |
19
+ What you gain over Convex out of the box:
40
20
 
41
- Both are valid choices zodvex trades some explicitness for significantly better ergonomics.
21
+ | | Convex | with zodvex |
22
+ |---|---|---|
23
+ | Type safety | end-to-end inference | same — driven by your Zod schemas |
24
+ | Runtime validation | structural checks | full Zod — refinements, transforms, codecs |
25
+ | End-to-end validation | per-function validators | one Zod schema — client, functions, and db access |
26
+ | Client calls | `useQuery(api.fn)` infers types | `useZodQuery(api.fn)` infers the runtime schema too — args encoded, results decoded (via codegen) |
27
+ | Dates & custom types | `number` timestamps | `Date` objects and custom codecs (`zx.date()`, `zx.codec()`) |
28
+ | Rows on read | as stored | parsed & decoded through your schema |
29
+ | Row-level rules & audit | build your own | `.withRules()` / `.audit()` on `ctx.db` |
30
+
31
+ All of it wired once with `initZodvex` — see [Features](#features).
42
32
 
43
33
  ## Installation
44
34
 
@@ -72,7 +62,9 @@ export const EventModel = defineZodModel('events', {
72
62
 
73
63
  > `defineZodModel` is client-safe — you can import it in React.
74
64
 
75
- ### Step 2. Build your schema
65
+ ### Step 2. Wire it up
66
+
67
+ Two one-time files: build your Convex schema from your models, and create your function builders.
76
68
 
77
69
  ```ts
78
70
  // convex/schema.ts
@@ -84,8 +76,6 @@ export default defineZodSchema({
84
76
  })
85
77
  ```
86
78
 
87
- ### Step 3. Set up builders
88
-
89
79
  ```ts
90
80
  // convex/functions.ts
91
81
  import { initZodvex } from 'zodvex/server'
@@ -101,9 +91,9 @@ export const { zq, zm, za, ziq, zim, zia } = initZodvex(schema, {
101
91
  })
102
92
  ```
103
93
 
104
- `initZodvex` returns builders for all six Convex function types. By default, `zq` and `zm` (and internal variants) wrap `ctx.db` with automatic codec encode/decode.
94
+ `initZodvex` returns builders for all six Convex function types, with `ctx.db` wrapped for automatic codec encode/decode. You write these two files once and rarely touch them again.
105
95
 
106
- ### Step 4. Write functions — Date conversion Just Works
96
+ ### Step 3. Write functions
107
97
 
108
98
  ```ts
109
99
  // convex/events.ts
@@ -114,7 +104,7 @@ import { EventModel } from './models'
114
104
 
115
105
  export const list = zq({
116
106
  args: {},
117
- returns: EventModel.schema.docArray,
107
+ returns: zx.docArray(EventModel),
118
108
  handler: async (ctx) => {
119
109
  // Dates come back as Date objects, not numbers
120
110
  return await ctx.db.query('events').collect()
@@ -136,39 +126,52 @@ export const create = zm({
136
126
  })
137
127
  ```
138
128
 
139
- > `zx.id('events')` is a typed Convex ID validator — it provides type branding for `GenericId<'events'>` but is NOT a codec (no wire transformation happens). `zx.date()` and `zx.codec()` ARE codecs.
129
+ > `zx.id('events')` is a typed ID validator — branding only, no wire transform. `zx.date()` and `zx.codec()` are codecs. Details in [Features](#features).
140
130
 
141
131
  See the full [quickstart example](./examples/quickstart/) for a runnable project.
142
132
 
143
133
  ## Import Paths
144
134
 
145
- Four entry points:
135
+ Four primary entry points:
146
136
 
147
137
  - **`zodvex`** — Client-safe full-Zod surface. Use in React components and shared code.
148
138
  - **`zodvex/server`** — Server-only. Use in Convex functions and schema definitions.
149
139
  - **`zodvex/mini`** — Client-safe zod/mini surface.
150
140
  - **`zodvex/mini/server`** — Server-only zod/mini surface.
151
- - **`zodvex/legacy`** — Deprecated runtime APIs kept only for migration.
152
141
 
153
- > `zodvex/core` remains as a deprecated compatibility alias for `zodvex`.
142
+ Focused entrypoints — `zodvex/react`, `zodvex/client`, `zodvex/codegen` (and their `zodvex/mini` variants) back the codegen workflow and are mostly consumed by generated code; see [Codegen](#codegen--client-side-schema-sharing).
143
+
144
+ Two deprecated paths remain for migration only:
145
+
146
+ - **`zodvex/legacy`** — Deprecated runtime APIs kept only for migration.
147
+ - **`zodvex/core`** — Deprecated compatibility alias for `zodvex`.
154
148
 
155
149
  ## Features
156
150
 
157
151
  ### Codec-Aware Database
158
152
 
159
- `initZodvex` wraps `ctx.db` so reads decode automatically and writes encode automatically.
153
+ `initZodvex` wraps `ctx.db` so reads decode automatically and writes encode automatically. Codecs are opt-in per field — schemas without them get the same validation, typed IDs, and correct optional/nullable mapping through the exact same setup.
160
154
 
161
155
  - **`zx.date()`** — Date ↔ timestamp codec. Stored as `v.float64()`, used as `Date` in handlers.
162
156
  - **`zx.codec(wire, runtime, transforms)`** — Custom codecs for complex transformations (encryption, serialization, etc.).
163
- - **`zx.id('table')`** — Typed Convex ID validator with `GenericId<T>` branding. This is NOT a codec — no wire transformation happens.
157
+ - **`zx.id('table')`** — Typed Convex ID validator with `GenericId<T>` branding — no wire transform (not a codec).
164
158
 
165
159
  Guides: [Custom Codecs](./docs/guide/custom-codecs.md), [Date Handling](./docs/guide/date-handling.md)
166
160
 
161
+ ### Row-Level Rules & Audit
162
+
163
+ The same wrapped `ctx.db` carries per-row security and observability — both operating on decoded documents (`Date` objects, typed IDs), never wire values:
164
+
165
+ - **`.withRules(ruleCtx, rules)`** — gate and transform reads and writes per table (`read`, `insert`, `patch`, `replace`, `delete`), with an optional deny-by-default policy.
166
+ - **`.audit({ afterRead, afterWrite })`** — observe successful reads and writes; composes with `.withRules()`, so audit sees only what the rules allowed.
167
+
168
+ Guide: [Rules & Audit](./docs/guide/rules-and-audit.md)
169
+
167
170
  ### Codegen & Client-Side Schema Sharing
168
171
 
169
172
  zodvex includes an optional CLI that generates typed client code:
170
173
 
171
- - **Typed hooks** — `useZodQuery`, `useZodMutation` with automatic codec decode
174
+ - **Typed hooks** — `useZodQuery`, `useZodMutation`, generated into `convex/_zodvex/client` import them from there; args are encoded and results decoded automatically
172
175
  - **Boundary helpers** — `encodeArgs`, `decodeResult` for custom client integrations
173
176
  - **Cross-function auto-codec** — `ctx.runQuery` / `ctx.runMutation` encode args + decode results, and `ctx.scheduler.runAfter` / `ctx.scheduler.runAt` encode args, via the registry. Pass natural decoded values; zodvex encodes them to wire at the call site.
174
177
 
@@ -183,20 +186,6 @@ zodvex dev # watch mode
183
186
 
184
187
  Guides: [Codegen](./docs/guide/codegen.md) | Example: [examples/task-manager/](./examples/task-manager/)
185
188
 
186
- ### Using zodvex Without Codecs
187
-
188
- `zodTable` and `zQueryBuilder` still work without `initZodvex`:
189
-
190
- ```ts
191
- import { zodTable, zQueryBuilder } from 'zodvex/legacy'
192
- import { query } from './_generated/server'
193
-
194
- const Users = zodTable('users', { name: z.string() })
195
- const zq = zQueryBuilder(query)
196
- ```
197
-
198
- At this level, zodvex is roughly equivalent to convex-helpers. This is a valid stepping-stone, but the API is deprecated. When you're ready for codecs, see the [Quick Start](#quick-start) above.
199
-
200
189
  ## Supported Types
201
190
 
202
191
  | Zod Type | Convex Validator |
@@ -217,52 +206,13 @@ At this level, zodvex is roughly equivalent to convex-helpers. This is a valid s
217
206
 
218
207
  > **Note:** Native `z.date()` is **not supported** — use `zx.date()` instead. See [Date Handling](./docs/guide/date-handling.md) for details.
219
208
 
220
- **Zod v4 Enum Type Note:**
221
-
222
- ¹ Enum types in Zod v4 produce a slightly different TypeScript signature than manually created unions:
223
-
224
- ```typescript
225
- // Manual union (precise tuple type)
226
- const manual = v.union(v.literal('a'), v.literal('b'))
227
- // Type: VUnion<"a" | "b", [VLiteral<"a", "required">, VLiteral<"b", "required">], "required", never>
228
-
229
- // From Zod enum (array type)
230
- const fromZod = zodToConvex(z.enum(['a', 'b']))
231
- // Type: VUnion<"a" | "b", Array<VLiteral<"a" | "b", "required">>, "required", never>
232
- ```
233
-
234
- **This difference is purely cosmetic with no functional impact:**
235
-
236
- - Value types are identical (`"a" | "b"`)
237
- - Runtime validation is identical
238
- - Type safety for function arguments/returns is preserved
239
- - Convex uses `T[number]` which works identically for both array and tuple types
209
+ ¹ Enum unions carry a cosmetically different TypeScript signature than hand-written `v.union(v.literal(...))` calls — identical values, runtime validation, and type safety. Details: [Mapping Helpers](./docs/guide/mapping-helpers.md#zod-v4-enum-type-note).
240
210
 
241
- This limitation exists because Zod v4 changed enum types from tuple-based to Record-based ([`ToEnum<T>`](https://github.com/colinhacks/zod/blob/v4/src/v4/core/util.ts#L83-L85)). TypeScript cannot convert a Record type to a specific tuple without knowing the keys at compile time. See [Zod v4 changelog](https://zod.dev/v4/changelog) and [enum evolution discussion](https://github.com/colinhacks/zod/discussions/2125) for more details.
242
-
243
- **zx namespace types:**
244
-
245
- ```ts
246
- import { zx } from 'zodvex'
247
-
248
- // Convex IDs — typed validator, NOT a codec
249
- zx.id('tableName') // → v.id('tableName')
250
- zx.id('tableName').optional() // → v.optional(v.id('tableName'))
251
-
252
- // Dates — codec (Date ↔ timestamp)
253
- zx.date() // → v.float64() (timestamp)
254
- zx.date().optional() // → v.optional(v.float64())
255
- zx.date().nullable() // → v.union(v.float64(), v.null())
256
-
257
- // Custom codecs
258
- zx.codec(wireSchema, runtimeSchema, { encode, decode })
259
- ```
211
+ For zodvex's own types `zx.id()`, `zx.date()`, `zx.codec()` and exactly how they map to Convex validators, see [The zx Namespace](./docs/guide/zx-namespace.md).
260
212
 
261
213
  ## Upgrading?
262
214
 
263
- > **Upgrading from a previous version?** Read the [migration guide](./docs/migration/v0.6.md) for what changed and why. Key takeaway: the CLI/codegen is optional — the Quick Start path above needs no codegen.
264
-
265
- Automated renames are available:
215
+ Read the [migration guide](./MIGRATION.md) for what changed in each release and why. Automated renames are available:
266
216
 
267
217
  ```bash
268
218
  npx zodvex migrate ./convex # apply renames
@@ -273,7 +223,8 @@ npx zodvex migrate ./convex --dry-run # preview changes
273
223
 
274
224
  - [The zx Namespace](./docs/guide/zx-namespace.md) — `zx.id()`, `zx.date()`, `zx.codec()`
275
225
  - [Builders](./docs/guide/builders.md) — `initZodvex` and legacy builders
276
- - [Custom Context](./docs/guide/custom-context.md) — `.withContext()`, `onSuccess`
226
+ - [Custom Context](./docs/guide/custom-context.md) — `.withContext()`, `defineContext`, `onSuccess`
227
+ - [Rules & Audit](./docs/guide/rules-and-audit.md) — `.withRules()`, `.audit()` on `ctx.db`
277
228
  - [Custom Codecs](./docs/guide/custom-codecs.md) — `zx.codec()`, `decodeDoc`/`encodeDoc`
278
229
  - [Date Handling](./docs/guide/date-handling.md) — `zx.date()` deep dive
279
230
  - [Form Validation](./docs/guide/form-validation.md) — react-hook-form integration
@@ -288,10 +239,7 @@ npx zodvex migrate ./convex --dry-run # preview changes
288
239
 
289
240
  ## Roadmap
290
241
 
291
- - Migration tooling: vanilla Convex zodvex (for new adopters with existing Convex projects)
292
- - Migration tooling: pre-0.5 → current zodvex
293
- - Additional example projects (e.g., full-stack with React, codegen showcase)
294
- - Per-feature READMEs in examples/task-manager/
242
+ See the [roadmap](./docs/roadmap.md) for where zodvex is heading deploy-scale performance, ecosystem interop, and the model/namespace evolution.
295
243
 
296
244
  ## License
297
245
 
@@ -299,4 +247,4 @@ MIT
299
247
 
300
248
  ---
301
249
 
302
- Built with ❤️ on top of [convex-helpers](https://github.com/get-convex/convex-helpers)
250
+ Built with ❤️ for the [Convex](https://www.convex.dev/) ecosystem. Thanks to [convex-helpers](https://github.com/get-convex/convex-helpers) for the early foundations.
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  export * from '../public/cli';
3
3
  import '../public/cli';
4
4
  //# sourceMappingURL=index.d.ts.map
package/dist/cli/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  import fs2, { readFileSync, writeFileSync, existsSync } from 'fs';
3
3
  import path3, { resolve, relative, join } from 'path';
4
4
  import { Project, SyntaxKind } from 'ts-morph';
@@ -1305,11 +1305,30 @@ function createSchemaUpdateSchema(name, inputSchema) {
1305
1305
  return inputSchema;
1306
1306
  }
1307
1307
  var HOOKS_SOURCE = `
1308
- export function resolve(specifier, context, nextResolve) {
1308
+ const EXT_CANDIDATES = ['.ts', '.tsx', '.mts', '.js', '.mjs', '.jsx', '/index.ts', '/index.js'];
1309
+
1310
+ export async function resolve(specifier, context, nextResolve) {
1309
1311
  if (/_generated\\/api(\\.[mc]?[jt]sx?)?$/.test(specifier)) {
1310
1312
  return { shortCircuit: true, url: 'zodvex-stub://api' };
1311
1313
  }
1312
- return nextResolve(specifier, context);
1314
+ try {
1315
+ return await nextResolve(specifier, context);
1316
+ } catch (err) {
1317
+ // Convex code uses extensionless relative imports (bundler resolution).
1318
+ // Bun resolves those natively; Node's ESM loader does not \u2014 retry with
1319
+ // the usual extension candidates before giving up.
1320
+ if (
1321
+ (specifier.startsWith('./') || specifier.startsWith('../')) &&
1322
+ !/\\.[cm]?[jt]sx?$/.test(specifier)
1323
+ ) {
1324
+ for (const ext of EXT_CANDIDATES) {
1325
+ try {
1326
+ return await nextResolve(specifier + ext, context);
1327
+ } catch {}
1328
+ }
1329
+ }
1330
+ throw err;
1331
+ }
1313
1332
  }
1314
1333
 
1315
1334
  export function load(url, context, nextLoad) {
@@ -1343,7 +1362,11 @@ var hooksRegistered = false;
1343
1362
  function registerDiscoveryHooks() {
1344
1363
  if (hooksRegistered) return true;
1345
1364
  try {
1346
- const { register } = __require("module");
1365
+ const nodeModule = typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:module") : (
1366
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1367
+ __require("module")
1368
+ );
1369
+ const { register } = nodeModule;
1347
1370
  if (typeof register !== "function") return false;
1348
1371
  register(`data:text/javascript,${encodeURIComponent(HOOKS_SOURCE)}`);
1349
1372
  hooksRegistered = true;
@@ -1572,6 +1595,7 @@ async function discoverModules(convexDir2) {
1572
1595
  "crons.js"
1573
1596
  ]
1574
1597
  }).sort();
1598
+ let importFailures = 0;
1575
1599
  try {
1576
1600
  for (const file of files) {
1577
1601
  const absPath = path3.resolve(convexDir2, file);
@@ -1579,6 +1603,7 @@ async function discoverModules(convexDir2) {
1579
1603
  try {
1580
1604
  moduleExports = await import(absPath);
1581
1605
  } catch (err) {
1606
+ importFailures++;
1582
1607
  console.warn(`[zodvex] Warning: Failed to import ${file}:`, err.message);
1583
1608
  continue;
1584
1609
  }
@@ -1646,6 +1671,11 @@ async function discoverModules(convexDir2) {
1646
1671
  modelCodecs.push(...found);
1647
1672
  }
1648
1673
  const functionCodecs = walkFunctionCodecs(functions);
1674
+ if (importFailures > 0 && models.length === 0 && functions.length === 0) {
1675
+ throw new Error(
1676
+ `[zodvex] Discovery imported 0 modules (${importFailures} file(s) failed to load). Importing TypeScript at runtime requires Bun or Node >= 22.18 (native type stripping). Re-run with Bun (\`bunx zodvex generate\`) or upgrade Node.`
1677
+ );
1678
+ }
1649
1679
  return { models, functions, codecs, modelCodecs, functionCodecs };
1650
1680
  } finally {
1651
1681
  cleanupStubs();