cvx-kit 0.1.3 → 0.1.4

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 (82) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +69 -24
  3. package/dist/agent-tools.d.mts +28 -12
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/auth.d.mts +4 -2
  7. package/dist/auth.mjs +1 -1
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/components/approvals/_generated/api.d.mts +1 -1
  10. package/dist/components/approvals/audit.mjs +1 -1
  11. package/dist/components/approvals/audit.mjs.map +1 -1
  12. package/dist/components/approvals/client.d.mts +1 -1
  13. package/dist/components/approvals/client.mjs +1 -1
  14. package/dist/components/approvals/client.mjs.map +1 -1
  15. package/dist/components/approvals/constants.mjs.map +1 -1
  16. package/dist/components/approvals/decisions.d.mts +6 -7
  17. package/dist/components/approvals/decisions.mjs +1 -1
  18. package/dist/components/approvals/decisions.mjs.map +1 -1
  19. package/dist/components/approvals/functions.mjs +1 -1
  20. package/dist/components/approvals/functions.mjs.map +1 -1
  21. package/dist/components/approvals/health.mjs.map +1 -1
  22. package/dist/components/approvals/requests.mjs.map +1 -1
  23. package/dist/components/approvals/validators.mjs.map +1 -1
  24. package/dist/components/approvals/workflow.mjs +1 -1
  25. package/dist/components/approvals/workflow.mjs.map +1 -1
  26. package/dist/components/approvals/workflow_steps.d.mts +3 -3
  27. package/dist/components/approvals/workflow_steps.mjs +1 -1
  28. package/dist/components/approvals/workflow_steps.mjs.map +1 -1
  29. package/dist/components/foundation/client.d.mts +17 -24
  30. package/dist/components/foundation/client.mjs +1 -1
  31. package/dist/components/foundation/client.mjs.map +1 -1
  32. package/dist/components/foundation/modules/command/command.d.mts +4 -4
  33. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  34. package/dist/components/foundation/modules/observability/observability.d.mts +1 -1
  35. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  36. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  37. package/dist/components/foundation/modules/query/query.d.mts +8 -8
  38. package/dist/components/foundation/modules/query/query.mjs +1 -1
  39. package/dist/components/foundation/modules/query/query.mjs.map +1 -1
  40. package/dist/components/foundation/operation.d.mts +1 -1
  41. package/dist/components/foundation/operation.mjs.map +1 -1
  42. package/dist/components/foundation/result.d.mts +1 -1
  43. package/dist/components/foundation/result.mjs.map +1 -1
  44. package/dist/components/foundation/telemetry.mjs +1 -1
  45. package/dist/components/foundation/telemetry.mjs.map +1 -1
  46. package/dist/crud.d.mts +177 -16
  47. package/dist/crud.mjs +1 -1
  48. package/dist/crud.mjs.map +1 -1
  49. package/dist/middleware.d.mts +7 -7
  50. package/dist/middleware.mjs +1 -1
  51. package/dist/middleware.mjs.map +1 -1
  52. package/dist/oxlint.d.mts +9 -0
  53. package/dist/oxlint.mjs +2 -0
  54. package/dist/oxlint.mjs.map +1 -0
  55. package/dist/tenancy.mjs +1 -1
  56. package/dist/tenancy.mjs.map +1 -1
  57. package/dist/triggers.mjs +1 -1
  58. package/dist/triggers.mjs.map +1 -1
  59. package/dist/webhooks.d.mts +17 -106
  60. package/dist/webhooks.mjs.map +1 -1
  61. package/dist/zod-table.d.mts +46 -90
  62. package/dist/zod-table.mjs +1 -1
  63. package/dist/zod-table.mjs.map +1 -1
  64. package/docs/approvals.md +57 -56
  65. package/docs/architecture.md +1 -1
  66. package/docs/auth.md +57 -33
  67. package/docs/commands.md +53 -53
  68. package/docs/conventions.md +40 -40
  69. package/docs/crud.md +5 -5
  70. package/docs/llms.md +117 -71
  71. package/docs/maintainability.md +4 -4
  72. package/docs/migration.md +33 -33
  73. package/docs/tenancy.md +5 -8
  74. package/docs/triggers.md +4 -2
  75. package/docs/upgrading.md +32 -7
  76. package/docs/zod-table.md +48 -45
  77. package/package.json +33 -22
  78. package/scripts/convex-config-bridge.mjs +7 -18
  79. package/scripts/smoke-packed-components.mjs +20 -61
  80. package/scripts/test-packed-ids.mjs +16 -24
  81. package/scripts/test-packed-oxlint.mjs +121 -0
  82. package/src/test.ts +8 -17
