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.
Files changed (51) hide show
  1. package/README.md +64 -45
  2. package/dist/querying/cursors.d.mts +5 -0
  3. package/dist/querying/cursors.mjs +1 -1
  4. package/dist/querying/cursors.mjs.map +1 -1
  5. package/dist/querying.d.mts +20 -6
  6. package/dist/querying.mjs +1 -1
  7. package/dist/querying.mjs.map +1 -1
  8. package/dist/relations.mjs +1 -1
  9. package/dist/relations.mjs.map +1 -1
  10. package/dist/zod-table.d.mts +59 -2
  11. package/dist/zod-table.mjs +1 -1
  12. package/dist/zod-table.mjs.map +1 -1
  13. package/package.json +10 -3
  14. package/AGENTS.md +0 -47
  15. package/CHANGELOG.md +0 -324
  16. package/ROADMAP.md +0 -126
  17. package/docs/agent-tools.md +0 -41
  18. package/docs/approvals.md +0 -201
  19. package/docs/architecture.md +0 -124
  20. package/docs/auth.md +0 -443
  21. package/docs/command-context.md +0 -150
  22. package/docs/commands-legacy.md +0 -513
  23. package/docs/commands.md +0 -173
  24. package/docs/conventions.md +0 -345
  25. package/docs/crud.md +0 -66
  26. package/docs/domain-contracts.md +0 -148
  27. package/docs/domain-operations.md +0 -615
  28. package/docs/effect.md +0 -403
  29. package/docs/llms.md +0 -324
  30. package/docs/maintainability.md +0 -124
  31. package/docs/migration.md +0 -351
  32. package/docs/oxlint.md +0 -254
  33. package/docs/querying.md +0 -293
  34. package/docs/tenancy.md +0 -176
  35. package/docs/triggers.md +0 -77
  36. package/docs/upgrading.md +0 -287
  37. package/docs/webhooks.md +0 -58
  38. package/docs/zod-table.md +0 -278
  39. package/scripts/convex-config-bridge.mjs +0 -44
  40. package/scripts/convex-smoke-env.mjs +0 -33
  41. package/scripts/extract-changelog.mjs +0 -29
  42. package/scripts/owned-process.mjs +0 -140
  43. package/scripts/owned-process.test.mjs +0 -107
  44. package/scripts/smoke-packed-components.mjs +0 -341
  45. package/scripts/smoke-packed-effect.mjs +0 -439
  46. package/scripts/smoke-packed-querying.mjs +0 -296
  47. package/scripts/test-packed-auth.mjs +0 -72
  48. package/scripts/test-packed-effect.mjs +0 -482
  49. package/scripts/test-packed-ids.mjs +0 -150
  50. package/scripts/test-packed-oxlint.mjs +0 -167
  51. package/scripts/test-packed-querying.mjs +0 -185
