cvx-kit 0.1.14 → 0.1.16

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 (51) hide show
  1. package/README.md +64 -45
  2. package/dist/querying/cursors.d.mts +5 -0
  3. package/dist/querying/cursors.mjs +1 -1
  4. package/dist/querying/cursors.mjs.map +1 -1
  5. package/dist/querying.d.mts +20 -6
  6. package/dist/querying.mjs +1 -1
  7. package/dist/querying.mjs.map +1 -1
  8. package/dist/relations.mjs +1 -1
  9. package/dist/relations.mjs.map +1 -1
  10. package/dist/zod-table.d.mts +59 -2
  11. package/dist/zod-table.mjs +1 -1
  12. package/dist/zod-table.mjs.map +1 -1
  13. package/package.json +10 -3
  14. package/AGENTS.md +0 -47
  15. package/CHANGELOG.md +0 -324
  16. package/ROADMAP.md +0 -126
  17. package/docs/agent-tools.md +0 -41
  18. package/docs/approvals.md +0 -201
  19. package/docs/architecture.md +0 -124
  20. package/docs/auth.md +0 -443
  21. package/docs/command-context.md +0 -150
  22. package/docs/commands-legacy.md +0 -513
  23. package/docs/commands.md +0 -173
  24. package/docs/conventions.md +0 -345
  25. package/docs/crud.md +0 -66
  26. package/docs/domain-contracts.md +0 -148
  27. package/docs/domain-operations.md +0 -615
  28. package/docs/effect.md +0 -403
  29. package/docs/llms.md +0 -324
  30. package/docs/maintainability.md +0 -124
  31. package/docs/migration.md +0 -351
  32. package/docs/oxlint.md +0 -254
  33. package/docs/querying.md +0 -293
  34. package/docs/tenancy.md +0 -176
  35. package/docs/triggers.md +0 -77
  36. package/docs/upgrading.md +0 -287
  37. package/docs/webhooks.md +0 -58
  38. package/docs/zod-table.md +0 -278
  39. package/scripts/convex-config-bridge.mjs +0 -44
  40. package/scripts/convex-smoke-env.mjs +0 -33
  41. package/scripts/extract-changelog.mjs +0 -29
  42. package/scripts/owned-process.mjs +0 -140
  43. package/scripts/owned-process.test.mjs +0 -107
  44. package/scripts/smoke-packed-components.mjs +0 -341
  45. package/scripts/smoke-packed-effect.mjs +0 -439
  46. package/scripts/smoke-packed-querying.mjs +0 -296
  47. package/scripts/test-packed-auth.mjs +0 -72
  48. package/scripts/test-packed-effect.mjs +0 -482
  49. package/scripts/test-packed-ids.mjs +0 -150
  50. package/scripts/test-packed-oxlint.mjs +0 -167
  51. package/scripts/test-packed-querying.mjs +0 -185
