cvx-kit 0.1.15 → 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 +59 -38
- 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 -337
- 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 -260
- package/docs/querying.md +0 -329
- 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 -298
- package/scripts/test-packed-auth.mjs +0 -72
- package/scripts/test-packed-effect.mjs +0 -482
- package/scripts/test-packed-ids.mjs +0 -150
- package/scripts/test-packed-oxlint.mjs +0 -167
- package/scripts/test-packed-querying.mjs +0 -185
package/docs/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.
|