package/docs/llms.md CHANGED
@@ -33,7 +33,7 @@ function constructors, a trigger registry, an audited command protocol
33
33
  3. **Every public query returns DTOs** via `<table>.toPublicDto(row)` —
34
34
  runtime redaction, not just types.
35
35
  4. **Every read is bounded**: `ctx.include(ctx.db.query('t')).matching(...)
36
- .execute(limit)` with `1 ≤ limit ≤ 100`. `.resolve()` falls back to a full
36
+ .execute(limit)` with `1 ≤ limit ≤ 100`. `.resolve()` falls back to a full
37
37
  table scan — avoid it.
38
38
  5. **State changes are commands**: an operations registry with mandatory
39
39
  `classification` and `audit()` per operation, executed via
@@ -60,31 +60,31 @@ function constructors, a trigger registry, an audited command protocol
60
60
  9. **Operation names are `domain.verb` lowercase-dotted; error codes are
61
61
  UPPER_SNAKE** — otherwise observability silently drops the events.
62
62
  10. **Row-level security is configured, not hand-rolled**: the optional
63
- `security` config on `createAuthFunctions` takes role-level `rules`
64
- (works standalone) and/or `tenancy` (a table registry; adds
65
- `ctx.tenant`, deny-default isolation, and pairs with `tenantTable` +
66
- `tenantOwnership`). Multi-tenant apps stamp inserts with
67
- `tenant: ctx.tenant` and re-verify client ids with
68
- `requireTenantReference`. See `tenancy.md`.
63
+ `security` config on `createAuthFunctions` takes role-level `rules`
64
+ (works standalone) and/or `tenancy` (a table registry; adds
65
+ `ctx.tenant`, deny-default isolation, and pairs with `tenantTable` +
66
+ `tenantOwnership`). Multi-tenant apps stamp inserts with
67
+ `tenant: ctx.tenant` and re-verify client ids with
68
+ `requireTenantReference`. See `tenancy.md`.
69
69
 
70
70
  ## Exports map
71
71
 
72
- | Import | Provides |
73
- |---|---|
74
- | `cvx-kit` | everything below re-exported (except components' defaults) |
75
- | `cvx-kit/zod-table` | `zodTable`, `tenantTable`, `createModule`, `paginated`, `zodVariantTable`, `jsonSafeZid`, `TIMESTAMP_FIELDS` |
76
- | `cvx-kit/auth` | `createAuthFunctions` (incl. optional `security` RLS config), `createInclude`, `defaultRoleMap` |
77
- | `cvx-kit/tenancy` | `createTenantRules`, `composeRules`, `requireTenantReference`, `assertTenantOwned`, `TENANT_FIELD` |
78
- | `cvx-kit/crud` | `createCrudCommands` — create/update/archive from a zodTable, inside the command pipeline; `enrich` REQUIRED for tenantTables |
79
- | `cvx-kit/state-machine` | `createStateMachine` — typed transitions from constants tuples; `assert` drops into command guards |
80
- | `cvx-kit/middleware` | `rateLimit` — packaged middleware over an injected rate-limiter instance; keyed by `ctx.tenant`, missing key = config error |
81
- | `cvx-kit/webhooks` | `createWebhookBoundary`, `recordWebhookEvent`, `webhookEventsTable` — raw-body verify, natural-key dedup in the mutation |
82
- | `cvx-kit/agent-tools` | `createAgentTools` — tool records from table masks; mutation handlers route through command executors |
83
- | `cvx-kit/triggers` | `createTriggers`, `timestamps`, `appendOnly`, `noDelete`, `tenantOwnership`, `Triggers` |
84
- | `cvx-kit/errors` | `KitError`, `defaultErrors`, `ErrorFactory` |
72
+ | Import | Provides |
73
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `cvx-kit` | everything below re-exported (except components' defaults) |
75
+ | `cvx-kit/zod-table` | `zodTable`, `tenantTable`, `createModule`, `paginated`, `zodVariantTable`, `jsonSafeZid`, `TIMESTAMP_FIELDS` |
76
+ | `cvx-kit/auth` | `createAuthFunctions` (incl. optional `security` RLS config), `createInclude`, `defaultRoleMap` |
77
+ | `cvx-kit/tenancy` | `createTenantRules`, `composeRules`, `requireTenantReference`, `assertTenantOwned`, `TENANT_FIELD` |
78
+ | `cvx-kit/crud` | `createCrudCommands` — create/update/archive from a zodTable, inside the command pipeline; `enrich` REQUIRED for tenantTables |
79
+ | `cvx-kit/state-machine` | `createStateMachine` — typed transitions from constants tuples; `assert` drops into command guards |
80
+ | `cvx-kit/middleware` | `rateLimit` — packaged middleware over an injected rate-limiter instance; keyed by `ctx.tenant`, missing key = config error |
81
+ | `cvx-kit/webhooks` | `createWebhookBoundary`, `recordWebhookEvent`, `webhookEventsTable` — raw-body verify, natural-key dedup in the mutation |
82
+ | `cvx-kit/agent-tools` | `createAgentTools` — tool records from table masks; mutation handlers route through command executors |
83
+ | `cvx-kit/triggers` | `createTriggers`, `timestamps`, `appendOnly`, `noDelete`, `tenantOwnership`, `Triggers` |
84
+ | `cvx-kit/errors` | `KitError`, `defaultErrors`, `ErrorFactory` |
85
85
  | `cvx-kit/components/foundation` | `Foundation` — the ONLY runtime export besides the default component config for `app.use`. Everything (`Command`, `Query`, `observability`, `executeResultBoundary`, `projectResult`, `emitSemanticEvent`) destructures from the instance; nothing is importable loose. |
86
- | `cvx-kit/components/approvals` | `Approvals` client; default export = component config for `app.use` |
87
- | `cvx-kit/test` | `registerFoundation(t)`, `registerApprovals(t)` for convex-test |
86
+ | `cvx-kit/components/approvals` | `Approvals` client; default export = component config for `app.use` |
87
+ | `cvx-kit/test` | `registerFoundation(t)`, `registerApprovals(t)` for convex-test |
88
88
 
89
89
  ## Minimal app wiring (the five root files)
90
90
 
@@ -105,27 +105,47 @@ timestamps(triggers, 'documents')
105
105
  appendOnly(triggers, 'history')
106
106
 
107
107
  // convex/functions.ts
108
- export const { authQuery, authMutation, authAction, adminQuery, adminMutation,
109
- adminAction, roleQuery, roleMutation, roleAction, systemQuery, systemMutation,
110
- systemAction, include } = createAuthFunctions<DataModel>({
111
- query, mutation, action, internalQuery, internalMutation, internalAction, // from ./_generated/server
112
- getAuthUser: (ctx) => authKit.getAuthUser(ctx),
113
- mapRole: defaultRoleMap, // 'member'→'writer'; reader|writer|admin pass
114
- adminRoles: ['admin'],
115
- triggers,
116
- verifyMembership: async ({ userId, organizationId }) => { /* live check; actions only */ },
117
- resolveOrganization: async ({ ctx, identity, user }) => { /* org+role from app tables; overrides claims; null/throw ⇒ FORBIDDEN */ },
108
+ export const {
109
+ authQuery,
110
+ authMutation,
111
+ authAction,
112
+ adminQuery,
113
+ adminMutation,
114
+ adminAction,
115
+ roleQuery,
116
+ roleMutation,
117
+ roleAction,
118
+ systemQuery,
119
+ systemMutation,
120
+ systemAction,
121
+ include,
122
+ } = createAuthFunctions<DataModel>({
123
+ query,
124
+ mutation,
125
+ action,
126
+ internalQuery,
127
+ internalMutation,
128
+ internalAction, // from ./_generated/server
129
+ getAuthUser: (ctx) => authKit.getAuthUser(ctx),
130
+ mapRole: defaultRoleMap, // 'member'→'writer'; reader|writer|admin pass
131
+ adminRoles: ['admin'],
132
+ triggers,
133
+ verifyMembership: async ({ userId, organizationId }) => {
134
+ /* live check; actions only */
135
+ },
136
+ resolveOrganization: async ({ ctx, identity, user }) => {
137
+ /* org+role from app tables; overrides claims; null/throw ⇒ FORBIDDEN */
138
+ },
118
139
  })
119
140
 
120
141
  // convex/foundation.ts
121
- export const { Command, Query, observability } = new Foundation(
122
- components.foundation,
123
- { observability: {
124
- enabled: () => process.env.OBS === 'true',
125
- classifyError, // → { outcome: 'denied'|'failed', errorCode }
126
- writeAudit: (ctx, entry) => writeAuditEntry(ctx, entry),
127
- } },
128
- )
142
+ export const { Command, Query, observability } = new Foundation(components.foundation, {
143
+ observability: {
144
+ enabled: () => process.env.OBS === 'true',
145
+ classifyError, // → { outcome: 'denied'|'failed', errorCode }
146
+ writeAudit: (ctx, entry) => writeAuditEntry(ctx, entry),
147
+ },
148
+ })
129
149
 
130
150
  // convex/approvals.ts
131
151
  export const approvals = new Approvals(components.approvals)
@@ -135,43 +155,58 @@ export const approvals = new Approvals(components.approvals)
135
155
 
136
156
  ```ts
