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.
- package/README.md +64 -45
- package/dist/querying/cursors.d.mts +5 -0
- package/dist/querying/cursors.mjs +1 -1
- package/dist/querying/cursors.mjs.map +1 -1
- package/dist/querying.d.mts +20 -6
- package/dist/querying.mjs +1 -1
- package/dist/querying.mjs.map +1 -1
- package/dist/relations.mjs +1 -1
- package/dist/relations.mjs.map +1 -1
- package/dist/zod-table.d.mts +59 -2
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/package.json +10 -3
- package/AGENTS.md +0 -47
- package/CHANGELOG.md +0 -324
- package/ROADMAP.md +0 -126
- package/docs/agent-tools.md +0 -41
- package/docs/approvals.md +0 -201
- package/docs/architecture.md +0 -124
- package/docs/auth.md +0 -443
- package/docs/command-context.md +0 -150
- package/docs/commands-legacy.md +0 -513
- package/docs/commands.md +0 -173
- package/docs/conventions.md +0 -345
- package/docs/crud.md +0 -66
- package/docs/domain-contracts.md +0 -148
- package/docs/domain-operations.md +0 -615
- package/docs/effect.md +0 -403
- package/docs/llms.md +0 -324
- package/docs/maintainability.md +0 -124
- package/docs/migration.md +0 -351
- package/docs/oxlint.md +0 -254
- package/docs/querying.md +0 -293
- package/docs/tenancy.md +0 -176
- package/docs/triggers.md +0 -77
- package/docs/upgrading.md +0 -287
- package/docs/webhooks.md +0 -58
- package/docs/zod-table.md +0 -278
- package/scripts/convex-config-bridge.mjs +0 -44
- package/scripts/convex-smoke-env.mjs +0 -33
- package/scripts/extract-changelog.mjs +0 -29
- package/scripts/owned-process.mjs +0 -140
- package/scripts/owned-process.test.mjs +0 -107
- package/scripts/smoke-packed-components.mjs +0 -341
- package/scripts/smoke-packed-effect.mjs +0 -439
- package/scripts/smoke-packed-querying.mjs +0 -296
- package/scripts/test-packed-auth.mjs +0 -72
- package/scripts/test-packed-effect.mjs +0 -482
- package/scripts/test-packed-ids.mjs +0 -150
- package/scripts/test-packed-oxlint.mjs +0 -167
- 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
|
-
}
|