cvx-kit 0.1.3 → 0.1.5
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/CHANGELOG.md +33 -0
- package/README.md +81 -24
- package/dist/agent-tools.d.mts +28 -12
- package/dist/agent-tools.mjs +1 -1
- package/dist/agent-tools.mjs.map +1 -1
- package/dist/auth.d.mts +4 -2
- package/dist/auth.mjs +1 -1
- package/dist/auth.mjs.map +1 -1
- package/dist/components/approvals/_generated/api.d.mts +1 -1
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/approvals/audit.mjs.map +1 -1
- package/dist/components/approvals/client.d.mts +1 -1
- package/dist/components/approvals/client.mjs +1 -1
- package/dist/components/approvals/client.mjs.map +1 -1
- package/dist/components/approvals/constants.mjs.map +1 -1
- package/dist/components/approvals/decisions.d.mts +6 -7
- package/dist/components/approvals/decisions.mjs +1 -1
- package/dist/components/approvals/decisions.mjs.map +1 -1
- package/dist/components/approvals/functions.mjs +1 -1
- package/dist/components/approvals/functions.mjs.map +1 -1
- package/dist/components/approvals/health.mjs.map +1 -1
- package/dist/components/approvals/requests.mjs.map +1 -1
- package/dist/components/approvals/validators.mjs.map +1 -1
- package/dist/components/approvals/workflow.mjs +1 -1
- package/dist/components/approvals/workflow.mjs.map +1 -1
- package/dist/components/approvals/workflow_steps.d.mts +3 -3
- package/dist/components/approvals/workflow_steps.mjs +1 -1
- package/dist/components/approvals/workflow_steps.mjs.map +1 -1
- package/dist/components/foundation/client.d.mts +17 -24
- package/dist/components/foundation/client.mjs +1 -1
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/components/foundation/modules/command/command.d.mts +4 -4
- package/dist/components/foundation/modules/command/command.mjs.map +1 -1
- package/dist/components/foundation/modules/observability/observability.d.mts +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
- package/dist/components/foundation/modules/query/query.d.mts +8 -8
- package/dist/components/foundation/modules/query/query.mjs +1 -1
- package/dist/components/foundation/modules/query/query.mjs.map +1 -1
- package/dist/components/foundation/operation.d.mts +1 -1
- package/dist/components/foundation/operation.mjs.map +1 -1
- package/dist/components/foundation/result.d.mts +1 -1
- package/dist/components/foundation/result.mjs.map +1 -1
- package/dist/components/foundation/telemetry.mjs +1 -1
- package/dist/components/foundation/telemetry.mjs.map +1 -1
- package/dist/crud.d.mts +177 -16
- package/dist/crud.mjs +1 -1
- package/dist/crud.mjs.map +1 -1
- package/dist/middleware.d.mts +7 -7
- package/dist/middleware.mjs +1 -1
- package/dist/middleware.mjs.map +1 -1
- package/dist/oxlint.d.mts +8 -0
- package/dist/oxlint.mjs +2 -0
- package/dist/oxlint.mjs.map +1 -0
- package/dist/tenancy.mjs +1 -1
- package/dist/tenancy.mjs.map +1 -1
- package/dist/triggers.mjs +1 -1
- package/dist/triggers.mjs.map +1 -1
- package/dist/webhooks.d.mts +17 -106
- package/dist/webhooks.mjs.map +1 -1
- package/dist/zod-table.d.mts +46 -90
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/docs/approvals.md +57 -56
- package/docs/architecture.md +1 -1
- package/docs/auth.md +57 -33
- package/docs/commands.md +53 -53
- package/docs/conventions.md +40 -40
- package/docs/crud.md +5 -5
- package/docs/llms.md +120 -71
- package/docs/maintainability.md +4 -4
- package/docs/migration.md +33 -33
- package/docs/oxlint.md +98 -0
- package/docs/tenancy.md +5 -8
- package/docs/triggers.md +4 -2
- package/docs/upgrading.md +47 -7
- package/docs/zod-table.md +48 -45
- package/package.json +33 -22
- package/scripts/convex-config-bridge.mjs +7 -18
- package/scripts/smoke-packed-components.mjs +20 -61
- package/scripts/test-packed-ids.mjs +16 -24
- package/scripts/test-packed-oxlint.mjs +141 -0
- package/src/test.ts +8 -17
package/docs/llms.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
> authoritative. Restructuring an existing raw project → `migration.md`.
|
|
10
10
|
> Upgrading from 0.0.x → `upgrading.md`. New 0.1.0 helpers: `crud.md`,
|
|
11
11
|
> `webhooks.md`, `agent-tools.md`; pagination in `zod-table.md`/`auth.md`.
|
|
12
|
+
> Consumer lint configuration and coverage → `oxlint.md`. The optional
|
|
13
|
+
> `cvx-kit/oxlint` plugin checks the statically detectable conventions;
|
|
14
|
+
> behavioral guarantees still need architecture and runtime tests.
|
|
12
15
|
|
|
13
16
|
> **Placeholders.** Names like `documents`, `users`, `history`, `title`,
|
|
14
17
|
> `ownerId`, and `documentPublish` in the examples below are illustrative
|
|
@@ -33,7 +36,7 @@ function constructors, a trigger registry, an audited command protocol
|
|
|
33
36
|
3. **Every public query returns DTOs** via `<table>.toPublicDto(row)` —
|
|
34
37
|
runtime redaction, not just types.
|
|
35
38
|
4. **Every read is bounded**: `ctx.include(ctx.db.query('t')).matching(...)
|
|
36
|
-
|
|
39
|
+
.execute(limit)` with `1 ≤ limit ≤ 100`. `.resolve()` falls back to a full
|
|
37
40
|
table scan — avoid it.
|
|
38
41
|
5. **State changes are commands**: an operations registry with mandatory
|
|
39
42
|
`classification` and `audit()` per operation, executed via
|
|
@@ -60,31 +63,31 @@ function constructors, a trigger registry, an audited command protocol
|
|
|
60
63
|
9. **Operation names are `domain.verb` lowercase-dotted; error codes are
|
|
61
64
|
UPPER_SNAKE** — otherwise observability silently drops the events.
|
|
62
65
|
10. **Row-level security is configured, not hand-rolled**: the optional
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
`security` config on `createAuthFunctions` takes role-level `rules`
|
|
67
|
+
(works standalone) and/or `tenancy` (a table registry; adds
|
|
68
|
+
`ctx.tenant`, deny-default isolation, and pairs with `tenantTable` +
|
|
69
|
+
`tenantOwnership`). Multi-tenant apps stamp inserts with
|
|
70
|
+
`tenant: ctx.tenant` and re-verify client ids with
|
|
71
|
+
`requireTenantReference`. See `tenancy.md`.
|
|
69
72
|
|
|
70
73
|
## Exports map
|
|
71
74
|
|
|
72
|
-
| Import
|
|
73
|
-
|
|
74
|
-
| `cvx-kit`
|
|
75
|
-
| `cvx-kit/zod-table`
|
|
76
|
-
| `cvx-kit/auth`
|
|
77
|
-
| `cvx-kit/tenancy`
|
|
78
|
-
| `cvx-kit/crud`
|
|
79
|
-
| `cvx-kit/state-machine`
|
|
80
|
-
| `cvx-kit/middleware`
|
|
81
|
-
| `cvx-kit/webhooks`
|
|
82
|
-
| `cvx-kit/agent-tools`
|
|
83
|
-
| `cvx-kit/triggers`
|
|
84
|
-
| `cvx-kit/errors`
|
|
75
|
+
| Import | Provides |
|
|
76
|
+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
77
|
+
| `cvx-kit` | everything below re-exported (except components' defaults) |
|
|
78
|
+
| `cvx-kit/zod-table` | `zodTable`, `tenantTable`, `createModule`, `paginated`, `zodVariantTable`, `jsonSafeZid`, `TIMESTAMP_FIELDS` |
|
|
79
|
+
| `cvx-kit/auth` | `createAuthFunctions` (incl. optional `security` RLS config), `createInclude`, `defaultRoleMap` |
|
|
80
|
+
| `cvx-kit/tenancy` | `createTenantRules`, `composeRules`, `requireTenantReference`, `assertTenantOwned`, `TENANT_FIELD` |
|
|
81
|
+
| `cvx-kit/crud` | `createCrudCommands` — create/update/archive from a zodTable, inside the command pipeline; `enrich` REQUIRED for tenantTables |
|
|
82
|
+
| `cvx-kit/state-machine` | `createStateMachine` — typed transitions from constants tuples; `assert` drops into command guards |
|
|
83
|
+
| `cvx-kit/middleware` | `rateLimit` — packaged middleware over an injected rate-limiter instance; keyed by `ctx.tenant`, missing key = config error |
|
|
84
|
+
| `cvx-kit/webhooks` | `createWebhookBoundary`, `recordWebhookEvent`, `webhookEventsTable` — raw-body verify, natural-key dedup in the mutation |
|
|
85
|
+
| `cvx-kit/agent-tools` | `createAgentTools` — tool records from table masks; mutation handlers route through command executors |
|
|
86
|
+
| `cvx-kit/triggers` | `createTriggers`, `timestamps`, `appendOnly`, `noDelete`, `tenantOwnership`, `Triggers` |
|
|
87
|
+
| `cvx-kit/errors` | `KitError`, `defaultErrors`, `ErrorFactory` |
|
|
85
88
|
| `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`
|
|
87
|
-
| `cvx-kit/test`
|
|
89
|
+
| `cvx-kit/components/approvals` | `Approvals` client; default export = component config for `app.use` |
|
|
90
|
+
| `cvx-kit/test` | `registerFoundation(t)`, `registerApprovals(t)` for convex-test |
|
|
88
91
|
|
|
89
92
|
## Minimal app wiring (the five root files)
|
|
90
93
|
|
|
@@ -105,27 +108,47 @@ timestamps(triggers, 'documents')
|
|
|
105
108
|
appendOnly(triggers, 'history')
|
|
106
109
|
|
|
107
110
|
// convex/functions.ts
|
|
108
|
-
export const {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
export const {
|
|
112
|
+
authQuery,
|
|
113
|
+
authMutation,
|
|
114
|
+
authAction,
|
|
115
|
+
adminQuery,
|
|
116
|
+
adminMutation,
|
|
117
|
+
adminAction,
|
|
118
|
+
roleQuery,
|
|
119
|
+
roleMutation,
|
|
120
|
+
roleAction,
|
|
121
|
+
systemQuery,
|
|
122
|
+
systemMutation,
|
|
123
|
+
systemAction,
|
|
124
|
+
include,
|
|
125
|
+
} = createAuthFunctions<DataModel>({
|
|
126
|
+
query,
|
|
127
|
+
mutation,
|
|
128
|
+
action,
|
|
129
|
+
internalQuery,
|
|
130
|
+
internalMutation,
|
|
131
|
+
internalAction, // from ./_generated/server
|
|
132
|
+
getAuthUser: (ctx) => authKit.getAuthUser(ctx),
|
|
133
|
+
mapRole: defaultRoleMap, // 'member'→'writer'; reader|writer|admin pass
|
|
134
|
+
adminRoles: ['admin'],
|
|
135
|
+
triggers,
|
|
136
|
+
verifyMembership: async ({ userId, organizationId }) => {
|
|
137
|
+
/* live check; actions only */
|
|
138
|
+
},
|
|
139
|
+
resolveOrganization: async ({ ctx, identity, user }) => {
|
|
140
|
+
/* org+role from app tables; overrides claims; null/throw ⇒ FORBIDDEN */
|
|
141
|
+
},
|
|
118
142
|
})
|
|
119
143
|
|
|
120
144
|
// convex/foundation.ts
|
|
121
|
-
export const { Command, Query, observability } = new Foundation(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
)
|
|
145
|
+
export const { Command, Query, observability } = new Foundation(components.foundation, {
|
|
146
|
+
observability: {
|
|
147
|
+
enabled: () => process.env.OBS === 'true',
|
|
148
|
+
classifyError, // → { outcome: 'denied'|'failed', errorCode }
|
|
149
|
+
writeAudit: (ctx, entry) => writeAuditEntry(ctx, entry),
|
|
150
|
+
},
|
|
151
|
+
})
|
|
129
152
|
|
|
130
153
|
// convex/approvals.ts
|
|
131
154
|
export const approvals = new Approvals(components.approvals)
|
|
@@ -135,43 +158,58 @@ export const approvals = new Approvals(components.approvals)
|
|
|
135
158
|
|
|
136
159
|
```ts
|
|
137
160
|
// domain/documents/schema.ts
|
|
138
|
-
export const documents = zodTable(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
export const documents = zodTable(
|
|
162
|
+
'documents',
|
|
163
|
+
(id) => ({
|
|
164
|
+
title: z.string(),
|
|
165
|
+
ownerId: id('users'),
|
|
166
|
+
secretNote: z.string(),
|
|
167
|
+
}),
|
|
168
|
+
{
|
|
169
|
+
commandFields: ['title'], // what a command may say
|
|
170
|
+
publicFields: ['title', 'ownerId'], // the DTO allowlist
|
|
171
|
+
},
|
|
172
|
+
)
|
|
144
173
|
|
|
145
174
|
// convex/schema.ts (via domain/table.ts)
|
|
146
175
|
defineSchema({ documents: documents.table.index('by_owner', ['ownerId']) })
|
|
147
176
|
|
|
148
177
|
// domain/documents/commands.ts
|
|
149
178
|
const operations = {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
179
|
+
'documents.rename': Command.operation({
|
|
180
|
+
command: documents.commandInput.extend({ id: zid('documents'), actorId: z.string() }),
|
|
181
|
+
result: z.object({ ok: z.literal(true) }).strict(),
|
|
182
|
+
classification: 'business',
|
|
183
|
+
audit: ({ command }) => ({
|
|
184
|
+
operation: 'documents.rename',
|
|
185
|
+
actorId: command.actorId,
|
|
186
|
+
aggregate: { type: 'document', id: command.id },
|
|
187
|
+
}),
|
|
188
|
+
}),
|
|
157
189
|
} as const
|
|
158
190
|
const commands = new Command<MutationCtx, typeof operations>(operations)
|
|
159
|
-
export const executeRename = commands.exec({
|
|
160
|
-
|
|
191
|
+
export const executeRename = commands.exec({
|
|
192
|
+
operation: 'documents.rename',
|
|
193
|
+
handler: async (ctx, cmd) => {
|
|
194
|
+
await ctx.db.patch(cmd.id, { title: cmd.title })
|
|
195
|
+
return { ok: true }
|
|
196
|
+
},
|
|
197
|
+
})
|
|
161
198
|
|
|
162
199
|
// api/documents.ts — thin public adapter
|
|
163
200
|
export const rename = authMutation({
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
args: documents.commandInput.extend({ id: zid('documents') }),
|
|
202
|
+
returns: z.object({ ok: z.literal(true) }).strict(),
|
|
203
|
+
handler: (ctx, args) => executeRename(ctx, { ...args, actorId: ctx.actor.userId }),
|
|
167
204
|
})
|
|
168
205
|
export const list = authQuery({
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
args: { limit: z.number() },
|
|
207
|
+
returns: z.array(documents.publicDto),
|
|
208
|
+
handler: (ctx, { limit }) =>
|
|
209
|
+
ctx
|
|
210
|
+
.include(ctx.db.query('documents'))
|
|
211
|
+
.matching('by_owner', (ix) => ix.eq('ownerId', ctx.actor.userId))
|
|
212
|
+
.execute(limit, (rows) => rows.map(documents.toPublicDto)),
|
|
175
213
|
})
|
|
176
214
|
```
|
|
177
215
|
|
|
@@ -186,13 +224,24 @@ canonical actor for audits/approvals.
|
|
|
186
224
|
## Approvals in one block
|
|
187
225
|
|
|
188
226
|
```ts
|
|
189
|
-
export const publishApproval = approvals.define({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
export const publishApproval = approvals.define({
|
|
228
|
+
name: 'documentPublish',
|
|
229
|
+
steps: [
|
|
230
|
+
approvals.decision('managerDecision', {
|
|
231
|
+
decisions: ['approved', 'rejected'],
|
|
232
|
+
quorum: { kind: 'count', approvals: 1 },
|
|
233
|
+
makerChecker: true,
|
|
234
|
+
expiresAfterMs: 604_800_000,
|
|
235
|
+
}),
|
|
236
|
+
approvals.branch('applyDecision', { approvedStepKey: 'publish', rejectedStepKey: 'notify' }),
|
|
237
|
+
approvals.mutation('publish', {
|
|
238
|
+
handler: internal.domain.documents.approval_functions.applyDecision,
|
|
239
|
+
}),
|
|
240
|
+
approvals.notify('notify', {
|
|
241
|
+
handler: internal.domain.documents.approval_functions.notifyRejected,
|
|
242
|
+
}),
|
|
243
|
+
],
|
|
244
|
+
})
|
|
196
245
|
// start(ctx, { scopeRef, resourceType, resourceRef, requester: ctx.actor, metadata? })
|
|
197
246
|
// decide(ctx, { runId, decision, reason? }, ctx.actor) · status/evidence/list/cancel/restart
|
|
198
247
|
```
|
package/docs/maintainability.md
CHANGED
|
@@ -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
|
|
5
|
-
(the code can't be written any other way) or
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
80
|
-
|
|
81
|
-
| `query(`, `mutation(`, `action(`, `internalMutation(`, `internalAction(` from `_generated/server` | `functions.ts` only
|
|
82
|
-
| `defineTable(`
|
|
83
|
-
| `defineSchema(`
|
|
84
|
-
| inline `z.enum([...])` / repeated literal unions
|
|
85
|
-
| `.collect()` / unbounded reads in public paths
|
|
86
|
-
| `makeFunctionReference`
|
|
87
|
-
| deep imports of a component's internals
|
|
88
|
-
| sibling-domain imports (`domain/a` → `domain/b`)
|
|
89
|
-
| `ctx.db.insert/patch/delete` on a mounted component's tables
|
|
90
|
-
| vendor SDK instantiation inside a domain
|
|
91
|
-
| `new Date().toISOString()` / hand-written timestamp fields
|
|
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
|
|
100
|
-
|
|
101
|
-
| `export const get/list = query({...})` at root
|
|
102
|
-
| `export const create/update = mutation({...})` at root
|
|
103
|
-
| `internalMutation`/`internalAction` helpers
|
|
104
|
-
| inline `defineTable({...})` in `schema.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
|
|
107
|
-
| shared grab-bag (`helpers.ts`, `utils.ts`, `lib.ts`)
|
|
108
|
-
| ad-hoc vendor clients (HTTP SDKs, caches, SQL)
|
|
109
|
-
| webhook handlers with logic in `http.ts`
|
|
110
|
-
| cron handlers with logic in `crons.ts`
|
|
111
|
-
| audit/log/history tables mirroring component state
|
|
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
|
-
|
|
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
|
|
203
|
-
gets audited and as
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
return buildProjection(parseInput(input))
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
function buildProjection(input: ProjectionInput) {
|
|
274
|
-
|
|
274
|
+
// implementation
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
package/docs/oxlint.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Oxlint rules for cvx-kit applications
|
|
2
|
+
|
|
3
|
+
`cvx-kit/oxlint` is a TypeScript-authored plugin bundled with the package. It
|
|
4
|
+
checks applications using the kit against `conventions.md`, the authoritative
|
|
5
|
+
source for folder structure and file anatomy. The package repository has a
|
|
6
|
+
separate, unpublished plugin for its own `src/` layout.
|
|
7
|
+
|
|
8
|
+
These consumer rules are available from 0.1.5; the initial 0.1.4 plugin targeted
|
|
9
|
+
library authoring. See `upgrading.md` when migrating that configuration.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Install Oxlint as a development dependency and load the plugin in
|
|
14
|
+
`.oxlintrc.json`. The root README includes a configuration enabling all rules.
|
|
15
|
+
Loading a plugin does not enable its rules. For example:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"jsPlugins": [{ "name": "cvx", "specifier": "cvx-kit/oxlint" }],
|
|
20
|
+
"rules": {
|
|
21
|
+
"cvx/no-raw-builders": "error",
|
|
22
|
+
"cvx/domain-import-boundaries": "error",
|
|
23
|
+
"cvx/thin-api-adapters": "error"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With Vite+, place those fields under `lint` in `vite.config.ts`. The plugin
|
|
29
|
+
exports a default plugin object, named `rules`, and the `RuleName` type. It is
|
|
30
|
+
tested with Oxlint 1.79.0 and Vite+ 0.3.0.
|
|
31
|
+
|
|
32
|
+
All rules default to `convex/` relative to the linter's working directory.
|
|
33
|
+
For another location, set `convexDir` on **each enabled rule**:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "cvx/no-raw-builders": ["error", { "convexDir": "src/server/convex" }] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Rules ignore files outside that directory, `_generated/`, `__tests__/`, and
|
|
40
|
+
files ending in `.test` or `.spec` before their JS/TS extension. They do not
|
|
41
|
+
offer automatic fixes for architectural moves.
|
|
42
|
+
|
|
43
|
+
## Coverage
|
|
44
|
+
|
|
45
|
+
Every name below has the `cvx/` prefix in lint configuration.
|
|
46
|
+
|
|
47
|
+
| Rule | Detects | Documentation |
|
|
48
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- |
|
|
49
|
+
| `root-facade-ownership` | `createAuthFunctions`, `createTriggers`, and `new Foundation` outside their root files, or repeated in one file. | `conventions.md`, Root files |
|
|
50
|
+
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules |
|
|
51
|
+
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
52
|
+
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules |
|
|
53
|
+
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
54
|
+
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
55
|
+
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
56
|
+
| `no-unbounded-reads` | Direct `ctx.db.query(...).collect()` chains and `ctx.include(...).resolve()` chains in host code. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
57
|
+
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
58
|
+
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
59
|
+
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
60
|
+
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
61
|
+
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
62
|
+
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
63
|
+
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
64
|
+
|
|
65
|
+
## Allowed patterns
|
|
66
|
+
|
|
67
|
+
- `functions.ts` imports raw builders to configure `createAuthFunctions`.
|
|
68
|
+
Type-only imports such as `MutationCtx` from `_generated/server` are valid
|
|
69
|
+
elsewhere. `httpAction` remains available for HTTP routing.
|
|
70
|
+
- Domain modules may import sibling `rules.ts` and `queries.ts`, as explicitly
|
|
71
|
+
allowed by the module-grain section. Cross-module writes go through the
|
|
72
|
+
optional `application/` layer. `domain/table.ts` combines module table maps.
|
|
73
|
+
- API adapters may delegate and perform the documented bounded inline query
|
|
74
|
+
and DTO projection. `system*` constructors may live in domain modules.
|
|
75
|
+
- Authored components under `convex/components/<name>/` may use their own raw
|
|
76
|
+
builders and raw tables. Host-specific facade, timestamp, and bounded-read
|
|
77
|
+
rules do not apply there. Configuration imports for mounting components are
|
|
78
|
+
allowed from root `convex.config.ts`.
|
|
79
|
+
- Host schemas use kit table constructors in `domain/<module>/schema.ts` and
|
|
80
|
+
attach indexes in `domain/<module>/table.ts`. `schema.ts` assembles them.
|
|
81
|
+
- `archivedAt` remains application-controlled. Singular `db.get` reads and
|
|
82
|
+
bounded `execute`/`paginate` calls are allowed.
|
|
83
|
+
|
|
84
|
+
## What still needs review and tests
|
|
85
|
+
|
|
86
|
+
These are syntax and lexical-binding checks, not whole-program validation.
|
|
87
|
+
They resolve relative import paths, not tsconfig aliases, symlinks, or computed
|
|
88
|
+
paths. Named import aliases and namespaces are recognized where documented;
|
|
89
|
+
arbitrary variable reassignment, wrapper functions, destructured database
|
|
90
|
+
methods, and calls split across variables are not traced. Database checks
|
|
91
|
+
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
92
|
+
|
|
93
|
+
The plugin does not prove DTO redaction, complete command/audit coverage,
|
|
94
|
+
tenant/RLS correctness, unique table ownership across files, required facade
|
|
95
|
+
existence, exactly one component mount, numeric read limits, or vocabulary
|
|
96
|
+
tuple ownership. Keep the architecture tests from `conventions.md` and the
|
|
97
|
+
behavioral tests described in `maintainability.md`. A clean lint run is not
|
|
98
|
+
evidence of those runtime guarantees.
|
package/docs/tenancy.md
CHANGED
|
@@ -126,11 +126,10 @@ security: {
|
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
128
|
rules: (bundle) => ({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
|
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
|