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.
Files changed (45) hide show
  1. package/README.md +59 -38
  2. package/dist/relations.mjs +1 -1
  3. package/dist/relations.mjs.map +1 -1
  4. package/dist/zod-table.d.mts +59 -2
  5. package/dist/zod-table.mjs +1 -1
  6. package/dist/zod-table.mjs.map +1 -1
  7. package/package.json +10 -3
  8. package/AGENTS.md +0 -47
  9. package/CHANGELOG.md +0 -337
  10. package/ROADMAP.md +0 -126
  11. package/docs/agent-tools.md +0 -41
  12. package/docs/approvals.md +0 -201
  13. package/docs/architecture.md +0 -124
  14. package/docs/auth.md +0 -443
  15. package/docs/command-context.md +0 -150
  16. package/docs/commands-legacy.md +0 -513
  17. package/docs/commands.md +0 -173
  18. package/docs/conventions.md +0 -345
  19. package/docs/crud.md +0 -66
  20. package/docs/domain-contracts.md +0 -148
  21. package/docs/domain-operations.md +0 -615
  22. package/docs/effect.md +0 -403
  23. package/docs/llms.md +0 -324
  24. package/docs/maintainability.md +0 -124
  25. package/docs/migration.md +0 -351
  26. package/docs/oxlint.md +0 -260
  27. package/docs/querying.md +0 -329
  28. package/docs/tenancy.md +0 -176
  29. package/docs/triggers.md +0 -77
  30. package/docs/upgrading.md +0 -287
  31. package/docs/webhooks.md +0 -58
  32. package/docs/zod-table.md +0 -278
  33. package/scripts/convex-config-bridge.mjs +0 -44
  34. package/scripts/convex-smoke-env.mjs +0 -33
  35. package/scripts/extract-changelog.mjs +0 -29
  36. package/scripts/owned-process.mjs +0 -140
  37. package/scripts/owned-process.test.mjs +0 -107
  38. package/scripts/smoke-packed-components.mjs +0 -341
  39. package/scripts/smoke-packed-effect.mjs +0 -439
  40. package/scripts/smoke-packed-querying.mjs +0 -298
  41. package/scripts/test-packed-auth.mjs +0 -72
  42. package/scripts/test-packed-effect.mjs +0 -482
  43. package/scripts/test-packed-ids.mjs +0 -150
  44. package/scripts/test-packed-oxlint.mjs +0 -167
  45. 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.
@@ -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.