137
157
  // domain/documents/schema.ts
138
- export const documents = zodTable('documents', (id) => ({
139
- title: z.string(), ownerId: id('users'), secretNote: z.string(),
140
- }), {
141
- commandFields: ['title'], // what a command may say
142
- publicFields: ['title', 'ownerId'], // the DTO allowlist
143
- })
158
+ export const documents = zodTable(
159
+ 'documents',
160
+ (id) => ({
161
+ title: z.string(),
162
+ ownerId: id('users'),
163
+ secretNote: z.string(),
164
+ }),
165
+ {
166
+ commandFields: ['title'], // what a command may say
167
+ publicFields: ['title', 'ownerId'], // the DTO allowlist
168
+ },
169
+ )
144
170
 
145
171
  // convex/schema.ts (via domain/table.ts)
146
172
  defineSchema({ documents: documents.table.index('by_owner', ['ownerId']) })
147
173
 
148
174
  // domain/documents/commands.ts
149
175
  const operations = {
150
- 'documents.rename': Command.operation({
151
- command: documents.commandInput.extend({ id: zid('documents'), actorId: z.string() }),
152
- result: z.object({ ok: z.literal(true) }).strict(),
153
- classification: 'business',
154
- audit: ({ command }) => ({ operation: 'documents.rename', actorId: command.actorId,
155
- aggregate: { type: 'document', id: command.id } }),
156
- }),
176
+ 'documents.rename': Command.operation({
177
+ command: documents.commandInput.extend({ id: zid('documents'), actorId: z.string() }),
178
+ result: z.object({ ok: z.literal(true) }).strict(),
179
+ classification: 'business',
180
+ audit: ({ command }) => ({
181
+ operation: 'documents.rename',
182
+ actorId: command.actorId,
183
+ aggregate: { type: 'document', id: command.id },
184
+ }),
185
+ }),
157
186
  } as const
