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/conventions.md
DELETED
|
@@ -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`.
|
package/docs/domain-contracts.md
DELETED
|
@@ -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.
|