package/docs/upgrading.md DELETED
@@ -1,287 +0,0 @@
1
- # Upgrading to 0.1.0 — the new way of things
2
-
3
- ## Current dependency requirements
4
-
5
- Install Convex 1.46 or newer and convex-helpers 0.1.127 or newer (below 0.2.0).
6
- These minimums match workflow 0.4.10. Regenerate your lockfile after updating.
7
- Auth declarations retain native Convex context types across package versions;
8
- the regression fixture also checks Convex 1.45, which is below the current
9
- full-package dependency minimum.
10
-
11
- ## Current command API — Effect declarations
12
-
13
- `Foundation.Command` and its separate `exec({ operation, handler })` API are
14
- deprecated. For new command code, use `createEffectFoundation` from
15
- `cvx-kit/effect`, declare handlers inside `command({...})`, and return
16
- `exec(operation, input, host)` through a shared Effect API builder.
17
- See [commands.md](./commands.md) for migration steps. Existing consumers remain
18
- compatible; older release sections below describe their historical APIs.
19
-
20
- ## 0.1.6 — stricter architecture checks
21
-
22
- Enable `project-structure`, `root-wiring-only`, `application-orchestration`,
23
- `internal-function-ownership`, and `domain-file-responsibilities` alongside the
24
- existing rules. These are opt-in additions; existing enabled boundary rules now
25
- also resolve tsconfig aliases and reject migration dependencies.
26
-
27
- Move root implementation folders into their owning domains. Keep `application/`
28
- limited to multi-domain coordination. Move centralized domain tests beside their
29
- owners and add real tests for nested executable directories. Do not create empty
30
- scaffold files. Replace copied structural architecture suites with the packaged
31
- checks; retain tests that exercise actual application behavior and wiring.
32
-
33
- ## 0.1.5 — consumer Oxlint rules
34
-
35
- The initial 0.1.4 plugin incorrectly targeted this package's `src/` layout.
36
- `cvx-kit/oxlint` now targets consuming applications under `convex/`, following
37
- `conventions.md`. The six existing rule names remain, with consumer semantics;
38
- nine additional rules cover builders, generated references, domain imports,
39
- public adapters, facade ownership, vocabularies, reads, and timestamps.
40
-
41
- Remove `entryPoints` and `packageName` options from your consumer lint config.
42
- Applications have no barrel entry points inside `convex/`. Every rule now
43
- accepts only an optional `convexDir`, relative to the linter's working directory
44
- (default: `convex`). Enable the new rules explicitly; loading the plugin alone
45
- does not enable them. See `oxlint.md` for coverage and the root README for a
46
- complete configuration. Repository authoring checks remain local tooling.
47
-
48
- No runtime API or storage migration is part of this correction.
49
-
50
- ## 0.1.4
51
-
52
- This release introduced the optional `cvx-kit/oxlint` export. Its original
53
- rules targeted library authoring; see the correction above for consumer use.
54
-
55
- This patch tightens several TypeScript contracts during the anti-slop cleanup:
56
-
57
- - Query middleware preserves the handler's result type. Return `next()` or
58
- throw; middleware that fabricates an unrelated result no longer typechecks.
59
- - `include().execute` and `.paginate` require a transform callback when
60
- requesting a different output type through an explicit type argument.
61
- - Audit metadata values must be serializable Convex `Value` values.
62
- - Webhook targets must be internal mutation references accepting
63
- `{ eventKey: string, payload: string, source: string }`.
64
- - CRUD enrichment derives from the table storage schema. Agent tools retain
65
- each supplied handler's input and result types.
66
-
67
- Run your application's typecheck after upgrading and update affected callbacks.
68
- No storage migration is required.
69
-
70
- ## 0.1.3
71
-
72
- Install `convex-helpers` as a direct peer, using one compatible instance:
73
-
74
- ```sh
75
- bun add cvx-kit@0.1.3 convex-helpers@0.1.124
76
- ```
77
-
78
- The peer range was `>=0.1.123 <0.2.0`. Remove overrides that force a
79
- separate helper copy and regenerate the installation without discarding
80
- unrelated dependency choices. This release is validated with Convex 1.45.0,
81
- Zod 4.5.4, and convex-test 0.0.56.
82
-
83
- Existing `Command.operation` and `Command.middleware` remain available.
84
- For callback inference, use `Command.withContext<MutationCtx>().operation`;
85
- see `commands.md` for typed middleware and transactional completion.
86
-
87
- Result transforms now run once, after middleware. A handler and middleware
88
- return the result schema's **input** type; audit, completion, and the executor
89
- receive its **output**. Middleware reading `await next()` sees the unparsed
90
- handler result. Remove workarounds that compensate for double transforms.
91
- When storing transformed outputs for replay, supply an output validator as
92
- `replayResult`; otherwise replay uses the ordinary result schema.
93
- Executor callers supply the command schema's input type; guards, prepare,
94
- middleware, and handlers receive its parsed output. For example, a string
95
- transformed to a number is passed as a string to the executor and observed
96
- as a number by its callbacks.
97
-
98
- Test registration no longer needs a registrar adapter or assertion:
99
-
100
- ```ts
101
- import { convexTest } from 'convex-test'
102
- import { registerFoundation, registerApprovals } from 'cvx-kit/test'
103
- import schema from './schema'
104
-
105
- const t = convexTest(schema, import.meta.glob('./**/*.ts'))
106
- registerFoundation(t)
107
- registerApprovals(t)
108
- ```
109
-
110
- Both helpers accept an optional custom component path as their second argument.
111
-
112
- ## Approvals packaging repair (0.1.x patch)
113
-
114
- `cvx-kit@0.1.0` published component schemas as `schema.mjs`, which Convex does
115
- not discover for packaged components. Upgrade to the patch containing this
116
- repair, remove any `patchedDependencies` entry that copied or renamed the
117
- approvals schema, reinstall from a clean lockfile state, and run one normal
118
- deployment:
119
-
120
- ```bash
121
- bun install
122
- bunx convex dev --once
123
- ```
124
-
125
- The repaired package publishes only `schema.js` and `schema.d.ts` for each
126
- component. Do not retain both `.mjs` and `.js`; Convex canonicalizes them to the
127
- same module and rejects the deployment with `CanonicalizationConflict`.
128
-
129
- This upgrade does not change table fields or index definitions, so a second
130
- deployment is not required for staged index backfills. Existing deployments
131
- that never received the component schema will create the tables and indexes on
132
- the first repaired deployment. Confirm readiness through
133
- `components.approvals.health.check` before resuming approval traffic.
134
-
135
- The migration file for consumers on 0.0.x. Each section: the old way you were
136
- hand-writing, the 0.1.0 way, and what stays unchanged. Nothing existing
137
- breaks in this release except one boundary-mask improvement (see the last
138
- section).
139
-
140
- **Semver contract from 0.1.0:** breaking changes land only on minor bumps;
141
- patches are additive or fixes. One caveat, stated honestly: the breaking
142
- window formally closes at the follow-up consolidation release (package-name
143
- decision + full export-name API audit) — until then the six new helper
144
- surfaces may still be renamed in a minor. Every tag now gets a GitHub
145
- Release with CHANGELOG notes.
146
-
147
- ## Pagination — was: `.take()` loops or hand-bridged validators
148
-
149
- Old way:
150
-
151
- ```ts
152
- // hand-rolled: convexToZod(paginationOptsValidator) in every project,
153
- // or worse, .execute(100) and client-side slicing.
154
- ```
155
-
156
- New way:
157
-
158
- ```ts
159
- import { paginated } from 'cvx-kit/zod-table'
160
-
161
- const page = paginated(<entities>.publicDto)
162
- export const list = authQuery({
163
- args: { paginationOpts: page.args.paginationOpts },
164
- returns: page.result,
165
- handler: (ctx, args) =>
166
- ctx.include(ctx.db.query('<entities>'))
167
- .matching('by_tenant', (ix) => ix.eq('tenant', ctx.tenant))
168
- .paginate(args.paginationOpts, (rows) => rows.map(<entities>.toPublicDto)),
169
- })
170
- ```
171
-
172
- Unchanged: `execute(limit)` for non-cursor reads; the maxRows bound applies
173
- to `numItems` too. Under RLS, pages may be SHORTER than `numItems` when
174
- rules reject rows mid-page — cursors remain correct, keep paging until
175
- `isDone`.
176
-
177
- ## CRUD — was: hand-written create/update/archive per entity
178
-
179
- New way:
180
-
181
- ```ts
182
- import { createCrudCommands } from 'cvx-kit/crud'
183
-
184
- const crud = createCrudCommands({
185
- Command, // destructured from your Foundation
186
- table: <entities>,
187
- aggregateType: '<entity>',
188
- actor: (ctx) => ctx.actor.userId,
189
- enrich: (ctx) => ({ tenant: ctx.tenant }), // REQUIRED for tenantTables
190
- })
191
- // crud.executeCreate / executeUpdate / executeArchive — audited commands
192
- ```
193
-
194
- Unchanged: your api/ adapters stay thin; bespoke operations keep using
195
- `Command.operation` directly. Archive is soft-delete only, by doctrine.
196
-
197
- ## Transition guards — was: hand-written `if` chains
198
-
199
- New way:
200
-
201
- ```ts
202
- import { createStateMachine } from 'cvx-kit/state-machine'
203
-
204
- const machine = createStateMachine(<ENTITY>_STATES, {
205
- draft: ['published', 'archived'],
206
- published: ['archived'],
207
- })
208
- // in a command guard:
209
- guard: async (ctx, command) => machine.assert(row.state, command.to)
210
- ```
211
-
212
- ## Rate limiting — was: hand-rolled counters or none
213
-
214
- New way (host mounts `@convex-dev/rate-limiter`; the kit stays
215
- dependency-free — it receives the instance):
216
-
217
- ```ts
218
- import { rateLimit } from 'cvx-kit/middleware'
219
-
220
- const commands = new Command(operations, {
221
- middleware: [rateLimit({ limiter, name: '<entities>.write' })],
222
- })
223
- ```
224
-
225
- Keyed by `ctx.tenant` by default. No tenant and no `key` fn →
226
- `RATE_LIMIT_KEY_MISSING` (configuration error, never a shared bucket).
227
-
228
- ## Webhooks — was: ad-hoc verification and dedup in the action
229
-
230
- New way:
231
-
232
- ```ts
233
- import { createWebhookBoundary, recordWebhookEvent, webhookEventsTable } from 'cvx-kit/webhooks'
234
-
235
- // schema: webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])
236
- const boundary = createWebhookBoundary({
237
- verify: async (raw, request) => timingSafeHmacVerify(raw, request), // RAW body, constant-time, secret from env
238
- eventKey: (raw) => {
239
- const e = JSON.parse(raw)
240
- return `${e.event}:${e.id}:${e.updatedAt}`
241
- },
242
- })
243
- // http.ts routes to boundary.handle(ctx, request, internal.<module>.functions.applyEvent)
244
- // applyEvent (systemMutation) calls recordWebhookEvent FIRST — transactional dedup.
245
- ```
246
-
247
- Dedup rows are host-owned: prune on a retention window longer than the
248
- provider's redelivery horizon (cleanup cron over `receivedAt`).
249
-
250
- ## Agent tools — was: hand-written tool definitions with raw db access
251
-
252
- New way:
253
-
254
- ```ts
255
- import { createAgentTools } from 'cvx-kit/agent-tools'
256
-
257
- const tools = createAgentTools(<entities>, {
258
- create: (ctx, input) => crud.executeCreate(ctx, input), // audited command
259
- get: (ctx, { id }) => loadDto(ctx, id), // reads: query handlers
260
- list: (ctx, { paginationOpts }) => listPage(ctx, paginationOpts),
261
- })
262
- // host: spread each record into @convex-dev/agent's createTool (or ai-sdk tool())
263
- ```
264
-
265
- Shape-compatible, dependency-free; checked against `@convex-dev/agent@0.6.x`
266
- — re-verify at install.
267
-
268
- ## Typed middleware context — was: loose `next({ context })`
269
-
270
- New way:
271
-
272
- ```ts
273
- const withVendor = Command.middleware<Ctx, { vendor: Vendor }>(async ({ context, next }) =>
274
- next({ context: { vendor: await load(context) } }),
275
- )
276
- // registries/operations accept typed middleware without casts
277
- ```
278
-
279
- Unchanged: untyped middleware keeps compiling.
280
-
281
- ## The one behavior change: tool masks use jsonSafeZid
282
-
283
- `<table>.tools.update` and `<table>.tools.id` now type ids with
284
- `jsonSafeZid` instead of `zid`, so generated JSON schemas present ids as
285
- plain strings (compile-time `Id<...>` preserved). If you relied on the masks
286
- producing `zid` runtime metadata, re-check; for LLM tool schemas this is the
287
- fix you wanted.
package/docs/webhooks.md DELETED
@@ -1,58 +0,0 @@
1
- # `cvx-kit/webhooks` — the sanctioned webhook boundary
2
-
3
- Webhooks are the one boundary that genuinely needs deduplication (Convex's
4
- client gives exactly-once for its own mutations; external providers redeliver).
5
- The helper encodes the pattern: **verify over the raw body (fail closed) →
6
- derive the natural key → delegate to an internal mutation whose first act is
7
- transactional dedup**.
8
-
9
- ## Wiring
10
-
11
- ```ts
12
- // schema — host-owned dedup table (the kit gains no tables):
13
- webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])
14
-
15
- // boundary — used inside the host's httpAction:
16
- const boundary = createWebhookBoundary({
17
- // RAW body, BEFORE parsing. Constant-time comparison (crypto.subtle HMAC
18
- // verify), secret from a Convex environment variable — never in code.
19
- verify: async (raw, request) => verifyVendorSignature(raw, request.headers),
20
- // The natural-key pattern:
21
- eventKey: (raw) => {
22
- const event = JSON.parse(raw)
23
- return `${event.type}:${event.id}:${event.updatedAt}`
24
- },
25
- source: 'vendor',
26
- })
27
-
28
- // http.ts — routing only:
29
- http.route({ path: '/webhooks/vendor', method: 'POST',
30
- handler: httpAction((ctx, request) =>
31
- boundary.handle(ctx, request, internal.<module>.functions.applyVendorEvent)) })
32
-
33
- // the target — a systemMutation; dedup FIRST, transactionally:
34
- export const applyVendorEvent = systemMutation({
35
- args: { eventKey: z.string(), payload: z.string(), source: z.string() },
36
- handler: async (ctx, args) => {
37
- const { duplicate } = await recordWebhookEvent(ctx, { key: args.eventKey, source: args.source })
38
- if (duplicate) return null
39
- const payload = JSON.parse(args.payload) // parse INSIDE the trusted path
40
- // ...apply, via domain executors
41
- return null
42
- },
43
- })
44
- ```
45
-
46
- ## Rules
47
-
48
- 1. **Dedup lives in the mutation, never the action.** The action can race;
49
- the mutation's insert-if-absent is transactional.
50
- 2. **Verification is over raw bytes.** Re-serialized/parsed bodies break or
51
- weaken HMACs; the helper hands `verify` the raw `request.text()`.
52
- 3. **Fail closed.** `verify` returning false OR throwing both reject with
53
- `WEBHOOK_SIGNATURE_INVALID`, before any delegation.
54
- 4. **Retention is yours.** Dedup rows grow forever unless pruned — run a
55
- cleanup cron over `receivedAt` with a window comfortably longer than the
56
- provider's redelivery horizon (days, not hours).
57
- 5. Providers with signed timestamps (Stripe-style): also enforce a tolerance
58
- window inside `verify`.
package/docs/zod-table.md DELETED
@@ -1,278 +0,0 @@
1
- # `cvx-kit/zod-table` — one zod shape per entity, masked into every boundary
2
-
3
- The core idea: an entity's shape is declared **once** as a zod object, and every
4
- boundary that shape crosses — storage, insert, update, command input, public
5
- DTO, LLM tool input — is a _derived mask_ of that single declaration. There is
6
- no second source of truth to drift.
7
-
8
- ```ts
9
- import { z } from 'zod'
10
- import { zodTable } from 'cvx-kit/zod-table'
11
-
12
- export const documents = zodTable(
13
- 'documents',
14
- (id) => ({
15
- title: z.string(),
16
- ownerId: id('users'), // `id` is convex-helpers' zid — typed Id<'users'>
17
- secretNote: z.string(),
18
- reviewState: z.enum(['draft', 'published']),
19
- }),
20
- {
21
- serverFields: ['reviewState'], // excluded from inserts — server assigns it
22
- commandFields: ['title'], // the ONLY fields a command may carry
23
- publicFields: ['title', 'ownerId'], // the DTO allowlist — everything else is redacted
24
- },
25
- )
26
- ```
27
-
28
- ## What you get back
29
-
30
- ### Shared ID validation (0.1.3)
31
-
32
- Install one compatible `convex-helpers` peer (`>=0.1.127 <0.2.0`) alongside
33
- cvx-kit. ID metadata lives in a helper-instance registry: copying schemas
34
- between separately installed helper instances can silently turn an ID into
35
- `v.any()`. An exact nested dependency previously caused that failure even
36
- when both helper versions were compatible. Fresh npm and Bun consumer tests
37
- now exercise storage, command, DTO, and nested ID conversion with a different
38
- compatible host version.
39
-
40
- Prefer the matching kit exports when deriving validators from kit schemas:
41
-
42
- ```ts
43
- import { zid, zodToConvex, convexToZod } from 'cvx-kit/zod-table'
44
- ```
45
-
46
- Do not force conflicting nested helper copies with overrides or reuse schema
47
- objects across separate bundles. The upstream converter cannot recognize an
48
- unregistered foreign ID and does not throw; peer installation prevents the
49
- supported package graph from creating that ambiguity. Verify critical ID
50
- validators retain `kind === 'id'` and the expected `tableName` after installation.
51
-
52
- | Property | What it is | Where you use it |
53
- | -------------------------------------------- | ------------------------------------------------------ | ------------------------------ |
54
- | `table` | `defineTable(...)` from the storage shape | `convex/schema.ts` |
55
- | `storage` | zod object of the raw row (no `_id`/`_creationTime`) | validating rows |
56
- | `schema` | `storage` + `_id` + `_creationTime` | full-document validation |
57
- | `insertSchema` | storage minus timestamps minus `serverFields` | mutation args |
58
- | `updateSchema` | `insertSchema.partial()` | patch mutation args |
59
- | `commandInput` | strict pick of `commandFields` | command protocol inputs |
60
- | `publicDto` | strict pick of `publicFields` | query return validators |
61
- | `dto(options?)` | public return schema with optional ID and extensions | query return validators |
62
- | `toPublicDto(row)` | projects **and re-parses** a row into the DTO | query handlers |
63
- | `paginationOpts(selection)` | native pagination options, selected filters, and order | query input validators |
64
- | `insert(omit?)` / `update(omit?)` | schema with per-callsite field omission | specialized mutations |
65
- | `tools.insert` / `tools.update` / `tools.id` | ready-made LLM tool input schemas | agent tool definitions |
66
- | `tableName` | the literal table name | indexes, triggers registration |
67
-
68
- ## Redaction is runtime, not just types
69
-
70
- `toPublicDto` does not merely `pick` at the type level — it re-parses the
71
- projected object through the strict `publicDto` schema. A field that leaks into
72
- the projection throws instead of shipping to the client. Use it on **every**
73
- row a public query returns:
74
-
75
- ```ts
76
- export const get = authQuery({
77
- args: { id: zid('documents') },
78
- returns: documents.publicDto,
79
- handler: async (ctx, { id }) => {
80
- const row = await ctx.db.get(id)
81
- if (!row) throw new KitError({ code: 'NOT_FOUND' })
82
- return documents.toPublicDto(row) // secretNote can never leak
83
- },
84
- })
85
- ```
86
-
87
- ## Public DTO parameters
88
-
89
- `publicDto` is the strict Zod schema selected by `publicFields`; it takes no
90
- parameters. Use `dto(...)` to derive a return schema and `toPublicDto(row, ...)`
91
- to project a row with the same options. These methods are also available on
92
- `tenantTable` boundaries.
93
-
94
- ```ts
95
- documents.toPublicDto(row) // publicFields only
96
- documents.toPublicDto(row, false) // same default projection
97
- documents.toPublicDto(row, true) // publicFields plus _id
98
- documents.toPublicDto(row, { _id: true }) // explicit equivalent
99
-
100
- const withoutId = documents.dto() // same boundary as publicDto
101
- const withId = documents.dto(true) // branded Id<'documents'>
102
- const explicit = documents.dto({ _id: true })
103
- ```
104
-
105
- Including `_id` requires a row containing an ID and validates it through the
106
- table's ID schema. It does not expose `_creationTime`, tenant metadata, or other
107
- storage fields. Do not spread an ID onto a parsed DTO manually; use the matching
108
- `dto(true)` return validator.
109
-
110
- Computed fields need both a schema and explicitly supplied values:
111
-
112
- ```ts
113
- const extend = { url: z.url() }
114
- const returnSchema = documents.dto({ _id: true, extend })
115
-
116
- const output = documents.toPublicDto(row, {
117
- _id: true,
118
- extend,
119
- values: { url: `https://example.test/documents/${row._id}` },
120
- })
121
- returnSchema.parse(output)
122
- ```
123
-
124
- `extend` is a map of field names to Zod validators, not an `extendFields` wrapper.
125
- `values` belongs only to the projector; `dto(...)` describes the result. Extension
126
- values are parsed as a strict object: missing required fields, unknown values,
127
- and invalid values fail. Values are never copied implicitly from storage.
128
-
129
- Extensions cannot replace public fields or use `_id`, `_creationTime`, `tenant`,
130
- `createdAt`, `updatedAt`, `archivedAt`, or `__proto__`. Their schemas must validate
131
- without changing values: transforms, pipes, coercion, preprocess, defaults,
132
- catches, and overwrites are rejected, including inside nested schemas.
133
- Refinements must not mutate their input. Existing public-field schemas retain
134
- their existing parsing behavior.
135
-
136
- ## Pagination options
137
-
138
- Select filterable scalar fields independently of the public DTO mask:
139
-
140
- ```ts
141
- const input = documents.paginationOpts({ title: true, ownerId: true })
142
- const args = input.parse({
143
- numItems: 25,
144
- cursor: null,
145
- order: 'desc',
146
- filters: { title: 'Proposal' },
147
- })
148
- ```
149
-
150
- The helper extends Convex's native pagination options with optional `filters`
151
- and `order: 'asc' | 'desc'`. It returns a Zod object that can be used directly as
152
- query args or extended with endpoint-specific fields. `numItems` must be a
153
- positive integer at most 100; engine configuration may lower that ceiling.
154
- Unknown filter fields fail. Stored scalar fields, including explicitly selected
155
- server-owned fields and IDs, can be filterable even when absent from the DTO.
156
- Arrays, objects, and other unsupported non-scalar fields cannot be selected.
157
- This schema creates no authorization policy by itself.
158
-
159
- For relationship filters and paginated public output, use
160
- `relations.tables.documents.paginationOpts(selection)` and `.pageDto(selection)`.
161
- See [querying.md](./querying.md) for declarations, conditional indexes, filters,
162
- authorization, and child-page continuation. Existing native pagination can keep
163
- using `paginated(dto)`; the querying engine is independent of `include`.
164
-
165
- ## Opinionated timestamps
166
-
167
- Every `zodTable` bakes in three optional, **server-owned** fields:
168
-
169
- - `createdAt` — set once on insert
170
- - `updatedAt` — moved on every update
171
- - `archivedAt` — soft-delete marker, under application control
172
-
173
- They are always excluded from `insertSchema`, `updateSchema`, and
174
- `commandInput` — a client or command can never write them. They are _not_ in
175
- `publicFields` by default; list them explicitly when a DTO needs them
176
- (`publicFields: ['title', 'createdAt']`).
177
-
178
- They maintain themselves only if you register the `timestamps` trigger once
179
- per table (see `triggers.md`). Without the trigger they stay `undefined`.
180
-
181
- Prefer `createdAt` over `_creationTime` in DTOs so the whole lifecycle
182
- vocabulary is yours; `_creationTime` remains available for index ordering.
183
-
184
- ## Field masks — who sees what
185
-
186
- Think of the options as three independent allowlists over the same shape:
187
-
188
- - `serverFields` — _the server assigns these._ Excluded from `insertSchema`
189
- and `updateSchema`. Example: `reviewState`, denormalized counters.
190
- - `commandFields` — _a command may say these._ The command protocol's input
191
- is exactly this pick, strict. Everything else must be derived by the handler.
192
- - `publicFields` — _a client may see these._ The DTO is exactly this pick;
193
- may include timestamp fields.
194
-
195
- All derived objects are `.strict()` — unknown keys are rejected, in both
196
- directions.
197
-
198
- ## `tenantTable` — tenant-owned rows
199
-
200
- `tenantTable` is `zodTable` plus a server-owned `tenant` string field —
201
- injected exactly like timestamps: present in storage, excluded from
202
- `insertSchema`/`updateSchema`/`commandInput`, in `publicDto` only by explicit
203
- allowlist. Handlers stamp it from `ctx.tenant`; row-level security matches on
204
- it; the `tenantOwnership` trigger forbids reassigning it. See `tenancy.md`.
205
-
206
- ```ts
207
- export const <entities> = tenantTable('<entities>', (id) => ({ ... }), {
208
- commandFields: [...], publicFields: [...],
209
- })
210
- ```
211
-
212
- ## `createModule` — module table maps
213
-
214
- Merges per-module table maps into the application schema, throwing when two
215
- modules declare the same table — the combinator for `domain/table.ts`:
216
-
217
- ```ts
218
- export default defineSchema(createModule(<moduleA>Tables, <moduleB>Tables))
219
- ```
220
-
221
- ## `zodVariantTable` — discriminated unions
222
-
223
- For tables whose rows are a discriminated union (events, polymorphic
224
- resources), `zodTable`'s object-shape model doesn't apply. `zodVariantTable`
225
- keeps the same "one owner per table" convention while accepting any zod type
226
- as storage:
227
-
228
- ```ts
229
- const events = zodVariantTable(
230
- 'events',
231
- z.discriminatedUnion('kind', [
232
- z.object({ kind: z.literal('created'), by: zid('users') }),
233
- z.object({ kind: z.literal('archived'), reason: z.string() }),
234
- ]),
235
- )
236
- // events.table → defineTable, events.insertSchema → the union itself
237
- ```
238
-
239
- Note: variant tables do not get the timestamp fields or boundary masks — the
240
- union is the boundary.
241
-
242
- ## `jsonSafeZid` — Ids in LLM tool schemas
243
-
244
- JSON-schema generators turn `zid('users')` into an opaque custom type that
245
- confuses LLM tool calling. `jsonSafeZid('users')` presents as a plain
246
- described string in the generated JSON schema while keeping `Id<'users'>` at
247
- the type level:
248
-
249
- ```ts
250
- const assignInput = z.object({
251
- documentId: jsonSafeZid('documents'), // LLM sees: string, "Convex document id for table \"documents\""
252
- })
253
- ```
254
-
255
- ## Wiring into `schema.ts`
256
-
257
- ```ts
258
- // convex/schema.ts
259
- import { defineSchema } from 'convex/server'
260
- import { documents } from './tables/documents'
261
-
262
- export default defineSchema({
263
- documents: documents.table
264
- .index('by_owner', ['ownerId'])
265
- .index('by_owner_archived', ['ownerId', 'archivedAt']),
266
- })
267
- ```
268
-
269
- Indexes are chained on `.table` exactly like a hand-written `defineTable`.
270
-
271
- ## Rules of thumb
272
-
273
- 1. One `zodTable` per entity, in its own module, exported by name.
274
- 2. Never define an inline `defineTable` or a second zod object for the same
275
- entity — masks only.
276
- 3. Every public query returns `publicDto`-shaped data via `toPublicDto`.
277
- 4. Every command input is `commandInput` (or a documented composition of it).
278
- 5. Timestamps are trigger-maintained; handlers never write them.
@@ -1,44 +0,0 @@
1
- // Convex's CLI discovers a packaged component only when its definition file
2
- // is literally named `convex.config.js` (see DEFINITION_FILENAME_JS in the
3
- // convex CLI). vp pack emits .mjs, so this postbuild step materializes the
4
- // compiled definition as convex.config.js in each component's dist directory.
5
- import { readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
6
- import { dirname, join } from 'node:path'
7
- import { fileURLToPath } from 'node:url'
8
-
9
- const root = join(dirname(fileURLToPath(import.meta.url)), '..')
10
-
11
- for (const component of ['foundation', 'approvals']) {
12
- const directory = join(root, 'dist', 'components', component)
13
- const config = readFileSync(join(directory, 'convex.config.mjs'), 'utf8').replace(
14
- /\/\/# sourceMappingURL=.*\n?/,
15
- '',
16
- )
17
- writeFileSync(join(directory, 'convex.config.js'), config)
18
-
19
- // Component implementations are discovered from schema.ts/schema.js only.
20
- // Keeping schema.mjs beside schema.js is also invalid because Convex
21
- // canonicalizes both extensions to the same module path.
22
- const schema = readFileSync(join(directory, 'schema.mjs'), 'utf8').replace(
23
- /\/\/# sourceMappingURL=.*\n?/,
24
- '',
25
- )
26
- writeFileSync(join(directory, 'schema.js'), schema)
27
- for (const filename of readdirSync(directory)) {
28
- if (!filename.endsWith('.mjs')) continue
29
- const path = join(directory, filename)
30
- const source = readFileSync(path, 'utf8')
31
- const bridged = source.replaceAll('./schema.mjs', './schema.js')
32
- if (bridged !== source) writeFileSync(path, bridged)
33
- }
34
- renameSync(join(directory, 'schema.d.mts'), join(directory, 'schema.d.ts'))
35
- const dataModelDeclaration = join(directory, '_generated', 'dataModel.d.mts')
36
- writeFileSync(
37
- dataModelDeclaration,
38
- readFileSync(dataModelDeclaration, 'utf8').replace('../schema.mjs', '../schema.js'),
39
- )
40
- rmSync(join(directory, 'schema.mjs'))
41
- rmSync(join(directory, 'schema.mjs.map'))
42
- console.log(`bridged dist/components/${component}/convex.config.js`)
43
- console.log(`bridged dist/components/${component}/schema.js`)
44
- }