158
187
  const commands = new Command<MutationCtx, typeof operations>(operations)
159
- export const executeRename = commands.exec({ operation: 'documents.rename',
160
- handler: async (ctx, cmd) => { await ctx.db.patch(cmd.id, { title: cmd.title }); return { ok: true } } })
188
+ export const executeRename = commands.exec({
189
+ operation: 'documents.rename',
190
+ handler: async (ctx, cmd) => {
191
+ await ctx.db.patch(cmd.id, { title: cmd.title })
192
+ return { ok: true }
193
+ },
194
+ })
161
195
 
162
196
  // api/documents.ts — thin public adapter
163
197
  export const rename = authMutation({
164
- args: documents.commandInput.extend({ id: zid('documents') }),
165
- returns: z.object({ ok: z.literal(true) }).strict(),
166
- handler: (ctx, args) => executeRename(ctx, { ...args, actorId: ctx.actor.userId }),
198
+ args: documents.commandInput.extend({ id: zid('documents') }),
199
+ returns: z.object({ ok: z.literal(true) }).strict(),
200
+ handler: (ctx, args) => executeRename(ctx, { ...args, actorId: ctx.actor.userId }),
167
201
  })
168
202
  export const list = authQuery({
169
- args: { limit: z.number() },
170
- returns: z.array(documents.publicDto),
171
- handler: (ctx, { limit }) =>
172
- ctx.include(ctx.db.query('documents'))
173
- .matching('by_owner', (ix) => ix.eq('ownerId', ctx.actor.userId))
174
- .execute(limit, (rows) => rows.map(documents.toPublicDto)),
203
+ args: { limit: z.number() },
204
+ returns: z.array(documents.publicDto),
205
+ handler: (ctx, { limit }) =>
206
+ ctx
207
+ .include(ctx.db.query('documents'))
208
+ .matching('by_owner', (ix) => ix.eq('ownerId', ctx.actor.userId))
209
+ .execute(limit, (rows) => rows.map(documents.toPublicDto)),
175
210
  })
176
211
  ```
177
212
 
@@ -186,13 +221,24 @@ canonical actor for audits/approvals.
186
221
  ## Approvals in one block
187
222
 
188
223
  ```ts