@@ -1,345 +0,0 @@
1
- # cvx-kit conventions — folder structure, file anatomy, and naming
2
-
3
- This is the kit's doctrine. Not a style suggestion: the structure below is
4
- what the kit's structural guarantees (auth, triggers, bounded reads, audited
5
- commands) assume, and what its packaged architecture rules enforce. Follow
6
- it as written; deviate only with a documented reason and a matching change to
7
- the tests.
8
-
9
- Names in `<angle brackets>` are placeholders for your vocabulary. Everything
10
- else — the folder roles, the file anatomy, the naming grammar — is fixed.
11
-
12
- ---
13
-
14
- ## 1. Folder structure
15
-
16
- ```
17
- convex/
18
- convex.config.ts
19
- schema.ts
20
- functions.ts
21
- triggers.ts
22
- foundation.ts
23
- relations.ts querying.ts (optional declarative relation/engine facades)
24
- <component>.ts (one per mounted component: audit.ts, approvals.ts, ...)
25
- auth.ts auth.config.ts http.ts crons.ts
26
- api/
27
- <entity>.ts
28
- domain/
29
- table.ts
30
- shared/
31
- <provider>.ts
32
- <entity>/
33
- schema.ts table.ts constants.ts commands.ts queries.ts
34
- rules.ts shared.ts actions.ts
35
- <workflow>.ts <workflow>_functions.ts
36
- __tests__/
37
- application/ (optional cross-module orchestration only)
38
- migrations/ (optional backfills and migrations)
39
- _generated/ (generated; exempt)
40
- components/ (only if you author portable components)
41
- <name>/
42
- __tests__/
43
- setup.ts facades.test.ts
44
- ```
45
-
46
- Three zones, three privileges:
47
-
48
- - **Root** — configuration and facades. May import kit modules, generated
49
- code, and component clients. May NOT export public functions or contain
50
- business decisions.
51
- - **`api/`** — the public surface. May import `functions.ts` constructors and
52
- its entity's domain modules. May NOT contain business logic, private
53
- helpers, or reads/writes beyond delegation.
54
- - **`domain/`** — all business logic. May import `functions.ts`,
55
- `foundation.ts`, facades, `domain/shared/`, and its own entity directory.
56
- May NOT import `api/`, `application/`, migrations, sibling implementations
57
- (except the rules/queries contract below), `_generated/server` builders, or
58
- component internals.
59
-
60
- Dependency direction is one-way: `api → domain → shared → kit`. Anything
61
- pointing the other way is a violation.
62
-
63
- ### Relational query facades
64
-
65
- When using the querying engine, root `relations.ts` exports one declarative
66
- `defineRelations(...)` registry and root `querying.ts` exports one configured
67
- `createQueryEngine(...)` facade. Establish native indexes before compiling the
68
- registry. Keep database execution and business predicates in domain Query
69
- handlers, and derive pagination/result validators in domain `contracts.ts`.
70
- Root facades contain configuration, not handlers or business branches.
71
- See [querying.md](./querying.md) for complete usage and [oxlint.md](./oxlint.md)
72
- for facade ownership and pagination diagnostics.
73
-
74
- ### Module grain
75
-
76
- `domain/<entity>/` is really `domain/<module>/`: the directory is a **bounded
77
- context that may own several related tables**, not necessarily one. Two
78
- tables belong in the same module when a single command routinely writes both,
79
- or when one's vocabulary is meaningless without the other; otherwise they are
80
- separate modules. The file anatomy in §2 is unchanged — a module's
81
- `schema.ts` simply declares several `zodTable`s and its `table.ts` exports
82
- one merged map (combine maps in `domain/table.ts` with `createModule`, which
83
- rejects duplicate owners). A one-table module is the degenerate case, so
84
- small apps look exactly like before.
85
-
86
- Cross-module rules:
87
-
88
- - Sibling modules may import each other's `rules.ts` and `queries.ts`
89
- **only** — never `commands.ts`: one module never mutates another's state.
90
- - A workflow that must atomically write across modules lives in an optional
91
- `application/` layer above `domain/` (`application/<workflow>.ts`); `api/`
92
- adapters then import `application/*` and their own module, never a second
93
- module.
94
- - `application/` is not a service or helper layer. Each executable file must
95
- coordinate at least two domain modules through their `commands.ts`, `queries.ts`,
96
- or `rules.ts`, or generated `internal.domain` references. No direct database
97
- access, provider construction, validators, private helpers, or business branches.
98
- Keep a workflow owned by one module inside that module, including its integrations.
99
- - The root directory allowlist is exhaustive: `api`, `domain`, `application`,
100
- `migrations`, `components`, `__tests__`, and `_generated`. Do not add root
101
- `host`, `internal`, `lib`, `utils`, `services`, or `helpers` directories.
102
- - A module may contain nested `internal/`, `integrations/`, provider, or workflow
103
- directories. Its responsibilities stay local. Do not create unused scaffold files.
104
- - Application code must not import migrations. Migrations can coordinate data
105
- changes across modules, but remain isolated from ordinary execution.
106
-
107
- ---
108
-
109
- ## 2. File anatomy — what each file contains, exactly
110
-
111
- Every file has one job. The lists below are exhaustive: if content isn't
112
- listed for a file, it doesn't belong there.
113
-
114
- ### Universal file order
115
-
116
- Every source file reads from public purpose to private implementation:
117
-
118
- 1. Imports.
119
- 2. Exported API — types, schemas, constants, functions.
120
- 3. File-local types and constants supporting the implementation.
121
- 4. Private helpers as **named function declarations**, at the bottom.
122
-
123
- No helper implementations above the exported API. No helper
124
- arrow-function constants where a `function` declaration works. No barrel
125
- files (`index.ts` re-exports) anywhere inside `convex/`. No re-exporting
126
- another file's function to shorten an import.
127
-
128
- ### Root files
129
-
130
- | File | Contains | Never contains |
131
- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
132
- | `convex.config.ts` | `defineApp()`, one `app.use(...)` per component, `defineApp({ env })` declarations, `export default app` | anything else |
133
- | `schema.ts` | `defineSchema(domainTables)` | table definitions, validators |
134
- | `functions.ts` | the single `createAuthFunctions<DataModel>()` call and its exported constructors | handlers, business policy beyond the injected config |
135
- | `triggers.ts` | `createTriggers()`, `timestamps`/`appendOnly`/`noDelete` registrations, calls to per-entity `register<Entity>Triggers` | trigger _logic_ for a specific entity (that lives in the entity) |
136
- | `foundation.ts` | one `createEffectFoundation(...)` or `createFoundation(...)`, bound command/query exports | command definitions |
137
- | `<component>.ts` | `new <Client>(components.<name>)` + minimal admin plumbing | workflow/business definitions |
138
- | `http.ts` | route registration delegating request handling to the owning domain | webhook business logic |
139
- | `crons.ts` | `cronJobs()` declarations targeting `internal.domain.<entity>...` | handler logic |
140
-
141
- ### `api/<entity>.ts` — the adapter file
142
-
143
- One file per entity exposed to clients. Each exported function follows one
144
- template and stays within ~10 lines of glue:
145
-
146
- ```ts
147
- export const <verb> = authMutation({
148
- args: <entity>.commandInput.extend({ /* ids, etc. */ }),
149
- returns: <resultSchema>,
150
- handler: (ctx, args) => execute<Verb><Entity>(ctx, { ...args, actorId: ctx.actor.userId }),
151
- })
152
-
153
- export const list = authQuery({
154
- args: { limit: z.number() },
155
- returns: z.array(<entity>.publicDto),
156
- handler: (ctx, { limit }) =>
157
- ctx.include(ctx.db.query('<entities>'))
158
- .matching('by_<field>', (ix) => ix.eq('<field>', ctx.actor.userId))
159
- .execute(limit, (rows) => rows.map(<entity>.toPublicDto)),
160
- })
161
- ```
162
-
163
- Adapters do four things — parse (boundary schemas), authorize (constructor
164
- choice), delegate (`execute*` with `ctx.actor`), project (`toPublicDto`).
165
- The moment an adapter needs a private helper or a business `if`, that code
166
- moves to the domain.
167
-
168
- ### `domain/<entity>/schema.ts` — shapes only
169
-
170
- - The module's `zodTable(...)` declarations (one per owned table), with `serverFields`,
171
- `commandFields`, `publicFields` decided deliberately.
172
- - DTO compositions derived from the table's boundaries
173
- (`z.object({ ...<entity>.publicDto.shape, ... }).strict()`).
174
- - Inferred types (`export type <Entity> = z.infer<typeof <entity>.schema>`).
175
- - NO indexes, NO `defineTable`, NO enum literals (consume `constants.ts`).
176
-
177
- ### `domain/<entity>/table.ts` — topology only
178
-
179
- ```ts
180
- export const <entity>Tables = {
181
- <entities>: <entity>.table
182
- .index('by_<field>', ['<field>'])
183
- .index('by_<field>_and_<field2>', ['<field>', '<field2>']),
184
- }
185
- ```
186
-
187
- Registered in `domain/table.ts`, which merges all entity maps into
188
- `domainTables`. Shapes change with the data model; indexes change with query
189
- patterns — that's why they are separate files.
190
-
191
- ### `domain/<entity>/constants.ts` — vocabularies
192
-
193
- Every finite value set the entity owns, one owner per vocabulary:
194
-
195
- ```ts
196
- export const <ENTITY>_STATES = ['<a>', '<b>', '<c>'] as const
197
- export type <Entity>State = (typeof <ENTITY>_STATES)[number]
198
- ```
199
-
200
- Schemas, validators, filters, transition maps, and tests consume the tuple.
201
- Transition maps are typed against the value type so an invalid literal cannot
202
- compile. Numeric limits and durations the entity owns also live here.
203
-
204
- ### `domain/<entity>/commands.ts` — every state change
205
-
206
- 1. One `Command({ context, operations: ({ command }) => ({ ... }) })` using
207
- the bound constructor from `createEffectFoundation` (`cvx-kit/effect`).
208
- 2. Each entry declares `command({ input, result, classification, handler, audit })`,
209
- with keys `'<entity>.<verb>'`. Handlers receive `(input, context)`.
210
- 3. Exported domain functions return `commands.exec(operation, input, host)`;
211
- the shared Effect API builder executes that Effect at the authenticated boundary.
212
- 4. Private handler helpers at the bottom.
213
-
214
- Audit derivation lives here, next to the operation it describes. No reads
215
- that belong in `queries.ts`; no pure predicates that belong in `rules.ts`.
216
- The Promise command facade is deprecated; see `commands-legacy.md` only for
217
- maintaining existing code. New implementations follow `commands.md`.
218
-
219
- ### `domain/<entity>/queries.ts` — domain reads
220
-
221
- Read logic worth reusing beyond one adapter: multi-step lookups, DTO
222
- assembly, cross-index selection. Bounded like everything else.
223
-
224
- ### `domain/<entity>/rules.ts` — pure invariants
225
-
226
- Predicates and transition checks with **no ctx and no db**: unit-testable
227
- business truth (`can<Verb>(doc, actor)`, `isTransitionLegal(from, to)`).
228
- Handlers call rules; rules never call handlers.
229
-
230
- ### `domain/<entity>/shared.ts` — entity-private helpers
231
-
232
- Code shared by two or more files _of this entity_. If a second entity needs
233
- it, it moves to `domain/shared/` — never a sibling import.
234
-
235
- ### `domain/<entity>/actions.ts` — external side effects
236
-
237
- Action-side logic owns external side effects. Put a provider used by one module
238
- in that module's `integrations/` or `providers/` directory. Share configured clients
239
- in `domain/shared/` only when multiple modules need them. Commands and queries
240
- must not perform network I/O; pure rules receive values instead of context or
241
- clock/random/environment dependencies.
242
-
243
- ### `domain/<entity>/<workflow>.ts` + `<workflow>_functions.ts`
244
-
245
- The workflow/approval definition the entity owns, and its internal callback
246
- targets built with `system*` constructors. Callbacks re-validate everything:
247
- `normalizeId`, `resourceType` check, staleness guard.
248
-
249
- ### `domain/shared/` — cross-entity, and only cross-entity
250
-
251
- Configured provider clients and infrastructure genuinely used by multiple modules.
252
- Shared infrastructure cannot import business modules. This directory is not a dumping
253
- ground: code with one consumer moves back to that consumer.
254
-
255
- ### Tests
256
-
257
- Every directory containing executable application source must have a direct
258
- `__tests__/` child with at least one `.test.ts` or `.spec.ts` file (JS variants
259
- also work). The same requirement covers `api/`, nested domain directories,
260
- `application/`, `migrations/`, and local components. Parent and child suites do
261
- not substitute for that directory's own tests.
262
-
263
- Generated code, declaration/type-only files, test fixtures inside `__tests__/`,
264
- and folders that only group child directories are exempt. `domain/table.ts` is
265
- an assembly exception; root wiring shares `convex/__tests__/`. Do not create
266
- empty directories or placeholder tests just to satisfy the checker.
267
-
268
- The package checks for a recognized, nonempty test callback outside skipped or
269
- TODO suites. It does not execute tests or prove coverage: run the application's
270
- behavioral tests in CI. Conditional test registration is not counted as the
271
- required runnable test. Keep fixtures and setup helpers under `__tests__/`.
272
-
273
- `cvx-kit/oxlint` supplies the structural checks. Consumers write behavioral tests,
274
- including meaningful facade/wiring tests where needed; they do not copy or
275
- maintain an architecture-testing framework.
276
-
277
- ---
278
-
279
- ## 3. Naming grammar
280
-
281
- | Thing | Convention | Example shape |
282
- | ---------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
283
- | Entity directory | plural camelCase, matches table name | `domain/<entities>/` |
284
- | Table | plural camelCase | `<entities>` |
285
- | Fields | camelCase; foreign keys `<other>Id`; timestamps only via the kit | `ownerId`, `archivedAt` |
286
- | Index | `by_<field>` / `by_<field>_and_<field>`, declaration order, nested paths flattened with `_` | `by_ownerId_and_state_status` |
287
- | Command operation | `<entity>.<verb>` lowercase dotted; must match `/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/` | `<entities>.approve` |
288
- | Error code | `UPPER_SNAKE`, matching `/^[A-Z][A-Z0-9_]{0,95}$/`, defined in one owner | `INVALID_REQUEST_BOUNDARY` |
289
- | Vocabulary tuple | `UPPER_SNAKE_CASE` `as const`; derived type PascalCase singular | `<ENTITY>_STATES` / `<Entity>State` |
290
- | Domain executor | `execute<Verb><Entity>` | `executeApprove<Entity>` |
291
- | Rule predicate | `can<Verb>` / `is<Condition>` | `canApprove`, `isTransitionLegal` |
292
- | Trigger registration | `register<Entity>Triggers(triggers)` | exported from the entity, called in root `triggers.ts` |
293
- | Internal-callback file | `<purpose>_functions.ts`, sibling of `<purpose>.ts` | `approval_functions.ts` |
294
- | zodTable export | camelCase plural, same as table | `export const <entities> = zodTable(...)` |
295
- | DTO schema | `<entity><Purpose>Dto` | `<entity>SummaryDto` |
296
- | Zod schema value | camelCase noun | `commandInput`, `publicDto` |
297
- | Type | PascalCase; no `I`/`T` prefixes | `<Entity>State` |
298
- | Test file | `<subject>.test.ts` in `__tests__/` | `commands.test.ts` |
299
- | Public function path | `api/<entity>:<fn>` — clients never call a root path | — |
300
-
301
- The two regexes are enforced at runtime: observability silently drops events
302
- whose operation/classification/errorCode don't match. Other naming conventions remain review guidance unless a documented package
303
- lint rule explicitly enforces them.
304
-
305
- ---
306
-
307
- ## 4. Import rules
308
-
309
- Allowed, per zone (anything not listed is forbidden):
310
-
311
- | From | May import |
312
- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
313
- | `api/<entity>.ts` | `functions.ts`, `domain/<entity>/*`, `domain/shared/*`, zod |
314
- | `domain/<entity>/*` | `functions.ts`, `foundation.ts`, component facades, `domain/shared/*`, own directory, generated `internal`/`api` (references only), kit modules |
315
- | `domain/shared/*` | kit modules and infrastructure shared by multiple modules; module-specific clients stay local |
316
- | root facades | kit modules, `components` from generated api |
317
- | `functions.ts` | `_generated/server` (the only file allowed to) |
318
- | `components/<name>/**` | its own directory + npm deps only — no host imports, no `process.env` |
319
-
320
- Forbidden everywhere: sibling-entity imports, `api/` from `domain/`,
321
- component internals (anything below a component's client facade),
322
- `makeFunctionReference`, barrel files.
323
-
324
- ---
325
-
326
- ## 5. Formatting and language
327
-
328
- - TypeScript strict; no `any` in exported signatures; `as never` only at kit
329
- seams that document it.
330
- - Zod objects that cross a boundary are `.strict()` — always.
331
- - `Object.freeze` for registries and long-lived configuration objects.
332
- - Comments state constraints the code can't (`// Optional only for rows
333
- created before <epoch>.`) — never narration of the next line.
334
- - One entity concept per file; when a file serves two purposes, split it
335
- along the anatomy in §2.
336
-
337
- Optional request context can be added through the existing Effect API adapter's
338
- `context` callback. Bind selected registries with `commands.withContext(value)`
339
- and consume `ctx.commands.exec(name, input)` through the application's existing
340
- function helper names. Keep dependencies local to each feature; explicit execution
341
- and input-only command handlers remain supported. See [optional context injection](./command-context.md).
342
-
343
- ### Reserved domain declarations
344
-
345
- Existing commands.ts and queries.ts each export one kit-backed Command or Query registry. Contracts.ts contains schemas, an optional defineDomainContract declaration, and an optional root error contract; handlers stay in their registries. See [Optional domain contracts](./domain-contracts.md).
package/docs/crud.md DELETED
@@ -1,66 +0,0 @@
1
- # `cvx-kit/crud` — the CRUD command factory
2
-
3
- > This factory uses the deprecated Promise command facade and remains available
4
- > for existing consumers. For new Effect commands, use `createEffectCrud` from
5
- > `cvx-kit/effect`; see [domain integrations](./domain-operations.md).
6
-
7
- `createCrudCommands` generates the three standard operations every entity
8
- starts with — create, update, archive — from one zodTable declaration,
9
- **fully inside the audited command pipeline**: strict command inputs from
10
- `commandFields`, mandatory audit with a declared aggregate type (enforced by
11
- the aggregates allowlist), observability, guards, and middleware.
12
-
13
- > convex-helpers ships a same-named `crud` helper. It was deliberately not
14
- > used here: it bypasses the command pipeline — no audits, no guards, and
15
- > hard deletes. This factory is the kit-doctrine equivalent.
16
-
17
- ## Usage
18
-
19
- ```ts
20
- import { createCrudCommands } from 'cvx-kit/crud'
21
- import { Command } from '../foundation' // the destructured facade
22
-
23
- const crud = createCrudCommands({
24
- Command,
25
- table: <entities>, // a zodTable / tenantTable
26
- aggregateType: '<entity>',
27
- classification: 'business', // default
28
- actor: (ctx) => ctx.actor.userId, // audited actor
29
- enrich: (ctx) => ({ tenant: ctx.tenant }), // server-owned stamps on create
30
- guards: { archive: (ctx, command) => machine.assert(...) }, // optional
31
- middleware: [rateLimit({ limiter, name: '<entities>.write' })], // optional
32
- })
33
-
34
- // thin api adapters:
35
- export const create = authMutation({
36
- args: <entities>.commandInput,
37
- handler: (ctx, args) => crud.executeCreate(ctx, args),
38
- })
39
- ```
40
-
41
- ## What gets generated
42
-
43
- | Operation | Input | Effect | Audit aggregate id |
44
- | ----------------- | ----------------------------------------------- | ------------------------------------ | ------------------ |
45
- | `<table>.create` | `commandInput` (strict) + `enrich` stamps | insert | the new row id |
46
- | `<table>.update` | `{ id, data: commandInput.partial() }` (strict) | patch | the row id |
47
- | `<table>.archive` | `{ id }` | sets `archivedAt` (soft delete ONLY) | the row id |
48
-
49
- No hard-delete operation is generated — kit doctrine (`noDelete` +
50
- `archivedAt`).
51
-
52
- ## Tenancy
53
-
54
- - `enrich` is **required for tenantTables** — the factory fails fast with
55
- `CRUD_ENRICH_REQUIRED` instead of letting untenanted rows ship. Stamp
56
- `tenant: ctx.tenant` there.
57
- - Generated update/archive take client-supplied ids: with `security.tenancy`
58
- configured, the RLS-wrapped writer rejects cross-tenant writes (this is
59
- covered by the kit's own tests) — but keep `requireTenantReference` in
60
- bespoke guards that load rows.
61
-
62
- ## Wiring agents
63
-
64
- The executors are the natural handlers for `createAgentTools` mutation
65
- kinds — agents get audited commands, never raw db access. See
66
- `agent-tools.md`.
@@ -1,148 +0,0 @@
1
- # Optional domain contracts
2
-
3
- Declare schemas and command classification once in `contracts.ts`, then attach
4
- the declaration to a Command or Query registry. Errors belong at the root beside
5
- commands and queries. Either section and the error contract may be omitted.
6
-
7
- The regular registry uses `createFoundation` from `cvx-kit`; the Effect registry
8
- uses `createEffectFoundation` from `cvx-kit/effect`. Both accept the same contract
9
- and handler-first implementation helpers. Existing inline Effect definitions and the
10
- older `new Foundation(...)` constructors continue to work.
11
-
12
- ```ts
13
- import { z } from 'zod'
14
- import { createFoundation } from 'cvx-kit'
15
- import { defineDomainContract } from 'cvx-kit/contracts'
16
- import { defineErrorContract } from 'cvx-kit/errors'
17
-
18
- const contract = defineDomainContract({
19
- errors: defineErrorContract({ Missing: { message: 'Task missing', details: {} } }),
20
- commands: {
21
- 'tasks.create': {
22
- input: z.string().transform(Number),
23
- result: z.number().transform((count) => ({ count })),
24
- classification: 'business',
25
- },
26
- 'tasks.close': {
27
- input: z.object({ id: z.string() }),
28
- result: z.boolean(),
29
- classification: 'business',
30
- },
31
- },
32
- queries: { 'tasks.get': { input: z.number(), result: z.number() } },
33
- })
34
- const { Command, Query } = createFoundation({
35
- observability: {
36
- enabled: false,
37
- classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
38
- },
39
- writeAudit: (_host: { actorId: string }, entry) => {
40
- console.log(entry.operation)
41
- },
42
- })
43
- const commands = Command({
44
- contract,
45
- context: (host: { actorId: string }) => host,
46
- audit: ({ operation, input, result }, context) => {
47
- if (operation !== 'tasks.create') return null
48
- return {
49
- operation,
50
- actorId: context.actorId,
51
- aggregate: { type: 'task', id: String(input) },
52
- metadata: { count: result.count },
53
- }
54
- },
55
- operations: ({ command }) => ({
56
- 'tasks.create': command.handler((input) => input + 1),
57
- 'tasks.close': command.handler((input) => input.id.length > 0, { audit: () => null }),
58
- }),
59
- })
60
- const queries = Query({
61
- contract,
62
- context: (host: { actorId: string }) => host,
63
- operations: ({ query }) => ({ 'tasks.get': query.handler((input) => input) }),
64
- })
65
- const created = await commands.exec('tasks.create', '2', { actorId: 'actor' })
66
- const count = await queries.withContext({ actorId: 'actor' }).exec('tasks.get', created.count)
67
- void count
68
- ```
69
-
70
- Each map must implement exactly its declared keys, including maps supplied through
71
- variables. The object key selects the contract entry and infers the handler and
72
- option callback types through `command.handler(fn, options?)` or
73
- `query.handler(fn, options?)`. Keep the map directly in `operations` for contextual
74
- inference. For separately assembled maps, the existing named helpers provide explicit
75
- contract selection without relying on the enclosing object for inference. Input, result,
76
- classification, replayResult, and wire come from the declaration and cannot be
77
- overridden in the implementation. Construction snapshots operation records and
78
- map membership without cloning validators or running user callbacks.
79
-
80
- Options include guard, middleware, metadata, and permission; commands also allow
81
- prepare, aggregates, and audit overrides. Options are optional when a root audit
82
- is configured; commands without one require an audit in their options. Existing
83
- `command['tasks.create']({ handler, ...options })` and named query helpers remain
84
- supported. Named helpers retain their exact key binding.
85
-
86
- The handler receives decoded input, returns the result schema's accepted input,
87
- and execution returns its decoded result. Normal handlers and hooks return values
88
- or Promises. Effect handlers and hooks may also return Effects; their error and
89
- service channels remain part of the selected operation's type. For example, with
90
- `Command` from `createEffectFoundation`:
91
-
92
- ```ts
93
- import { Effect } from 'effect'
94
- import { createEffectFoundation } from 'cvx-kit/effect'
95
-
96
- const effectFoundation = createEffectFoundation({
97
- observability: {
98
- enabled: false,
99
- classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
100
- },
101
- writeAudit: (_host: { actorId: string }) => undefined,
102
- })
103
- const effectCommands = effectFoundation.Command({
104
- contract,
105
- context: (host: { actorId: string }) => host,
106
- audit: () => null,
107
- operations: ({ command }) => ({
108
- 'tasks.create': command.handler((input) => Effect.succeed(input + 1)),
109
- 'tasks.close': command.handler((input) => Effect.succeed(input.id.length > 0)),
110
- }),
111
- })
112
- ```
113
-
114
- The same contract owns both implementations; selecting a different adapter does
115
- not require repeating schemas.
116
-
117
- For independent wire projections, use the existing `operationContract` helper
118
- inside a domain declaration. It infers the projector's parameter from the result
119
- schema; provide both its replayResult and wire fields. A command may instead
120
- supply only replayResult directly in its domain entry. A query cannot prepare,
121
- audit, declare aggregates, or declare replayResult.
122
-
123
- A root audit receives `{ operation, input, result }` and resolved base context.
124
- Its operation discriminates the corresponding input and result types. An
125
- operation's audit override receives the existing `{ command, result }` envelope
126
- and replaces the root audit. Exactly one callback runs after fresh-result
127
- validation. Returning null skips the write. Replay skips handlers, audits,
128
- writes, and completion; permission still runs before replay. Each command needs
129
- a definite audit source. A definite override excludes the root audit's Effect
130
- channels; an optional override retains channels from both possible callbacks.
131
-
132
- Inline registries can also use root audit, leaving schemas and classification
133
- on their operation definitions. The legacy constructor API keeps its existing
134
- context-first execution signatures; the new regular registry uses input-first
135
- handlers and `exec(operation, input, host)` like Effect.
136
-
137
- Pass `contract.errors` explicitly to the existing `createEffectApi({ errors })`
138
- option. Attaching a contract does not change a globally configured API. The
139
- original error-contract instance still owns projection: foreign and forged errors
140
- become UnknownFailure. Wire exposure remains explicit through `.expose(owner, key)`.
141
-
142
- Enable `cvx/reserved-domain-file-anatomy` together with
143
- `cvx/domain-file-responsibilities` to enforce reserved filenames. A domain's
144
- commands.ts and queries.ts each export one genuine bound registry. Contracts are
145
- declarative; schema.ts may declare several zodTable shapes; table.ts owns their
146
- topology, and domain/table.ts uses createModule to assemble module maps. The rules
147
- recognize aliases, namespaces, and the root foundation.ts facade. They do not
148
- require absent reserved files to be created.