cvx-kit 0.0.1 → 0.0.2
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 +16 -0
- package/docs/approvals.md +152 -0
- package/docs/architecture.md +124 -0
- package/docs/auth.md +157 -0
- package/docs/commands.md +177 -0
- package/docs/conventions.md +270 -0
- package/docs/convex-architecture-explainer.html +460 -0
- package/docs/llms.md +194 -0
- package/docs/maintainability.md +124 -0
- package/docs/migration.md +351 -0
- package/docs/triggers.md +70 -0
- package/docs/zod-table.md +150 -0
- package/package.json +6 -3
|
@@ -0,0 +1,270 @@
|
|
|
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 recommended architecture tests 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
|
+
<component>.ts (one per mounted component: audit.ts, approvals.ts, ...)
|
|
24
|
+
auth.ts auth.config.ts http.ts crons.ts
|
|
25
|
+
api/
|
|
26
|
+
<entity>.ts
|
|
27
|
+
domain/
|
|
28
|
+
table.ts
|
|
29
|
+
shared/
|
|
30
|
+
<provider>.ts
|
|
31
|
+
<entity>/
|
|
32
|
+
schema.ts table.ts constants.ts commands.ts queries.ts
|
|
33
|
+
rules.ts shared.ts actions.ts
|
|
34
|
+
<workflow>.ts <workflow>_functions.ts
|
|
35
|
+
__tests__/
|
|
36
|
+
components/ (only if you author portable components)
|
|
37
|
+
<name>/
|
|
38
|
+
__tests__/
|
|
39
|
+
setup.ts architecture.test.ts facades.test.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Three zones, three privileges:
|
|
43
|
+
|
|
44
|
+
- **Root** — configuration and facades. May import kit modules, generated
|
|
45
|
+
code, and component clients. May NOT export public functions or contain
|
|
46
|
+
business decisions.
|
|
47
|
+
- **`api/`** — the public surface. May import `functions.ts` constructors and
|
|
48
|
+
its entity's domain modules. May NOT contain business logic, private
|
|
49
|
+
helpers, or reads/writes beyond delegation.
|
|
50
|
+
- **`domain/`** — all business logic. May import `functions.ts`,
|
|
51
|
+
`foundation.ts`, facades, `domain/shared/`, and its own entity directory.
|
|
52
|
+
May NOT import `api/`, sibling entities, `_generated/server` builders, or
|
|
53
|
+
component internals.
|
|
54
|
+
|
|
55
|
+
Dependency direction is one-way: `api → domain → shared → kit`. Anything
|
|
56
|
+
pointing the other way is a violation.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. File anatomy — what each file contains, exactly
|
|
61
|
+
|
|
62
|
+
Every file has one job. The lists below are exhaustive: if content isn't
|
|
63
|
+
listed for a file, it doesn't belong there.
|
|
64
|
+
|
|
65
|
+
### Universal file order
|
|
66
|
+
|
|
67
|
+
Every source file reads from public purpose to private implementation:
|
|
68
|
+
|
|
69
|
+
1. Imports.
|
|
70
|
+
2. Exported API — types, schemas, constants, functions.
|
|
71
|
+
3. File-local types and constants supporting the implementation.
|
|
72
|
+
4. Private helpers as **named function declarations**, at the bottom.
|
|
73
|
+
|
|
74
|
+
No helper implementations above the exported API. No helper
|
|
75
|
+
arrow-function constants where a `function` declaration works. No barrel
|
|
76
|
+
files (`index.ts` re-exports) anywhere inside `convex/`. No re-exporting
|
|
77
|
+
another file's function to shorten an import.
|
|
78
|
+
|
|
79
|
+
### Root files
|
|
80
|
+
|
|
81
|
+
| File | Contains | Never contains |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `convex.config.ts` | `defineApp()`, one `app.use(...)` per component, `defineApp({ env })` declarations, `export default app` | anything else |
|
|
84
|
+
| `schema.ts` | `defineSchema(domainTables)` | table definitions, validators |
|
|
85
|
+
| `functions.ts` | the single `createAuthFunctions<DataModel>()` call and its exported constructors | handlers, business policy beyond the injected config |
|
|
86
|
+
| `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) |
|
|
87
|
+
| `foundation.ts` | the single `new Foundation(...)`, destructured exports | command definitions |
|
|
88
|
+
| `<component>.ts` | `new <Client>(components.<name>)` + minimal admin plumbing | workflow/business definitions |
|
|
89
|
+
| `http.ts` | routes: verify signature → parse payload → delegate to a domain function | webhook business logic |
|
|
90
|
+
| `crons.ts` | `cronJobs()` declarations targeting `internal.domain.<entity>...` | handler logic |
|
|
91
|
+
|
|
92
|
+
### `api/<entity>.ts` — the adapter file
|
|
93
|
+
|
|
94
|
+
One file per entity exposed to clients. Each exported function follows one
|
|
95
|
+
template and stays within ~10 lines of glue:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
export const <verb> = authMutation({
|
|
99
|
+
args: <entity>.commandInput.extend({ /* ids, etc. */ }),
|
|
100
|
+
returns: <resultSchema>,
|
|
101
|
+
handler: (ctx, args) => execute<Verb><Entity>(ctx, { ...args, actorId: ctx.actor.userId }),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export const list = authQuery({
|
|
105
|
+
args: { limit: z.number() },
|
|
106
|
+
returns: z.array(<entity>.publicDto),
|
|
107
|
+
handler: (ctx, { limit }) =>
|
|
108
|
+
ctx.include(ctx.db.query('<entities>'))
|
|
109
|
+
.matching('by_<field>', (ix) => ix.eq('<field>', ctx.actor.userId))
|
|
110
|
+
.execute(limit, (rows) => rows.map(<entity>.toPublicDto)),
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Adapters do four things — parse (boundary schemas), authorize (constructor
|
|
115
|
+
choice), delegate (`execute*` with `ctx.actor`), project (`toPublicDto`).
|
|
116
|
+
The moment an adapter needs a private helper or a business `if`, that code
|
|
117
|
+
moves to the domain.
|
|
118
|
+
|
|
119
|
+
### `domain/<entity>/schema.ts` — shapes only
|
|
120
|
+
|
|
121
|
+
- The entity's single `zodTable(...)` call, with `serverFields`,
|
|
122
|
+
`commandFields`, `publicFields` decided deliberately.
|
|
123
|
+
- DTO compositions derived from the table's boundaries
|
|
124
|
+
(`z.object({ ...<entity>.publicDto.shape, ... }).strict()`).
|
|
125
|
+
- Inferred types (`export type <Entity> = z.infer<typeof <entity>.schema>`).
|
|
126
|
+
- NO indexes, NO `defineTable`, NO enum literals (consume `constants.ts`).
|
|
127
|
+
|
|
128
|
+
### `domain/<entity>/table.ts` — topology only
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export const <entity>Tables = {
|
|
132
|
+
<entities>: <entity>.table
|
|
133
|
+
.index('by_<field>', ['<field>'])
|
|
134
|
+
.index('by_<field>_and_<field2>', ['<field>', '<field2>']),
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Registered in `domain/table.ts`, which merges all entity maps into
|
|
139
|
+
`domainTables`. Shapes change with the data model; indexes change with query
|
|
140
|
+
patterns — that's why they are separate files.
|
|
141
|
+
|
|
142
|
+
### `domain/<entity>/constants.ts` — vocabularies
|
|
143
|
+
|
|
144
|
+
Every finite value set the entity owns, one owner per vocabulary:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
export const <ENTITY>_STATES = ['<a>', '<b>', '<c>'] as const
|
|
148
|
+
export type <Entity>State = (typeof <ENTITY>_STATES)[number]
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Schemas, validators, filters, transition maps, and tests consume the tuple.
|
|
152
|
+
Transition maps are typed against the value type so an invalid literal cannot
|
|
153
|
+
compile. Numeric limits and durations the entity owns also live here.
|
|
154
|
+
|
|
155
|
+
### `domain/<entity>/commands.ts` — every state change
|
|
156
|
+
|
|
157
|
+
1. The frozen operation registry: `Command.operation({ command, result,
|
|
158
|
+
classification, audit })` per operation, keys `'<entity>.<verb>'`.
|
|
159
|
+
2. One `new Command<Ctx, typeof operations>(operations)`.
|
|
160
|
+
3. Exported executors: `export const execute<Verb><Entity> = commands.exec(...)`.
|
|
161
|
+
4. Private handler helpers at the bottom.
|
|
162
|
+
|
|
163
|
+
Audit derivation lives here, next to the operation it describes. No reads
|
|
164
|
+
that belong in `queries.ts`; no pure predicates that belong in `rules.ts`.
|
|
165
|
+
|
|
166
|
+
### `domain/<entity>/queries.ts` — domain reads
|
|
167
|
+
|
|
168
|
+
Read logic worth reusing beyond one adapter: multi-step lookups, DTO
|
|
169
|
+
assembly, cross-index selection. Bounded like everything else.
|
|
170
|
+
|
|
171
|
+
### `domain/<entity>/rules.ts` — pure invariants
|
|
172
|
+
|
|
173
|
+
Predicates and transition checks with **no ctx and no db**: unit-testable
|
|
174
|
+
business truth (`can<Verb>(doc, actor)`, `isTransitionLegal(from, to)`).
|
|
175
|
+
Handlers call rules; rules never call handlers.
|
|
176
|
+
|
|
177
|
+
### `domain/<entity>/shared.ts` — entity-private helpers
|
|
178
|
+
|
|
179
|
+
Code shared by two or more files *of this entity*. If a second entity needs
|
|
180
|
+
it, it moves to `domain/shared/` — never a sibling import.
|
|
181
|
+
|
|
182
|
+
### `domain/<entity>/actions.ts` — external side effects
|
|
183
|
+
|
|
184
|
+
Action-side logic using the shared provider clients from
|
|
185
|
+
`domain/shared/<provider>.ts`. Domains import the shared client; they never
|
|
186
|
+
instantiate their own vendor SDK.
|
|
187
|
+
|
|
188
|
+
### `domain/<entity>/<workflow>.ts` + `<workflow>_functions.ts`
|
|
189
|
+
|
|
190
|
+
The workflow/approval definition the entity owns, and its internal callback
|
|
191
|
+
targets built with `system*` constructors. Callbacks re-validate everything:
|
|
192
|
+
`normalizeId`, `resourceType` check, staleness guard.
|
|
193
|
+
|
|
194
|
+
### `domain/shared/` — cross-entity, and only cross-entity
|
|
195
|
+
|
|
196
|
+
One configured client per external provider (`<provider>.ts`), plus zod/util
|
|
197
|
+
helpers genuinely used by multiple entities. This directory is not a dumping
|
|
198
|
+
ground: code with one consumer moves back to that consumer.
|
|
199
|
+
|
|
200
|
+
### Tests
|
|
201
|
+
|
|
202
|
+
- `domain/<entity>/__tests__/` — behavioral tests for the entity, driven
|
|
203
|
+
through the real constructors (`t.withIdentity(...)`).
|
|
204
|
+
- `convex/__tests__/architecture.test.ts` — the executable conventions;
|
|
205
|
+
every rule proved against a synthetic violating fixture first.
|
|
206
|
+
- `convex/__tests__/facades.test.ts` — every `app.use` exactly once; no
|
|
207
|
+
private-child access.
|
|
208
|
+
- `convex/__tests__/setup.ts` — env stubs for `defineApp({ env })` vars.
|
|
209
|
+
- Test files are named `<subject>.test.ts` and live in a `__tests__/`
|
|
210
|
+
directory beside the code they test — never in a parallel top-level tree.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 3. Naming grammar
|
|
215
|
+
|
|
216
|
+
| Thing | Convention | Example shape |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| Entity directory | plural camelCase, matches table name | `domain/<entities>/` |
|
|
219
|
+
| Table | plural camelCase | `<entities>` |
|
|
220
|
+
| Fields | camelCase; foreign keys `<other>Id`; timestamps only via the kit | `ownerId`, `archivedAt` |
|
|
221
|
+
| Index | `by_<field>` / `by_<field>_and_<field>`, declaration order, nested paths flattened with `_` | `by_ownerId_and_state_status` |
|
|
222
|
+
| Command operation | `<entity>.<verb>` lowercase dotted; must match `/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/` | `<entities>.approve` |
|
|
223
|
+
| Error code | `UPPER_SNAKE`, matching `/^[A-Z][A-Z0-9_]{0,95}$/`, defined in one owner | `INVALID_REQUEST_BOUNDARY` |
|
|
224
|
+
| Vocabulary tuple | `UPPER_SNAKE_CASE` `as const`; derived type PascalCase singular | `<ENTITY>_STATES` / `<Entity>State` |
|
|
225
|
+
| Domain executor | `execute<Verb><Entity>` | `executeApprove<Entity>` |
|
|
226
|
+
| Rule predicate | `can<Verb>` / `is<Condition>` | `canApprove`, `isTransitionLegal` |
|
|
227
|
+
| Trigger registration | `register<Entity>Triggers(triggers)` | exported from the entity, called in root `triggers.ts` |
|
|
228
|
+
| Internal-callback file | `<purpose>_functions.ts`, sibling of `<purpose>.ts` | `approval_functions.ts` |
|
|
229
|
+
| zodTable export | camelCase plural, same as table | `export const <entities> = zodTable(...)` |
|
|
230
|
+
| DTO schema | `<entity><Purpose>Dto` | `<entity>SummaryDto` |
|
|
231
|
+
| Zod schema value | camelCase noun | `commandInput`, `publicDto` |
|
|
232
|
+
| Type | PascalCase; no `I`/`T` prefixes | `<Entity>State` |
|
|
233
|
+
| Test file | `<subject>.test.ts` in `__tests__/` | `commands.test.ts` |
|
|
234
|
+
| Public function path | `api/<entity>:<fn>` — clients never call a root path | — |
|
|
235
|
+
|
|
236
|
+
The two regexes are enforced at runtime: observability silently drops events
|
|
237
|
+
whose operation/classification/errorCode don't match. The rest is enforced by
|
|
238
|
+
the architecture tests — a naming rule without a test is a wish.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 4. Import rules
|
|
243
|
+
|
|
244
|
+
Allowed, per zone (anything not listed is forbidden):
|
|
245
|
+
|
|
246
|
+
| From | May import |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `api/<entity>.ts` | `functions.ts`, `domain/<entity>/*`, `domain/shared/*`, zod |
|
|
249
|
+
| `domain/<entity>/*` | `functions.ts`, `foundation.ts`, component facades, `domain/shared/*`, own directory, generated `internal`/`api` (references only), kit modules |
|
|
250
|
+
| `domain/shared/*` | kit modules, vendor SDKs (this is the ONLY home for vendor clients) |
|
|
251
|
+
| root facades | kit modules, `components` from generated api |
|
|
252
|
+
| `functions.ts` | `_generated/server` (the only file allowed to) |
|
|
253
|
+
| `components/<name>/**` | its own directory + npm deps only — no host imports, no `process.env` |
|
|
254
|
+
|
|
255
|
+
Forbidden everywhere: sibling-entity imports, `api/` from `domain/`,
|
|
256
|
+
component internals (anything below a component's client facade),
|
|
257
|
+
`makeFunctionReference`, barrel files.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 5. Formatting and language
|
|
262
|
+
|
|
263
|
+
- TypeScript strict; no `any` in exported signatures; `as never` only at kit
|
|
264
|
+
seams that document it.
|
|
265
|
+
- Zod objects that cross a boundary are `.strict()` — always.
|
|
266
|
+
- `Object.freeze` for registries and long-lived configuration objects.
|
|
267
|
+
- Comments state constraints the code can't (`// Optional only for rows
|
|
268
|
+
created before <epoch>.`) — never narration of the next line.
|
|
269
|
+
- One entity concept per file; when a file serves two purposes, split it
|
|
270
|
+
along the anatomy in §2.
|