189
- export const publishApproval = approvals.define({ name: 'documentPublish', steps: [
190
- approvals.decision('managerDecision', { decisions: ['approved','rejected'],
191
- quorum: { kind: 'count', approvals: 1 }, makerChecker: true, expiresAfterMs: 604_800_000 }),
192
- approvals.branch('applyDecision', { approvedStepKey: 'publish', rejectedStepKey: 'notify' }),
193
- approvals.mutation('publish', { handler: internal.domain.documents.approval_functions.applyDecision }),
194
- approvals.notify('notify', { handler: internal.domain.documents.approval_functions.notifyRejected }),
195
- ]})
224
+ export const publishApproval = approvals.define({
225
+ name: 'documentPublish',
226
+ steps: [
227
+ approvals.decision('managerDecision', {
228
+ decisions: ['approved', 'rejected'],
229
+ quorum: { kind: 'count', approvals: 1 },
230
+ makerChecker: true,
231
+ expiresAfterMs: 604_800_000,
232
+ }),
233
+ approvals.branch('applyDecision', { approvedStepKey: 'publish', rejectedStepKey: 'notify' }),
234
+ approvals.mutation('publish', {
235
+ handler: internal.domain.documents.approval_functions.applyDecision,
236
+ }),
237
+ approvals.notify('notify', {
238
+ handler: internal.domain.documents.approval_functions.notifyRejected,
239
+ }),
240
+ ],
241
+ })
196
242
  // start(ctx, { scopeRef, resourceType, resourceRef, requester: ctx.actor, metadata? })
197
243
  // decide(ctx, { runId, decision, reason? }, ctx.actor) · status/evidence/list/cancel/restart
198
244
  ```
@@ -1,8 +1,8 @@
1
1
  # Maintaining a Convex application built on cvx-kit
2
2
 
3
3
  Practices that keep a cvx-kit backend maintainable over years, taken from the
4
- origin application. The theme throughout: make every rule either *structural*
5
- (the code can't be written any other way) or *executable* (a test fails when
4
+ origin application. The theme throughout: make every rule either _structural_
5
+ (the code can't be written any other way) or _executable_ (a test fails when
6
6
  it's violated). Conventions that live only in documentation decay.
7
7
 
8
8
  ## Schema and indexes
@@ -14,7 +14,7 @@ it's violated). Conventions that live only in documentation decay.
14
14
  - **Index naming**: `by_<field>_and_<field>...`, camelCase field names in
15
15
  declaration order, nested paths flattened with `_`
16
16
  (`by_assignedToUserId_and_state_status` for `['assignedToUserId',
17
- 'state.status']`). A reader should reconstruct the index definition from its
17
+ 'state.status']`). A reader should reconstruct the index definition from its
18
18
  name.
19
19
  - Every read goes through `ctx.include(...)` with an explicit limit ≤ 100.
20
20
  If a query can't be served by an index, that's a schema problem, not a
