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.
@@ -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.