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/ROADMAP.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# Roadmap to 1.0.0
|
|
2
|
-
|
|
3
|
-
Where cvx-kit is going and what "stable" will mean. Tracked with checkboxes;
|
|
4
|
-
each milestone ships as one minor version. From `0.1.0` on, **breaking
|
|
5
|
-
changes only land on minor bumps** (0.x semver: minor = breaking, patch =
|
|
6
|
-
fix/additive) and every release gets a CHANGELOG entry.
|
|
7
|
-
|
|
8
|
-
## 0.1.0 — the big one: API consolidation + the feature set
|
|
9
|
-
|
|
10
|
-
Goal: one release that both closes the breaking window AND ships the full
|
|
11
|
-
helper surface, so consumers adopt a complete kit once instead of chasing
|
|
12
|
-
minors. Everything here may still rename freely — that's why the features
|
|
13
|
-
belong in this milestone: they land already conforming to the frozen names.
|
|
14
|
-
|
|
15
|
-
### Consolidation (the last breaking pass)
|
|
16
|
-
|
|
17
|
-
- [ ] Decide the published name (`cvx-kit` is a working title) and, if it
|
|
18
|
-
changes, publish under the final name with `cvx-kit` deprecated.
|
|
19
|
-
- [ ] API audit: review every exported name, option key, and error code
|
|
20
|
-
against `conventions.md`'s own naming grammar; freeze the error-code
|
|
21
|
-
taxonomy (`KitError` codes) as a documented tuple.
|
|
22
|
-
- [x] Typed middleware context accumulation: `next({ context })` currently
|
|
23
|
-
merges loosely; tighten so enrichment widens the downstream ctx type
|
|
24
|
-
(TanStack-style), without breaking the loose form.
|
|
25
|
-
- [x] Introduce `CHANGELOG.md` + release notes on the GitHub tag; adopt the
|
|
26
|
-
0.x semver contract above. (GitHub Releases automated in release.yml.)
|
|
27
|
-
- [ ] CI hardening: add `--dry-run` pack + `attw` (Are The Types Wrong) next
|
|
28
|
-
to publint; bump the deprecated actions pins.
|
|
29
|
-
|
|
30
|
-
### Features
|
|
31
|
-
|
|
32
|
-
- [x] **Pagination bridge** — `paginated(table.publicDto)` arg/return
|
|
33
|
-
helpers over Convex's pagination validators, and an
|
|
34
|
-
`include(...).paginate(opts)` terminal next to `.execute(limit)`:
|
|
35
|
-
cursors, not just bounded `take`.
|
|
36
|
-
- [x] **CRUD command factory** — `createCrudCommands(table, policy)`
|
|
37
|
-
generating the standard create/update/archive operations from a
|
|
38
|
-
zodTable's masks: command input from `commandFields`, audit derivation
|
|
39
|
-
from the aggregate, timestamps/tenant already structural. The single
|
|
40
|
-
biggest remaining per-entity boilerplate.
|
|
41
|
-
- [x] **State-machine helper** — `createStateMachine(STATES, transitions)`
|
|
42
|
-
typed against the constants tuple, returning `isTransitionLegal` /
|
|
43
|
-
`assertTransition` that drop straight into a command `guard`.
|
|
44
|
-
- [x] **Rate-limit middleware factory** — the first official middleware:
|
|
45
|
-
`rateLimit({ key: (ctx) => ctx.tenant, ... })` over
|
|
46
|
-
`@convex-dev/rate-limiter`, packaged as `Command.middleware` /
|
|
47
|
-
`Query.middleware` and the template for future middleware.
|
|
48
|
-
- [x] **Webhook boundary helper** — signature verification + event dedup by
|
|
49
|
-
natural key (`${event}:${id}:${updatedAt}`) + delegation to a
|
|
50
|
-
`system*` function: the one boundary that genuinely needs dedup, as a
|
|
51
|
-
first-class helper.
|
|
52
|
-
- [x] **Agent tool generator** — `createAgentTools(table, executors)`
|
|
53
|
-
emitting `@convex-dev/agent` tool definitions from the existing
|
|
54
|
-
`tools.*` masks + `jsonSafeZid`, with handlers routed through the
|
|
55
|
-
command protocol — agents get audited commands, never raw db access.
|
|
56
|
-
|
|
57
|
-
Each feature ships with its own doc page (or section), tests on the real
|
|
58
|
-
Convex runtime where applicable, and an `llms.md` entry.
|
|
59
|
-
|
|
60
|
-
## 0.2.0 — guardrails as a product
|
|
61
|
-
|
|
62
|
-
Goal: the conventions the docs prescribe become importable tests, so a
|
|
63
|
-
consumer gets the origin app's discipline on day one — enforced against the
|
|
64
|
-
by-then-frozen 0.1.0 names.
|
|
65
|
-
|
|
66
|
-
- [ ] `createArchitectureTest(rules)` — glob-based rule runner with the
|
|
67
|
-
standard rules built in (no raw builders outside functions.ts, no
|
|
68
|
-
inline tables/enums, no sibling-domain imports, no deep component
|
|
69
|
-
imports, facades mount once), each rule fixture-proved, extensible
|
|
70
|
-
with app-specific rules.
|
|
71
|
-
- [ ] Tenancy exhaustiveness helper: assert every `tenantTable` is in the
|
|
72
|
-
registry and vice versa; assert internal builders accept no tenant arg.
|
|
73
|
-
- [ ] `llms.md` conformance check: an automated test that every export in
|
|
74
|
-
its tables exists with that signature (also an rc gate — build it here).
|
|
75
|
-
- [ ] Port the approvals copyability test (cp -r + tsc) into this repo's
|
|
76
|
-
suite as the permanent portability guarantee for both components.
|
|
77
|
-
|
|
78
|
-
## 0.3.0 — scaffolding
|
|
79
|
-
|
|
80
|
-
Goal: the first hour of a new app is generated, not hand-written.
|
|
81
|
-
|
|
82
|
-
- [ ] WorkOS scaffold generator: `auth.ts`, `auth.config.ts`, `http.ts`,
|
|
83
|
-
`functions.ts`, `foundation.ts`, `triggers.ts` from one command.
|
|
84
|
-
- [ ] Audit facade helpers over any `convex-audit-log` instance:
|
|
85
|
-
fingerprinted cursors, bounded windows, admin readers.
|
|
86
|
-
- [ ] Decide on the optional idempotency/replay component (deferred from
|
|
87
|
-
0.0.x — only if a consumer demonstrates the need beyond Convex's
|
|
88
|
-
exactly-once mutation delivery).
|
|
89
|
-
|
|
90
|
-
## 1.0.0-rc — proving it
|
|
91
|
-
|
|
92
|
-
Entry gates, not work items. The rc ships when all are true:
|
|
93
|
-
|
|
94
|
-
- [ ] Two real applications run the latest kit in production-like use for
|
|
95
|
-
several weeks (the multi-tenant integration and one more).
|
|
96
|
-
- [ ] One full minor cycle with zero breaking changes needed.
|
|
97
|
-
- [ ] Docs complete and verified: the `llms.md` conformance check (built in
|
|
98
|
-
0.2.0) green in CI.
|
|
99
|
-
- [ ] CI matrix: bun + node LTS, convex peer-range min and latest.
|
|
100
|
-
- [ ] Public-API test coverage: every exported runtime symbol exercised by
|
|
101
|
-
at least one test.
|
|
102
|
-
|
|
103
|
-
## 1.0.0
|
|
104
|
-
|
|
105
|
-
- [ ] Freeze the export surface; document the semver contract (what counts
|
|
106
|
-
as breaking, deprecation policy of one minor with warnings).
|
|
107
|
-
- [ ] `MIGRATING.md` from 0.x.
|
|
108
|
-
- [ ] Provenance, README badges, and a docs site or expanded README landing.
|
|
109
|
-
|
|
110
|
-
## Candidates (recorded, not committed)
|
|
111
|
-
|
|
112
|
-
- Observability sink adapters: OTel/Axiom-shaped formatters for the
|
|
113
|
-
injectable `emit`, so `command.execution` events land in real telemetry
|
|
114
|
-
without per-consumer mapping.
|
|
115
|
-
- Migration companion: a thin `@convex-dev/migrations` wrapper aligned with
|
|
116
|
-
zodTable epochs ("make optional field required after backfill" as a
|
|
117
|
-
declared step), closing the loop with `migration.md`.
|
|
118
|
-
|
|
119
|
-
## Explicitly out of scope for 1.0
|
|
120
|
-
|
|
121
|
-
- General idempotency store (platform already covers single-call
|
|
122
|
-
exactly-once; boundary dedup is the webhook helper's job).
|
|
123
|
-
- Frontend helpers/React bindings.
|
|
124
|
-
- Multi-provider auth abstractions beyond the injected-policy seam
|
|
125
|
-
(`createAuthFunctions` already accepts any provider via `getAuthUser`/
|
|
126
|
-
`mapRole`/`verifyMembership`).
|
package/docs/agent-tools.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# `cvx-kit/agent-tools` — agents get audited commands
|
|
2
|
-
|
|
3
|
-
`createAgentTools` turns a table's tool masks into framework-agnostic tool
|
|
4
|
-
records `{ name, description, args, handler }` — directly spreadable into
|
|
5
|
-
`@convex-dev/agent`'s `createTool`, adaptable to ai-sdk `tool()`, and the
|
|
6
|
-
kit depends on neither (shape checked against `@convex-dev/agent@0.6.x`;
|
|
7
|
-
re-verify at install).
|
|
8
|
-
|
|
9
|
-
The point is the routing, not the shape: **mutation handlers go through
|
|
10
|
-
command executors**, so every agent action is validated, guarded, audited,
|
|
11
|
-
and observed. An agent never gets raw db access.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { createAgentTools } from 'cvx-kit/agent-tools'
|
|
15
|
-
|
|
16
|
-
const tools = createAgentTools(<entities>, {
|
|
17
|
-
// mutations — audited commands (createCrudCommands executors fit exactly):
|
|
18
|
-
create: (ctx, input) => crud.executeCreate(ctx, input),
|
|
19
|
-
update: (ctx, input) => crud.executeUpdate(ctx, input),
|
|
20
|
-
archive: (ctx, input) => crud.executeArchive(ctx, input),
|
|
21
|
-
// reads — caller-supplied query handlers (reads don't route through commands):
|
|
22
|
-
get: (ctx, { id }) => loadDto(ctx, id),
|
|
23
|
-
list: (ctx, { paginationOpts }) => listPage(ctx, paginationOpts),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
// host-side, with @convex-dev/agent:
|
|
27
|
-
// const agentTools = Object.fromEntries(Object.values(tools).map((t) =>
|
|
28
|
-
// [t.name, createTool({ description: t.description, args: t.args, handler: t.handler })]))
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Contracts
|
|
32
|
-
|
|
33
|
-
- **Args are the table masks, no drift**: `create` = `tools.insert`
|
|
34
|
-
(`commandFields`), `update` = `tools.update`, `archive`/`get` = `tools.id`,
|
|
35
|
-
`list` = the pagination opts boundary. Ids present as plain strings in
|
|
36
|
-
generated JSON schemas (`jsonSafeZid`) while keeping `Id<...>` types.
|
|
37
|
-
- **Only the kinds you supply handlers for are emitted**; names are
|
|
38
|
-
`<tableName>_<verb>`; descriptions are derived and overridable via
|
|
39
|
-
`options.descriptions`.
|
|
40
|
-
- DTO discipline applies to reads: `get`/`list` handlers should return
|
|
41
|
-
`toPublicDto`-projected data, same as any public query.
|
package/docs/approvals.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# `cvx-kit/components/approvals` — declarative approval workflows
|
|
2
|
-
|
|
3
|
-
A self-contained Convex component for human-in-the-loop approval flows:
|
|
4
|
-
declarative step lists (decision / branch / mutation / action / notify) with
|
|
5
|
-
count quorum, maker-checker, expiry, compatibility keys, durable execution via
|
|
6
|
-
a **nested** `@convex-dev/workflow`, and its own audit trail via a **nested**
|
|
7
|
-
`convex-audit-log`. The component never imports host code: tenancy is an
|
|
8
|
-
opaque `scopeRef`, resources are opaque `resourceType`/`resourceRef` strings,
|
|
9
|
-
and callbacks cross the boundary as **function handles**.
|
|
10
|
-
|
|
11
|
-
## Mounting
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
// convex/convex.config.ts
|
|
15
|
-
import approvals from 'cvx-kit/components/approvals/convex.config'
|
|
16
|
-
app.use(approvals)
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
After installation or upgrade, deploy once and run the component health query
|
|
20
|
-
from a host wrapper:
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
return ctx.runQuery(components.approvals.health.check, {})
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
It returns the installed approvals schema version and required index names. It
|
|
27
|
-
also executes reads through
|
|
28
|
-
`approvalDecisions.by_runId_and_decidedAt` and
|
|
29
|
-
`approvalDecisions.by_runId_and_stepKey_and_actor_actorRef`, failing with an
|
|
30
|
-
installation-specific diagnostic if the packaged component schema was not
|
|
31
|
-
deployed.
|
|
32
|
-
|
|
33
|
-
The component mounts its own private `workflow` and `auditLog` children. Do
|
|
34
|
-
**not** re-register those children at the app root under the same identity,
|
|
35
|
-
and never reach into `components.approvals.workflow` / `.auditLog` from host
|
|
36
|
-
code — the `Approvals` client class is the only contract.
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
// convex/approvals.ts — host facade, declared once
|
|
40
|
-
import { Approvals } from 'cvx-kit/components/approvals'
|
|
41
|
-
import { components } from './_generated/api'
|
|
42
|
-
|
|
43
|
-
export const approvals = new Approvals(components.approvals)
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Defining a workflow — in the owning domain
|
|
47
|
-
|
|
48
|
-
Workflow _definitions_ are business logic and live in the domain that owns the
|
|
49
|
-
resource, not in the root facade:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
// convex/domain/documents/approval.ts
|
|
53
|
-
import { approvals } from '../../approvals'
|
|
54
|
-
import { internal } from '../../_generated/api'
|
|
55
|
-
|
|
56
|
-
export const publishApproval = approvals.define({
|
|
57
|
-
name: 'documentPublish',
|
|
58
|
-
steps: [
|
|
59
|
-
approvals.decision('managerDecision', {
|
|
60
|
-
decisions: ['approved', 'rejected'],
|
|
61
|
-
quorum: { kind: 'count', approvals: 1 },
|
|
62
|
-
makerChecker: true, // requester cannot decide
|
|
63
|
-
expiresAfterMs: 7 * 24 * 60 * 60 * 1000,
|
|
64
|
-
}),
|
|
65
|
-
approvals.branch('applyDecision', {
|
|
66
|
-
approvedStepKey: 'publishDocument',
|
|
67
|
-
rejectedStepKey: 'notifyRejected',
|
|
68
|
-
}),
|
|
69
|
-
approvals.mutation('publishDocument', {
|
|
70
|
-
handler: internal.domain.documents.approval_functions.applyDecision,
|
|
71
|
-
}),
|
|
72
|
-
approvals.notify('notifyRejected', {
|
|
73
|
-
handler: internal.domain.documents.approval_functions.notifyRejected,
|
|
74
|
-
retry: true,
|
|
75
|
-
}),
|
|
76
|
-
],
|
|
77
|
-
})
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Step kinds
|
|
81
|
-
|
|
82
|
-
| Step | Runs | Notes |
|
|
83
|
-
| ---------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------- |
|
|
84
|
-
| `decision(key, { decisions, quorum, makerChecker?, expiresAfterMs? })` | waits for humans | quorum is `{ kind: 'count', approvals: n }`; expiry resolves the run as `expired` |
|
|
85
|
-
| `branch(key, { approvedStepKey, rejectedStepKey })` | routing | jumps by the preceding decision's outcome |
|
|
86
|
-
| `mutation(key, { handler, retry? })` | internal mutation handle | transactional apply step |
|
|
87
|
-
| `action(key, { handler, retry? })` | internal action handle | side effects (third parties) |
|
|
88
|
-
| `notify(key, { handler, retry? })` | internal action handle | semantically a notification; same contract as action |
|
|
89
|
-
|
|
90
|
-
Limits (enforced by the component's validators): ≤ 32 steps per workflow,
|
|
91
|
-
≤ 8 decisions per decision step, names ≤ 96 chars, references ≤ 256 chars,
|
|
92
|
-
metadata ≤ 32 entries (keys ≤ 64, values ≤ 512 chars).
|
|
93
|
-
|
|
94
|
-
### Compatibility keys
|
|
95
|
-
|
|
96
|
-
`compatibilityKey` (defaults to `name`) versions the workflow shape. Every
|
|
97
|
-
`decide`/`cancel`/`status`/`restart` call sends it, so a run started under an
|
|
98
|
-
old definition cannot be driven by an incompatible new one. When you change a
|
|
99
|
-
workflow's steps in a breaking way, change the key.
|
|
100
|
-
|
|
101
|
-
## Driving a run
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
// start — from an authMutation, with ctx.actor as the requester
|
|
105
|
-
const { runId } = await publishApproval.start(ctx, {
|
|
106
|
-
scopeRef: ctx.org.organizationId, // opaque tenancy
|
|
107
|
-
resourceType: 'document',
|
|
108
|
-
resourceRef: documentId, // opaque to the component
|
|
109
|
-
requester: ctx.actor,
|
|
110
|
-
metadata: { title }, // bounded string map
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// decide — a manager approves or rejects
|
|
114
|
-
await publishApproval.decide(ctx, { runId, decision: 'approved', reason }, ctx.actor)
|
|
115
|
-
|
|
116
|
-
// observe from a host query
|
|
117
|
-
await publishApproval.status(ctx, runId) // pending | approved | rejected | expired | canceled
|
|
118
|
-
await publishApproval.evidence(ctx, runId) // per-step decision evidence
|
|
119
|
-
await publishApproval.list(ctx, {
|
|
120
|
-
scopeRef,
|
|
121
|
-
state: 'pending',
|
|
122
|
-
paginationOpts: { cursor: null, numItems: 20 },
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
// manage
|
|
126
|
-
await publishApproval.cancel(ctx, runId, ctx.actor)
|
|
127
|
-
await publishApproval.restart(ctx, runId)
|
|
128
|
-
await approvals.cleanupAudit(ctx, { olderThanDays: 365, batchSize: 100 })
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Callback handlers — validate everything coming back
|
|
132
|
-
|
|
133
|
-
Step handlers are **internal** functions (build them with `systemMutation` /
|
|
134
|
-
`systemAction`). They receive `ApprovalCallbackInput`: `{ runId, scopeRef,
|
|
135
|
-
resourceType, resourceRef, metadata?, decision? }` — all opaque strings.
|
|
136
|
-
Because the component cannot know your tables, the handler must re-establish
|
|
137
|
-
trust:
|
|
138
|
-
|
|
139
|
-
```ts
|
|
140
|
-
import { approvalCallbackArgs } from 'cvx-kit/components/approvals'
|
|
141
|
-
|
|
142
|
-
export const applyDecision = systemMutation({
|
|
143
|
-
args: approvalCallbackArgs,
|
|
144
|
-
handler: async (ctx, input) => {
|
|
145
|
-
if (input.resourceType !== 'document') throw new Error('WRONG_RESOURCE')
|
|
146
|
-
const id = ctx.db.normalizeId('documents', input.resourceRef)
|
|
147
|
-
if (!id) throw new Error('BAD_REF')
|
|
148
|
-
const document = await ctx.db.get(id)
|
|
149
|
-
// guard against stale runs: the doc must still point at this run
|
|
150
|
-
if (document?.approvalRunId !== input.runId) return
|
|
151
|
-
await ctx.db.patch(id, { reviewState: 'published' })
|
|
152
|
-
},
|
|
153
|
-
})
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
That `approvalRunId` check is the idempotency/staleness guard from the origin
|
|
157
|
-
app — copy it.
|
|
158
|
-
|
|
159
|
-
For branch callbacks, `decision.evidence` contains
|
|
160
|
-
`{ stepKey, actorRef, decision, reason?, decidedAt }` items. Use the exported
|
|
161
|
-
`approvalCallbackArgs` validator instead of duplicating that protocol boundary.
|
|
162
|
-
|
|
163
|
-
### Paginated inboxes
|
|
164
|
-
|
|
165
|
-
Expose the configured workflow through a host query; component pagination uses
|
|
166
|
-
`convex-helpers` internally because Convex's built-in `.paginate()` is not
|
|
167
|
-
available inside components:
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
export const approvalInbox = authQuery({
|
|
171
|
-
args: {
|
|
172
|
-
state: z.enum(['pending', 'approved', 'rejected', 'expired', 'canceled']).optional(),
|
|
173
|
-
paginationOpts: convexToZod(paginationOptsValidator),
|
|
174
|
-
},
|
|
175
|
-
handler: (ctx, args) =>
|
|
176
|
-
publishApproval.list(ctx, {
|
|
177
|
-
scopeRef: ctx.org.organizationId,
|
|
178
|
-
...args,
|
|
179
|
-
}),
|
|
180
|
-
})
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
The result keeps Convex's standard pagination shape, so React consumers can
|
|
184
|
-
call `usePaginatedQuery(api.approvals.approvalInbox, { state: 'pending' },
|
|
185
|
-
{ initialNumItems: 20 })`. Pass each returned cursor only back to the same
|
|
186
|
-
scope/state query.
|
|
187
|
-
|
|
188
|
-
## Rules and footguns
|
|
189
|
-
|
|
190
|
-
1. Root facade (`convex/approvals.ts`) instantiates the client; workflow
|
|
191
|
-
_definitions_ live in the owning domain.
|
|
192
|
-
2. Handlers get opaque strings — always `normalizeId`, re-check
|
|
193
|
-
`resourceType`, and verify the resource still references the run.
|
|
194
|
-
3. `makerChecker: true` means the requester's decisions are rejected — design
|
|
195
|
-
your UI accordingly.
|
|
196
|
-
4. The component keeps its own audit trail (retention category
|
|
197
|
-
`approval-protocol`, cleanup via `cleanupAudit`, retention 1–3650 days,
|
|
198
|
-
batch ≤ 100). Host-side business audit still goes through your command
|
|
199
|
-
protocol — they are different trails.
|
|
200
|
-
5. Changing steps without changing `compatibilityKey` lets old runs be driven
|
|
201
|
-
by a new incompatible shape; bump the key on breaking changes.
|
package/docs/architecture.md
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# Architecture — the shape of a cvx-kit application
|
|
2
|
-
|
|
3
|
-
cvx-kit was extracted from a production Convex application (~12k LOC backend).
|
|
4
|
-
This document describes the architecture that application follows and that the
|
|
5
|
-
kit is designed to reproduce: strict ownership boundaries, injected policy,
|
|
6
|
-
portable components, and conventions that are _executable_ (enforced by tests)
|
|
7
|
-
rather than aspirational.
|
|
8
|
-
|
|
9
|
-
## The layers
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
convex/
|
|
13
|
-
convex.config.ts # component mounting — each app.use(x) exactly once
|
|
14
|
-
schema.ts # defineSchema(domainTables) — nothing else
|
|
15
|
-
functions.ts # createAuthFunctions(...) — the ONLY place raw builders appear
|
|
16
|
-
foundation.ts # const { Command, Query, observability } = createEffectFoundation(...)
|
|
17
|
-
approvals.ts # const approvals = new Approvals(components.approvals)
|
|
18
|
-
audit.ts # writeAuditEntry over convex-audit-log + admin readers
|
|
19
|
-
triggers.ts # the single Triggers registry + helper registrations
|
|
20
|
-
auth.ts / auth.config.ts / http.ts # identity provider wiring (e.g. WorkOS AuthKit)
|
|
21
|
-
api/ # PUBLIC surface — thin adapters only
|
|
22
|
-
<documents>.ts # authQuery/authMutation wrapping domain executors
|
|
23
|
-
domain/
|
|
24
|
-
table.ts # merges per-domain table maps into domainTables
|
|
25
|
-
shared/ # genuinely cross-entity helpers only
|
|
26
|
-
<entity>/ # one directory per entity/aggregate
|
|
27
|
-
schema.ts # zodTable declaration (shapes only)
|
|
28
|
-
table.ts # indexes attached to <zodTable>.table
|
|
29
|
-
constants.ts # finite vocabularies (UPPER_SNAKE readonly tuples)
|
|
30
|
-
commands.ts # Command({ context, operations }) with handlers inside command({...})
|
|
31
|
-
queries.ts # domain read logic
|
|
32
|
-
approval.ts # approvals.define(...) workflow definitions
|
|
33
|
-
*_functions.ts # internal callbacks (systemMutation/systemAction)
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Layer rules
|
|
37
|
-
|
|
38
|
-
1. **`api/` is the only public surface.** It contains no bare `query(` or
|
|
39
|
-
`mutation(` — only `auth*`/`role*`/`admin*` constructors delegating to
|
|
40
|
-
domain `execute*` functions. Public functions are adapters: parse, pass
|
|
41
|
-
`ctx.actor`, project DTOs.
|
|
42
|
-
2. **`domain/<entity>/` owns everything about the entity** — its zod shape,
|
|
43
|
-
indexes, vocabularies, commands, queries, workflows. Cross-domain reuse
|
|
44
|
-
goes through `domain/shared/`, never sibling imports of another domain's
|
|
45
|
-
internals.
|
|
46
|
-
3. **Root files are facades over mounted components.** `foundation.ts`,
|
|
47
|
-
`approvals.ts`, `audit.ts` configure infrastructure; business definitions
|
|
48
|
-
stay in the owning domain.
|
|
49
|
-
4. **Internal functions use `systemMutation`/`systemAction`** — never raw
|
|
50
|
-
`internalMutation(`/`internalAction(` — so triggers and `include` apply to
|
|
51
|
-
scheduled/callback work too.
|
|
52
|
-
|
|
53
|
-
## Injection, not import
|
|
54
|
-
|
|
55
|
-
The kit's central design rule: **library code never imports app singletons**.
|
|
56
|
-
Every kit surface takes its policy as a parameter:
|
|
57
|
-
|
|
58
|
-
- `createAuthFunctions` receives the generated builders, `getAuthUser`,
|
|
59
|
-
`mapRole`, `verifyMembership`, `triggers`, `errors`.
|
|
60
|
-
- `createEffectFoundation` receives `classifyError`, `writeAudit`, the `enabled` flag.
|
|
61
|
-
- `Approvals` receives only its component reference; host data crosses as
|
|
62
|
-
opaque validated strings.
|
|
63
|
-
|
|
64
|
-
The reverse rule holds for components: a portable component must not import
|
|
65
|
-
host domains, generated host APIs, environment configuration, or vendor SDKs
|
|
66
|
-
(WorkOS, Drizzle, Redis…). Host values cross its public API only as
|
|
67
|
-
**validated, bounded, opaque data** — `scopeRef`, `resourceRef`, function
|
|
68
|
-
handles. Component schemas contain only their own records and never mirror a
|
|
69
|
-
private child component's tables.
|
|
70
|
-
|
|
71
|
-
## Boundaries as schemas
|
|
72
|
-
|
|
73
|
-
Data crossing any boundary is a zod schema derived from one `zodTable`
|
|
74
|
-
declaration per entity (see `zod-table.md`):
|
|
75
|
-
|
|
76
|
-
- storage → `defineTable` via `zodToConvex`
|
|
77
|
-
- writes → `insertSchema`/`updateSchema` (server fields and timestamps
|
|
78
|
-
excluded)
|
|
79
|
-
- commands → `commandInput` (explicit allowlist)
|
|
80
|
-
- clients → `publicDto` + `toPublicDto` (runtime redaction)
|
|
81
|
-
- LLM tools → `tools.*` and `jsonSafeZid`
|
|
82
|
-
|
|
83
|
-
Everything is `.strict()`. There is exactly one source of truth per entity and
|
|
84
|
-
no boundary that "just trusts" its input.
|
|
85
|
-
|
|
86
|
-
## Writes as commands
|
|
87
|
-
|
|
88
|
-
State changes flow through the audited command protocol (see `commands.md`):
|
|
89
|
-
parse input → observe → run handler → parse output → write audit — one
|
|
90
|
-
transaction, rollback-consistent. Classifications and audit derivation are
|
|
91
|
-
type-required per operation. Human-gated changes use the approvals component;
|
|
92
|
-
its apply steps are commands too.
|
|
93
|
-
|
|
94
|
-
## Conventions worth enforcing with tests
|
|
95
|
-
|
|
96
|
-
The origin app enforces its architecture with a test suite that raw-globs the
|
|
97
|
-
source and asserts violations — each rule proved against a synthetic fixture
|
|
98
|
-
first so the checker can't silently pass. Highly recommended for consumers:
|
|
99
|
-
|
|
100
|
-
- **No inline tables/schemas** — `defineTable`/`defineSchema` only in
|
|
101
|
-
`table.ts`/`schema.ts`.
|
|
102
|
-
- **No inline enums** — `z.enum([...])` with an inline array literal is
|
|
103
|
-
banned; vocabularies are named `UPPER_SNAKE` tuples in `constants.ts`.
|
|
104
|
-
- **No deep server imports** — components consumed only through their
|
|
105
|
-
`client.ts` facade; explicit allowlist for exceptions.
|
|
106
|
-
- **No handwritten references** — `makeFunctionReference` banned; use
|
|
107
|
-
generated `internal`/`api`.
|
|
108
|
-
- **No sibling-domain imports** — a domain reaches another only via
|
|
109
|
-
`domain/shared/` or the public API.
|
|
110
|
-
- **Facades mount once** — every `app.use(x)` appears exactly once; facades
|
|
111
|
-
never reach a component's private children.
|
|
112
|
-
- **Component boundary tests** — no host imports, no
|
|
113
|
-
`process.env`/vendor-SDK references inside `components/*`.
|
|
114
|
-
- **Copyability** — copy the component to a tmpdir with only its package
|
|
115
|
-
deps, run `tsc`: proves portability continuously.
|
|
116
|
-
|
|
117
|
-
## Component mounting order
|
|
118
|
-
|
|
119
|
-
In `convex.config.ts`, mount infrastructure before consumers: auth kit →
|
|
120
|
-
foundation → audit log → approvals → named workflow/workpool instances. Use
|
|
121
|
-
`app.use(x, { name })` when mounting the same component twice; `components.
|
|
122
|
-
<name>` keys off that name. Required env vars can be declared on
|
|
123
|
-
`defineApp({ env: { ... } })` so deploys fail fast — remember to stub them in
|
|
124
|
-
test setup.
|