@@ -119,6 +119,6 @@ test modules the same way (`@convex-dev/workflow/test`,
119
119
  any `appendOnly`/`noDelete` discipline.
120
120
  4. `constants.ts` — vocabularies as readonly tuples.
121
121
  5. `commands.ts` — operation registry (`domain.verb`, classification, audit)
122
- + executors.
122
+ - executors.
123
123
  6. `api/<entity>.ts` — thin `auth*` adapters, DTO projection.
124
124
  7. Tests: behavior + add the entity to the architecture globs.
package/docs/migration.md CHANGED
@@ -65,7 +65,7 @@ convex/
65
65
  ### The root rule
66
66
 
67
67
  **The root of `convex/` contains configuration and facades only.** Every root
68
- file is a *declaration point* — the single place one piece of infrastructure
68
+ file is a _declaration point_ — the single place one piece of infrastructure
69
69
  is configured — and none of them exports a public function or contains
70
70
  business logic. If a client can call it, it lives in `api/`. If it decides
71
71
  anything about the business, it lives in `domain/`. If `ls convex/*.ts` shows
@@ -76,19 +76,19 @@ anything else, the migration is not done.
76
76
  These patterns must not exist anywhere after migration — each one is a hole in
77
77
  a structural guarantee:
78
78
 
79
- | Forbidden | Where it's allowed instead | What it breaks otherwise |
80
- |---|---|---|
81
- | `query(`, `mutation(`, `action(`, `internalMutation(`, `internalAction(` from `_generated/server` | `functions.ts` only | auth, triggers, bounded reads |
82
- | `defineTable(` | `domain/<entity>/table.ts` via `<zodTable>.table` | single source of truth per entity |
83
- | `defineSchema(` | root `schema.ts` only | schema assembly |
84
- | inline `z.enum([...])` / repeated literal unions | tuple in `constants.ts` | vocabulary ownership |
85
- | `.collect()` / unbounded reads in public paths | `ctx.include(...).execute(limit)` | read bounds |
86
- | `makeFunctionReference` | generated `internal` / `api` | refactoring safety |
87
- | deep imports of a component's internals | the component's client facade | component boundary |
88
- | sibling-domain imports (`domain/a` → `domain/b`) | `domain/shared/` or the public API | ownership |
89
- | `ctx.db.insert/patch/delete` on a mounted component's tables | the component's client methods | component persistence |
90
- | vendor SDK instantiation inside a domain | one shared client in `domain/shared/<provider>.ts` | connection discipline |
91
- | `new Date().toISOString()` / hand-written timestamp fields | the `timestamps` trigger | server-owned lifecycle |
79
+ | Forbidden | Where it's allowed instead | What it breaks otherwise |
80
+ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------- | --------------------------------- |
81
+ | `query(`, `mutation(`, `action(`, `internalMutation(`, `internalAction(` from `_generated/server` | `functions.ts` only | auth, triggers, bounded reads |
82
+ | `defineTable(` | `domain/<entity>/table.ts` via `<zodTable>.table` | single source of truth per entity |
83
+ | `defineSchema(` | root `schema.ts` only | schema assembly |
84
+ | inline `z.enum([...])` / repeated literal unions | tuple in `constants.ts` | vocabulary ownership |
85
+ | `.collect()` / unbounded reads in public paths | `ctx.include(...).execute(limit)` | read bounds |
86
+ | `makeFunctionReference` | generated `internal` / `api` | refactoring safety |
87
+ | deep imports of a component's internals | the component's client facade | component boundary |
88
+ | sibling-domain imports (`domain/a` → `domain/b`) | `domain/shared/` or the public API | ownership |
89
+ | `ctx.db.insert/patch/delete` on a mounted component's tables | the component's client methods | component persistence |
90
+ | vendor SDK instantiation inside a domain | one shared client in `domain/shared/<provider>.ts` | connection discipline |
91
+ | `new Date().toISOString()` / hand-written timestamp fields | the `timestamps` trigger | server-owned lifecycle |
92
92
 
93
93
  ---
94
94
 
@@ -96,19 +96,19 @@ a structural guarantee:
96
96
 
97
97
  A raw project's root files usually mix several kinds of code. Pull them apart:
98
98
 
99
- | You have today | It becomes |
100
- |---|---|
101
- | `export const get/list = query({...})` at root | adapter in `api/<entity>.ts` + read logic in `domain/<entity>/queries.ts` |
102
- | `export const create/update = mutation({...})` at root | adapter in `api/<entity>.ts` + operation in `domain/<entity>/commands.ts` |
103
- | `internalMutation`/`internalAction` helpers | `system*` functions in `domain/<entity>/<purpose>_functions.ts` |
104
- | inline `defineTable({...})` in `schema.ts` | `zodTable` in `domain/<entity>/schema.ts` + indexes in `domain/<entity>/table.ts` |
105
- | `v.union(v.literal(...))` / inline `z.enum` repeated around | one `UPPER_SNAKE` tuple in `domain/<entity>/constants.ts` |
106
- | validation/business `if`-chains inside handlers | pure predicates in `domain/<entity>/rules.ts` |
107
- | shared grab-bag (`helpers.ts`, `utils.ts`, `lib.ts`) | split by owner into `domain/<entity>/shared.ts`; only genuinely cross-entity code into `domain/shared/` |
108
- | ad-hoc vendor clients (HTTP SDKs, caches, SQL) | one configured client per provider in `domain/shared/<provider>.ts` |
109
- | webhook handlers with logic in `http.ts` | route in `http.ts`, logic in `domain/<entity>/actions.ts` |
110
- | cron handlers with logic in `crons.ts` | declaration in `crons.ts`, target `system*` function in the domain |
111
- | audit/log/history tables mirroring component state | the component's own persistence via its facade |
99
+ | You have today | It becomes |
100
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
101
+ | `export const get/list = query({...})` at root | adapter in `api/<entity>.ts` + read logic in `domain/<entity>/queries.ts` |
102
+ | `export const create/update = mutation({...})` at root | adapter in `api/<entity>.ts` + operation in `domain/<entity>/commands.ts` |
103
+ | `internalMutation`/`internalAction` helpers | `system*` functions in `domain/<entity>/<purpose>_functions.ts` |
104
+ | inline `defineTable({...})` in `schema.ts` | `zodTable` in `domain/<entity>/schema.ts` + indexes in `domain/<entity>/table.ts` |
105
+ | `v.union(v.literal(...))` / inline `z.enum` repeated around | one `UPPER_SNAKE` tuple in `domain/<entity>/constants.ts` |
106
+ | validation/business `if`-chains inside handlers | pure predicates in `domain/<entity>/rules.ts` |
107
+ | shared grab-bag (`helpers.ts`, `utils.ts`, `lib.ts`) | split by owner into `domain/<entity>/shared.ts`; only genuinely cross-entity code into `domain/shared/` |
108
+ | ad-hoc vendor clients (HTTP SDKs, caches, SQL) | one configured client per provider in `domain/shared/<provider>.ts` |
109
+ | webhook handlers with logic in `http.ts` | route in `http.ts`, logic in `domain/<entity>/actions.ts` |
110
+ | cron handlers with logic in `crons.ts` | declaration in `crons.ts`, target `system*` function in the domain |
111
+ | audit/log/history tables mirroring component state | the component's own persistence via its facade |
112
112
 
113
113
  ---
114
114
 
@@ -132,7 +132,7 @@ codebase greppable.
132
132
  the full set once (one owner), not per callsite.
133
133
  - **Vocabulary tuples**: `UPPER_SNAKE_CASE` exported `as const`; derived type
134
134
  `PascalCase` singular (`const <ENTITY>_STATES = [...] as const;
135
- type <Entity>State = (typeof <ENTITY>_STATES)[number]`).
135
+ type <Entity>State = (typeof <ENTITY>_STATES)[number]`).
136
136
  - **Domain executors**: `execute<Verb><Entity>` (`executeApproveInvoice`) —
137
137
  the grep-able seam between adapters and domain.
138
138
  - **Internal callback files**: `<purpose>_functions.ts` next to the
@@ -199,8 +199,8 @@ For each public function at the root:
199
199
 
200
200
  1. **Extract the domain logic.** Reads → `domain/<entity>/queries.ts`.
201
201
  Writes → an operation in the entity's command registry with
202
- classification and audit derivation (see `commands.md`) — deciding *what
203
- gets audited and as what* is a product decision; make it explicitly, not
202
+ classification and audit derivation (see `commands.md`) — deciding _what
203
+ gets audited and as what_ is a product decision; make it explicitly, not
204
204
  as an afterthought. Pure conditions the handler checks
205
205
  (state-transition legality, permission-beyond-role predicates) →
206
206
  `rules.ts`, so they're unit-testable without a ctx.
@@ -225,7 +225,7 @@ in the domain.
225
225
  `<purpose>_functions.ts`. Scheduled and callback writes now run under the
226
226
  same trigger regime as user writes.
227
227
  - `crons.ts` becomes declarations only, targeting `internal.domain.<entity>.
228
- <purpose>_functions.*`.
228
+ <purpose>_functions.*`.
229
229
  - `http.ts` becomes routing only: verify the webhook signature, parse the
230
230
  payload at the boundary, delegate to a domain action/function.
231
231
 
@@ -267,11 +267,11 @@ substantially changed; don't mechanically rewrite untouched files.
267
267
  import { parseInput } from './schema'
268
268
 
269
269
  export async function createProjection(input: unknown) {
270
- return buildProjection(parseInput(input))
270
+ return buildProjection(parseInput(input))
271
271
  }
272
272
 
273
273
  function buildProjection(input: ProjectionInput) {
274
- // implementation
274
+ // implementation
275
275
  }
276
276
  ```
277
277
 
package/docs/tenancy.md CHANGED
@@ -126,11 +126,10 @@ security: {
126
126
 
127
127
  ```ts
128
128
  rules: (bundle) => ({
129
- '<entities>': {
130
- modify: async (_ctx, doc) =>
131
- bundle.role === 'admin' || doc.ownerId === bundle.actor.userId,
132
- },
133
- '<evidence>': { modify: async () => false }, // frozen via RLS as well as trigger
129
+ '<entities>': {
130
+ modify: async (_ctx, doc) => bundle.role === 'admin' || doc.ownerId === bundle.actor.userId,
131
+ },
132
+ '<evidence>': { modify: async () => false }, // frozen via RLS as well as trigger
134
133
  })
135
134
  ```
136
135
 
@@ -171,9 +170,7 @@ the schema and rejects a table declared by two modules:
171
170
 
172
171
  ```ts
173
172
  // convex/domain/table.ts
174
- export const domainTables = createModule(
175
- <moduleA>Tables, <moduleB>Tables, <moduleC>Tables,
176
- )
173
+ export const domainTables = createModule(<moduleA>Tables, <moduleB>Tables, <moduleC>Tables)
177
174
  // convex/schema.ts
178
175
  export default defineSchema(domainTables)
179
176
  ```
package/docs/triggers.md CHANGED
@@ -27,7 +27,9 @@ noDelete(triggers, 'documents')
27
27
 
28
28
  // Domain triggers register on the same instance
29
29
  triggers.register('documents', async (ctx, change) => {
30
- if (change.operation === 'insert') { /* denormalize, validate, cascade */ }
30
+ if (change.operation === 'insert') {
31
+ /* denormalize, validate, cascade */
32
+ }
31
33
  })
32
34
  ```
33
35
 
@@ -65,7 +67,7 @@ createAuthFunctions<DataModel>({ ..., triggers })
65
67
  (useful for seeding, dangerous for assertions about trigger behavior).
66
68
  2. **Wrapper spread order matters** if you compose ctx manually:
67
69
  `{ ...wrapDB(ctx), ...otherStuff }` — `wrapDB` returns a full ctx, so
68
- spreading it *after* anything that carries `db` restores the raw database.
70
+ spreading it _after_ anything that carries `db` restores the raw database.
69
71
  The kit's constructors already order this correctly
70
72
  (`{ ...wrapDB(ctx), include, ...authBundle }`).
71
73
  3. **A throwing trigger aborts the transaction.** That is the point for
package/docs/upgrading.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Upgrading to 0.1.0 — the new way of things
2
2
 
3
+ ## 0.1.4
4
+
5
+ The optional Oxlint plugin is available through `cvx-kit/oxlint`; the root
6
+ README shows consumer configuration. Loading the plugin does not enable rules
7
+ automatically. Select rules in your lint configuration and list your public
8
+ entry points in `cvx/no-internal-reexports`.
9
+
10
+ This patch tightens several TypeScript contracts during the anti-slop cleanup:
11
+
12
+ - Query middleware preserves the handler's result type. Return `next()` or
13
+ throw; middleware that fabricates an unrelated result no longer typechecks.
14
+ - `include().execute` and `.paginate` require a transform callback when
15
+ requesting a different output type through an explicit type argument.
16
+ - Audit metadata values must be serializable Convex `Value` values.
17
+ - Webhook targets must be internal mutation references accepting
18
+ `{ eventKey: string, payload: string, source: string }`.
19
+ - CRUD enrichment derives from the table storage schema. Agent tools retain
20
+ each supplied handler's input and result types.
21
+
22
+ Run your application's typecheck after upgrading and update affected callbacks.
23
+ No storage migration is required.
24
+
3
25
  ## 0.1.3
4
26
 
5
27
  Install `convex-helpers` as a direct peer, using one compatible instance:
@@ -135,8 +157,8 @@ New way:
135
157
  import { createStateMachine } from 'cvx-kit/state-machine'
136
158
 
137
159
  const machine = createStateMachine(<ENTITY>_STATES, {
138
- draft: ['published', 'archived'],
139
- published: ['archived'],
160
+ draft: ['published', 'archived'],
161
+ published: ['archived'],
140
162
  })
141
163
  // in a command guard:
142
164
  guard: async (ctx, command) => machine.assert(row.state, command.to)
@@ -151,7 +173,7 @@ dependency-free — it receives the instance):
151
173
  import { rateLimit } from 'cvx-kit/middleware'
152
174
 
153
175
  const commands = new Command(operations, {
154
- middleware: [rateLimit({ limiter, name: '<entities>.write' })],
176
+ middleware: [rateLimit({ limiter, name: '<entities>.write' })],
155
177
  })
156
178
  ```
157
179
 
@@ -167,8 +189,11 @@ import { createWebhookBoundary, recordWebhookEvent, webhookEventsTable } from 'c
167
189
 
168
190
  // schema: webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])
169
191
  const boundary = createWebhookBoundary({
170
- verify: async (raw, request) => timingSafeHmacVerify(raw, request), // RAW body, constant-time, secret from env
171
- eventKey: (raw) => { const e = JSON.parse(raw); return `${e.event}:${e.id}:${e.updatedAt}` },
192
+ verify: async (raw, request) => timingSafeHmacVerify(raw, request), // RAW body, constant-time, secret from env
193
+ eventKey: (raw) => {
194
+ const e = JSON.parse(raw)
195
+ return `${e.event}:${e.id}:${e.updatedAt}`
196
+ },
172
197
  })
173
198
  // http.ts routes to boundary.handle(ctx, request, internal.<module>.functions.applyEvent)
174
199
  // applyEvent (systemMutation) calls recordWebhookEvent FIRST — transactional dedup.
@@ -200,8 +225,8 @@ Shape-compatible, dependency-free; checked against `@convex-dev/agent@0.6.x`
200
225
  New way:
201
226
 
202
227
  ```ts
203
- const withVendor = Command.middleware<Ctx, { vendor: Vendor }>(
204
- async ({ context, next }) => next({ context: { vendor: await load(context) } }),
228
+ const withVendor = Command.middleware<Ctx, { vendor: Vendor }>(async ({ context, next }) =>
229
+ next({ context: { vendor: await load(context) } }),
205
230
  )
206
231
  // registries/operations accept typed middleware without casts
207
232
  ```