cvx-kit 0.1.12 → 0.1.14
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/AGENTS.md +47 -0
- package/CHANGELOG.md +36 -0
- package/README.md +144 -0
- package/dist/auth.d.mts +3 -1
- package/dist/auth.mjs +1 -1
- package/dist/auth.mjs.map +1 -1
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +1 -1
- package/dist/modules/contracts/binding.d.mts +13 -1
- package/dist/modules/contracts/binding.mjs +1 -1
- package/dist/modules/contracts/binding.mjs.map +1 -1
- package/dist/modules/effect/api-zod.d.mts +1 -1
- package/dist/modules/effect/command.d.mts +13 -9
- package/dist/modules/effect/command.mjs.map +1 -1
- package/dist/modules/effect/domain.d.mts +31 -3
- package/dist/modules/effect/domain.mjs.map +1 -1
- package/dist/modules/effect/query.d.mts +6 -5
- package/dist/modules/effect/query.mjs.map +1 -1
- package/dist/modules/foundation/foundation.d.mts +14 -13
- package/dist/modules/foundation/foundation.mjs.map +1 -1
- package/dist/modules/foundation/operation.d.mts +20 -1
- package/dist/modules/foundation/operation.mjs.map +1 -1
- package/dist/oxlint/architecture.d.mts +2 -0
- package/dist/oxlint/architecture.mjs +1 -1
- package/dist/oxlint/architecture.mjs.map +1 -1
- package/dist/oxlint/ast.d.mts +10 -1
- package/dist/oxlint/ast.mjs +1 -1
- package/dist/oxlint/ast.mjs.map +1 -1
- package/dist/oxlint.d.mts +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/dist/querying/cursors.d.mts +45 -0
- package/dist/querying/cursors.mjs +2 -0
- package/dist/querying/cursors.mjs.map +1 -0
- package/dist/querying/execution.d.mts +30 -0
- package/dist/querying/execution.mjs +2 -0
- package/dist/querying/execution.mjs.map +1 -0
- package/dist/querying/filters.d.mts +136 -0
- package/dist/querying/filters.mjs +2 -0
- package/dist/querying/filters.mjs.map +1 -0
- package/dist/querying/planner.d.mts +50 -0
- package/dist/querying/planner.mjs +2 -0
- package/dist/querying/planner.mjs.map +1 -0
- package/dist/querying/relationships.d.mts +41 -0
- package/dist/querying/relationships.mjs +2 -0
- package/dist/querying/relationships.mjs.map +1 -0
- package/dist/querying/security.d.mts +107 -0
- package/dist/querying/security.mjs +2 -0
- package/dist/querying/security.mjs.map +1 -0
- package/dist/querying.d.mts +39 -0
- package/dist/querying.mjs +2 -0
- package/dist/querying.mjs.map +1 -0
- package/dist/relations-public.d.mts +2 -0
- package/dist/relations-public.mjs +1 -0
- package/dist/relations.d.mts +155 -0
- package/dist/relations.mjs +2 -0
- package/dist/relations.mjs.map +1 -0
- package/dist/zod-convex.d.mts +10 -0
- package/dist/zod-convex.mjs +2 -0
- package/dist/zod-convex.mjs.map +1 -0
- package/dist/zod-registration.d.mts +10 -0
- package/dist/zod-registration.mjs +2 -0
- package/dist/zod-registration.mjs.map +1 -0
- package/dist/zod-table.d.mts +63 -8
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/docs/conventions.md +12 -0
- package/docs/domain-contracts.md +42 -9
- package/docs/llms.md +15 -5
- package/docs/oxlint.md +129 -25
- package/docs/querying.md +293 -0
- package/docs/zod-table.md +93 -13
- package/package.json +11 -1
- package/scripts/owned-process.mjs +140 -0
- package/scripts/owned-process.test.mjs +107 -0
- package/scripts/smoke-packed-querying.mjs +296 -0
- package/scripts/test-packed-effect.mjs +10 -1
- package/scripts/test-packed-oxlint.mjs +5 -1
- package/scripts/test-packed-querying.mjs +185 -0
package/docs/oxlint.md
CHANGED
|
@@ -11,7 +11,8 @@ library authoring. The five architecture rules below were added in 0.1.6. See `u
|
|
|
11
11
|
## Configuration
|
|
12
12
|
|
|
13
13
|
Install Oxlint as a development dependency and load the plugin in
|
|
14
|
-
`.oxlintrc.json`. The root README includes a configuration enabling all rules.
|
|
14
|
+
`.oxlintrc.json`. The root README includes a configuration enabling all rules. The coverage table
|
|
15
|
+
below lists all 23 rule names.
|
|
15
16
|
Loading a plugin does not enable its rules. For example:
|
|
16
17
|
|
|
17
18
|
```json
|
|
@@ -44,28 +45,31 @@ offer automatic fixes for architectural moves.
|
|
|
44
45
|
|
|
45
46
|
Every name below has the `cvx/` prefix in lint configuration.
|
|
46
47
|
|
|
47
|
-
| Rule | Detects | Documentation
|
|
48
|
-
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
-
| `project-structure` | Root folder allowlist, domain ownership, colocated runnable tests, misplaced tests, symlinks, parse failures, runtime domain-module cycles, and shared implementations with only one domain consumer. | Folder structure and Tests
|
|
50
|
-
| `root-wiring-only` | Unknown root files without a component facade, root function declarations, nondelegating callbacks, branches, loops, direct writes/network calls, and inline root schema construction. | Root files
|
|
51
|
-
| `application-orchestration` | Files without two domain dependencies, provider construction, external runtime dependencies, database access, validator construction, and business branches. | Module grain
|
|
52
|
-
| `internal-function-ownership` | Internal/system registrations at root, in public API adapters, or in domain/shared. | Folder structure
|
|
53
|
-
| `domain-file-responsibilities` | Impure rules, query writes/mutations, database access in declarative files, misplaced direct external I/O, and imports that violate these roles. | File anatomy
|
|
54
|
-
| `root-facade-ownership` | `createAuthFunctions`, `createTriggers`, and `
|
|
55
|
-
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules
|
|
56
|
-
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules
|
|
57
|
-
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules
|
|
58
|
-
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure
|
|
59
|
-
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file
|
|
60
|
-
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies
|
|
61
|
-
| `no-unbounded-reads` |
|
|
62
|
-
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md`
|
|
63
|
-
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import
|
|
64
|
-
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules
|
|
65
|
-
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology
|
|
66
|
-
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order
|
|
67
|
-
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order
|
|
68
|
-
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order
|
|
48
|
+
| Rule | Detects | Documentation |
|
|
49
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
|
|
50
|
+
| `project-structure` | Root folder allowlist, domain ownership, colocated runnable tests, misplaced tests, symlinks, parse failures, runtime domain-module cycles, and shared implementations with only one domain consumer. | Folder structure and Tests |
|
|
51
|
+
| `root-wiring-only` | Unknown root files without a component facade, root function declarations, nondelegating callbacks, branches, loops, direct writes/network calls, and inline root schema construction. | Root files |
|
|
52
|
+
| `application-orchestration` | Files without two domain dependencies, provider construction, external runtime dependencies, database access, validator construction, and business branches. | Module grain |
|
|
53
|
+
| `internal-function-ownership` | Internal/system registrations at root, in public API adapters, or in domain/shared. | Folder structure |
|
|
54
|
+
| `domain-file-responsibilities` | Impure rules, query writes/mutations, database access in declarative files, misplaced direct external I/O, and imports that violate these roles. | File anatomy |
|
|
55
|
+
| `root-facade-ownership` | `createAuthFunctions`, foundation factories, `createTriggers`, `defineRelations`, and `createQueryEngine` outside their root facades, or repeated in one file. | `conventions.md`, Root files |
|
|
56
|
+
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules |
|
|
57
|
+
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
58
|
+
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules |
|
|
59
|
+
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
60
|
+
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
61
|
+
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
62
|
+
| `no-unbounded-reads` | Recognized unbounded database/stream/relationship reads and eligibility `.filter()` calls after bounded take/execute/paginate results. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
63
|
+
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
64
|
+
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
65
|
+
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
66
|
+
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
67
|
+
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
68
|
+
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
69
|
+
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
70
|
+
| `no-manual-id-projection` | Adding `id`/`_id` to a recognized table DTO through object spreads or property assignments. | Public DTO parameters in `zod-table.md` |
|
|
71
|
+
| `no-copied-pagination` | Zod/native validator objects copying `numItems` + `cursor` or `page` + `isDone` + `continueCursor`. | Pagination options in `zod-table.md`; `querying.md` |
|
|
72
|
+
| `reserved-domain-file-anatomy` | Reserved domain files without their expected declarative schema/contracts/table or Command/Query registry shape. | Reserved domain files below |
|
|
69
73
|
|
|
70
74
|
## Allowed patterns
|
|
71
75
|
|
|
@@ -87,14 +91,114 @@ Every name below has the `cvx/` prefix in lint configuration.
|
|
|
87
91
|
- `archivedAt` remains application-controlled. Singular `db.get` reads and
|
|
88
92
|
bounded `execute`/`paginate` calls are allowed.
|
|
89
93
|
|
|
94
|
+
## DTO and pagination diagnostics
|
|
95
|
+
|
|
96
|
+
Enable the new diagnostics alongside the existing architecture rules:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"rules": {
|
|
101
|
+
"cvx/no-manual-id-projection": "error",
|
|
102
|
+
"cvx/no-copied-pagination": "error",
|
|
103
|
+
"cvx/no-unbounded-reads": "error",
|
|
104
|
+
"cvx/root-facade-ownership": "error",
|
|
105
|
+
"cvx/root-wiring-only": "error",
|
|
106
|
+
"cvx/reserved-domain-file-anatomy": "error"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `no-manual-id-projection`
|
|
112
|
+
|
|
113
|
+
Recognizes a table DTO projection/parse, including supported binding aliases,
|
|
114
|
+
then reports adding `id` or `_id` by a spread or property assignment:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// Reported
|
|
118
|
+
const output = { ...tasks.toPublicDto(row), _id: row._id }
|
|
119
|
+
const parsed = tasks.publicDto.parse(input)
|
|
120
|
+
parsed.id = row._id
|
|
121
|
+
|
|
122
|
+
// Use the matching projection and return boundary
|
|
123
|
+
const outputWithId = tasks.toPublicDto(row, true)
|
|
124
|
+
const returns = tasks.dto(true)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The helper returns `_id`, not an `id` alias. For computed fields use the
|
|
128
|
+
validated `extend`/`values` API described in [zod-table.md](./zod-table.md).
|
|
129
|
+
The rule depends on recognized table/DTO provenance; it does not treat every
|
|
130
|
+
object named `dto` as a kit boundary or prove all runtime projection paths safe.
|
|
131
|
+
|
|
132
|
+
### `no-copied-pagination`
|
|
133
|
+
|
|
134
|
+
Reports recognized `z.object`/Convex `v.object` shapes containing both `numItems`
|
|
135
|
+
and `cursor`, or all three of `page`, `isDone`, and `continueCursor`:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// Reported
|
|
139
|
+
const options = z.object({ numItems: z.number(), cursor: z.string().nullable() })
|
|
140
|
+
const result = z.object({
|
|
141
|
+
page: z.array(tasks.publicDto),
|
|
142
|
+
isDone: z.boolean(),
|
|
143
|
+
continueCursor: z.string(),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Table query input and relation-aware output
|
|
147
|
+
const options = tasks.paginationOpts({ status: true })
|
|
148
|
+
const result = relations.tables.tasks.pageDto({})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For ordinary native pagination, retain `paginationOptsValidator` (bridge through
|
|
152
|
+
`convexToZod` when needed) or `paginated(tasks.dto())`. These preserve native
|
|
153
|
+
validators instead of copying their current fields. Application-specific fields
|
|
154
|
+
can extend the derived Zod object. Use the engine's `paginationOpts` for its
|
|
155
|
+
selected filters and order; a native options validator alone supplies neither.
|
|
156
|
+
|
|
157
|
+
### `no-unbounded-reads`
|
|
158
|
+
|
|
159
|
+
The existing rule now follows recognized native, kit `include`, stream, and
|
|
160
|
+
relationship read chains. It reports unbounded `collect()`/`resolve()` where
|
|
161
|
+
supported provenance is known, and `.filter()` on recognized bounded results:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// Reported: eligible rows outside the first 25 can be missed
|
|
165
|
+
const rows = await ctx.db.query('tasks').take(25)
|
|
166
|
+
const eligible = rows.filter((task) => task.status === 'open')
|
|
167
|
+
|
|
168
|
+
// Put eligibility into the query before page filling
|
|
169
|
+
const page = await querying(ctx).tasks.where({ status: 'open' }).paginate(args)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Choose an indexed native constraint where appropriate, or express the predicate
|
|
173
|
+
through the querying engine. Many relationships use bounded pages and separate
|
|
174
|
+
continuation endpoints. There is no automatic predicate-movement fix: moving a
|
|
175
|
+
predicate can change authorization, ordering, and continuation semantics.
|
|
176
|
+
|
|
177
|
+
### Relation and engine facade ownership
|
|
178
|
+
|
|
179
|
+
`root-facade-ownership` requires `defineRelations` in `convex/relations.ts` and
|
|
180
|
+
`createQueryEngine` in `convex/querying.ts`, each configured once. The same rule
|
|
181
|
+
continues to enforce auth, triggers, and foundation facades.
|
|
182
|
+
|
|
183
|
+
`root-wiring-only` and the whole-project checker permit these root files only
|
|
184
|
+
as narrow declarative factory configurations. `relations.ts` can import domain
|
|
185
|
+
schema/table declarations (including the domain table assembly); `querying.ts`
|
|
186
|
+
can import domain query/contract modules where permitted. Neither file is a
|
|
187
|
+
place for endpoint handlers, business branches, or direct database operations.
|
|
188
|
+
Domain contracts derive validators, domain Query registries own handlers, and
|
|
189
|
+
`api/` adapters delegate through authenticated constructors. See
|
|
190
|
+
[querying.md](./querying.md) and [conventions.md](./conventions.md).
|
|
191
|
+
|
|
90
192
|
## What still needs review and tests
|
|
91
193
|
|
|
92
194
|
These are syntax and lexical-binding checks, not whole-program validation.
|
|
93
195
|
Import-boundary rules resolve relative imports and tsconfig aliases (including
|
|
94
196
|
extended configs) using the package resolver. Symlinked source is rejected by
|
|
95
197
|
the project scan. Computed import paths are not resolved. Named import aliases and namespaces are recognized where documented;
|
|
96
|
-
|
|
97
|
-
methods
|
|
198
|
+
General variable reassignment, arbitrary wrappers, and destructured database
|
|
199
|
+
methods are not whole-program traced. DTO/read checks do follow supported local
|
|
200
|
+
initializers, aliases, awaited results, and recognized helper chains; unrelated
|
|
201
|
+
user functions and unknown provenance remain outside that bounded analysis. Database checks
|
|
98
202
|
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
99
203
|
|
|
100
204
|
The plugin does not prove DTO redaction, complete command/audit coverage,
|
package/docs/querying.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# `cvx-kit/querying` — typed, authorized stream pagination
|
|
2
|
+
|
|
3
|
+
Declare relationships with `cvx-kit/relations`, configure one query engine, then
|
|
4
|
+
paginate with a small declarative chain. The engine runs streams and relationship
|
|
5
|
+
lookups in the caller's Convex query transaction. Filtering determines eligible
|
|
6
|
+
parents before they fill a page. `include` and native `paginated(dto)` remain
|
|
7
|
+
independent APIs.
|
|
8
|
+
|
|
9
|
+
## Declare tables and indexes
|
|
10
|
+
|
|
11
|
+
Keep shapes in `domain/<module>/schema.ts` and native indexes in its `table.ts`.
|
|
12
|
+
These examples put tasks, labels, and their junction in one owning module.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// convex/domain/tasks/schema.ts
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import { zodTable } from 'cvx-kit/zod-table'
|
|
18
|
+
|
|
19
|
+
export const tasks = zodTable(
|
|
20
|
+
'tasks',
|
|
21
|
+
(id) => ({
|
|
22
|
+
title: z.string(),
|
|
23
|
+
department: z.string(),
|
|
24
|
+
status: z.string(),
|
|
25
|
+
priority: z.number(),
|
|
26
|
+
reference: id('tasks').nullable().optional(),
|
|
27
|
+
}),
|
|
28
|
+
{ publicFields: ['title', 'department', 'status', 'priority'] },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export const labels = zodTable(
|
|
32
|
+
'labels',
|
|
33
|
+
() => ({
|
|
34
|
+
name: z.string(),
|
|
35
|
+
}),
|
|
36
|
+
{ publicFields: ['name'] },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export const edges = zodTable(
|
|
40
|
+
'edges',
|
|
41
|
+
(id) => ({
|
|
42
|
+
task: id('tasks'),
|
|
43
|
+
label: id('labels'),
|
|
44
|
+
}),
|
|
45
|
+
{ publicFields: [] },
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// convex/domain/tasks/table.ts
|
|
51
|
+
import { tasks, labels, edges } from './schema'
|
|
52
|
+
|
|
53
|
+
export const tables = {
|
|
54
|
+
tasks: tasks.table.index('by_department_status', ['department', 'status']),
|
|
55
|
+
labels: labels.table,
|
|
56
|
+
edges: edges.table.index('by_task', ['task']),
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Assemble those table definitions in the application's root schema. Declare every
|
|
61
|
+
index used by a relationship before compiling the relation registry.
|
|
62
|
+
|
|
63
|
+
## Declare relationships once
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// convex/relations.ts
|
|
67
|
+
import { defineRelations } from 'cvx-kit/relations'
|
|
68
|
+
import './domain/tasks/table'
|
|
69
|
+
import { tasks, labels, edges } from './domain/tasks/schema'
|
|
70
|
+
|
|
71
|
+
export const relations = defineRelations({ tasks, labels, edges }, (r) => ({
|
|
72
|
+
tasks: {
|
|
73
|
+
referenceTask: r.one.tasks({ from: r.tasks.reference, to: r.tasks._id }),
|
|
74
|
+
labels: r.many.labels({
|
|
75
|
+
from: r.tasks._id.through(r.edges.task),
|
|
76
|
+
to: r.labels._id.through(r.edges.label),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
}))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A direct one edge uses `r.one.target({ from, to })`; a direct many edge uses
|
|
83
|
+
`r.many.target({ from, to })`. For example, with a stored `labels.task` foreign
|
|
84
|
+
key and a supporting `by_task` index on labels:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
r.many.labels({ from: r.tasks._id, to: r.labels.task })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Relationship names distinguish multiple references to the same table. Self
|
|
91
|
+
references and junctions are supported. Tables must be object-shaped `zodTable`
|
|
92
|
+
or `tenantTable` boundaries; variant tables are outside this API. Declarations
|
|
93
|
+
are validated against table ownership, field compatibility, and supporting
|
|
94
|
+
indexes. Convex does not infer relationships; this registry supplies them.
|
|
95
|
+
|
|
96
|
+
## Configure the engine and authentication
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// convex/querying.ts
|
|
100
|
+
import { createQueryEngine } from 'cvx-kit/querying'
|
|
101
|
+
import schema from './schema'
|
|
102
|
+
import { relations } from './relations'
|
|
103
|
+
import { cursor } from './functions'
|
|
104
|
+
|
|
105
|
+
export const querying = createQueryEngine({ schema, relations, cursor })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The application's `functions.ts` facade configures `createAuthFunctions`,
|
|
109
|
+
including its membership resolution and effective read/tenancy policies. Call
|
|
110
|
+
`querying(ctx)` with a query context supplied by that facade's `authQuery`.
|
|
111
|
+
The engine consumes the private managed reader capability installed there and
|
|
112
|
+
rechecks composed authorization for parents, junctions, targets, and continuation
|
|
113
|
+
reads. The example declarations do not establish access policy on their own.
|
|
114
|
+
See [auth.md](./auth.md) and [tenancy.md](./tenancy.md) for policy setup.
|
|
115
|
+
|
|
116
|
+
Unknown contexts fail closed with `UNMANAGED_QUERY_CONTEXT`. Mutation and action
|
|
117
|
+
contexts are unsupported. A host with another auth system can centrally configure
|
|
118
|
+
`trustedAdapter`; it must attest the query reader, effective policy, and security
|
|
119
|
+
scope. Do not expose adapter selection to endpoint arguments.
|
|
120
|
+
|
|
121
|
+
`cursor` has `{ version, currentKeyId, keys }`. `keys` maps key IDs to server-only
|
|
122
|
+
64-byte `Uint8Array` secrets; `currentKeyId` names the key used for new tokens.
|
|
123
|
+
Load real secrets through server configuration, never from clients or a constant
|
|
124
|
+
fixture key. Keep prior keys during rotation to preserve existing cursors.
|
|
125
|
+
The default codec uses authenticated AES-SIV encryption; there is no plaintext
|
|
126
|
+
fallback. Identical bound payloads reveal equality.
|
|
127
|
+
|
|
128
|
+
## Define input and result contracts
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// Inside convex/domain/tasks/contracts.ts
|
|
132
|
+
const input = relations.tables.tasks.paginationOpts({
|
|
133
|
+
department: true,
|
|
134
|
+
status: true,
|
|
135
|
+
priority: true,
|
|
136
|
+
referenceTask: { status: true },
|
|
137
|
+
labels: { name: true },
|
|
138
|
+
})
|
|
139
|
+
const result = relations.tables.tasks.pageDto({
|
|
140
|
+
referenceTask: true,
|
|
141
|
+
labels: true,
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`paginationOpts(selection)` extends native Convex pagination options with optional
|
|
146
|
+
`filters` and global `order: 'asc' | 'desc'`. The selection permits client filters;
|
|
147
|
+
it does not change public output or grant access. The result selection must
|
|
148
|
+
match `.with(...)`. `.pageDto({})` returns a native page of the table's public
|
|
149
|
+
DTOs including branded `_id`. One relationships return a public DTO or `null`;
|
|
150
|
+
many relationships return their own bounded native pages. Missing and unreadable
|
|
151
|
+
targets have the same public result. Duplicate junction occurrences remain in
|
|
152
|
+
output, even when target reads are memoized.
|
|
153
|
+
|
|
154
|
+
For scalar-only input, use `tasks.paginationOpts({ status: true })`. Both forms
|
|
155
|
+
are Zod objects and support `.extend(...)` for endpoint-specific arguments.
|
|
156
|
+
Use the contracts through your domain Query registry and keep public auth
|
|
157
|
+
registration in `api/<entity>.ts`; see [domain-contracts.md](./domain-contracts.md).
|
|
158
|
+
|
|
159
|
+
## Query and filter
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Inside an authenticated domain Query handler
|
|
163
|
+
const department = args.filters?.department
|
|
164
|
+
const indexCondition = typeof department === 'string' ? department : undefined
|
|
165
|
+
const page = await querying(ctx)
|
|
166
|
+
.tasks.where({ status: 'open' })
|
|
167
|
+
.when(indexCondition, 'by_department_status')
|
|
168
|
+
.with({ referenceTask: true, labels: true })
|
|
169
|
+
.paginate(args)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`.where(spec)` adds server-owned constraints, combined with client filters by
|
|
173
|
+
AND. Use it for mandatory business constraints; effective auth policy still
|
|
174
|
+
controls access. `.with(selection)` chooses related output. A relation can be
|
|
175
|
+
used as an eligibility filter without also including it in output. A parent
|
|
176
|
+
relationship predicate does not filter the nested output page: it determines
|
|
177
|
+
whether the parent is eligible. Child-page filters are a separate query input.
|
|
178
|
+
|
|
179
|
+
Example client args for the contract above:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
{
|
|
183
|
+
numItems: 25,
|
|
184
|
+
cursor: null,
|
|
185
|
+
order: 'desc',
|
|
186
|
+
filters: {
|
|
187
|
+
department: 'engineering',
|
|
188
|
+
priority: { gte: 2, lt: 5 },
|
|
189
|
+
labels: { some: { name: 'urgent' } },
|
|
190
|
+
not: { status: 'blocked' },
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Scalar predicates accept direct equality or operator objects with `eq`, `in`,
|
|
196
|
+
and, for supported range types, `gt`, `gte`, `lt`, and `lte`. Optional fields
|
|
197
|
+
also support `exists`. `and` and `or` contain nonempty arrays of predicates;
|
|
198
|
+
`not` contains one predicate. Use `fields: { ... }` to address scalar fields
|
|
199
|
+
whose names collide with grammar keys such as `and`, `or`, `not`, or `fields`.
|
|
200
|
+
One relationships accept a nested predicate or `null`; many relationships use
|
|
201
|
+
`some` or `none` over authorized targets. Unknown fields and incompatible scalar
|
|
202
|
+
values/operators fail validation. Filter depth is at most four, the whole tree
|
|
203
|
+
has at most 256 nodes, and an `in` list contains at most 100 values.
|
|
204
|
+
|
|
205
|
+
## Conditional indexes and ordering
|
|
206
|
+
|
|
207
|
+
`.when(value, 'index_name')` selects the first present condition. `false`, `0`,
|
|
208
|
+
and `''` are present; `null` and `undefined` are absent. The condition chooses an
|
|
209
|
+
index but does not bind its fields: validated filters and `.where(...)` supply
|
|
210
|
+
the bindings. An incompatible explicit index fails instead of silently scanning.
|
|
211
|
+
Relationship traversal retains its mandatory foreign-key prefix.
|
|
212
|
+
|
|
213
|
+
Without an explicit choice, the planner selects the longest usable equality
|
|
214
|
+
prefix, with stable index-name ordering for ties and creation-time fallback.
|
|
215
|
+
OR branches contribute only guaranteed common bindings. `order: 'desc'` reverses
|
|
216
|
+
the selected native index order; it does not sort by an arbitrary field.
|
|
217
|
+
|
|
218
|
+
## Continue relationship pages
|
|
219
|
+
|
|
220
|
+
Return many relationships as pages, then use a separate authenticated endpoint
|
|
221
|
+
for their next page. Validate `parent` with `tasks.schema.shape._id` and use the
|
|
222
|
+
related table's pagination contract:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const input = relations.tables.labels
|
|
226
|
+
.paginationOpts({ name: true })
|
|
227
|
+
.extend({ parent: tasks.schema.shape._id })
|
|
228
|
+
const result = relations.tables.labels.pageDto({})
|
|
229
|
+
|
|
230
|
+
// Inside the child Query handler
|
|
231
|
+
const { parent, ...options } = args
|
|
232
|
+
return querying(ctx).tasks.relation(parent, 'labels').paginate(options)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The client sends the parent ID and the nested page's `continueCursor`, preserving
|
|
236
|
+
its original order and output selection. For the parent example above that means
|
|
237
|
+
`order: 'desc'`, selection `{}`, and no child filters on resume; adding a `name`
|
|
238
|
+
filter would change the binding and reject that cursor. A new child traversal
|
|
239
|
+
with `cursor: null` can establish its own filters. Typed
|
|
240
|
+
paths can traverse several declared edges to a many relationship. Each request
|
|
241
|
+
rechecks the parent path and current authorization; a child token cannot be
|
|
242
|
+
replayed under another parent.
|
|
243
|
+
|
|
244
|
+
Root, child, split, and end cursors bind filters, output selection, index, order,
|
|
245
|
+
ancestry, principal/tenant/security scope, and registry/policy/engine versions.
|
|
246
|
+
Changing page size alone remains valid. Treat tokens as opaque; preserve native
|
|
247
|
+
`isDone`, `continueCursor`, and optional page-status/split metadata. Permission
|
|
248
|
+
changes are rechecked on resume. Native `endCursor` supports bounded partitions.
|
|
249
|
+
|
|
250
|
+
## Central limits and operational behavior
|
|
251
|
+
|
|
252
|
+
`createQueryEngine` also accepts `maxPageSize`, `budgets`, `policyVersion`,
|
|
253
|
+
`engineVersion`, and `trustedAdapter`. These belong in central configuration,
|
|
254
|
+
not per-endpoint filter/project callbacks. Page sizes are positive integers up
|
|
255
|
+
to 100; `maxPageSize` can lower that ceiling. Shared request budgets cover read
|
|
256
|
+
operations, index ranges, raw candidates, document bytes, fan-out, traversal
|
|
257
|
+
depth, and memo entries, including managed policy reads. Native request read
|
|
258
|
+
limits may lower ceilings. Output relationship depth is at most four.
|
|
259
|
+
|
|
260
|
+
If eligibility cannot be established within the budget, execution throws
|
|
261
|
+
`MANAGED_READ_BUDGET`; it does not return a misleading exhausted page or conclude
|
|
262
|
+
that a negative relationship predicate matched. Policy errors propagate rather
|
|
263
|
+
than turning into eligible rows. Memoization is request-local and preserves
|
|
264
|
+
junction multiplicity. Relationships can require many database reads; one public
|
|
265
|
+
query does not imply one physical read. Document accounting is an application
|
|
266
|
+
bound, not a measurement of Convex storage/index/prefetch I/O.
|
|
267
|
+
|
|
268
|
+
Arbitrary async predicates, custom projections/sorts, mutation/action execution,
|
|
269
|
+
search/vector optimization, and global caching are outside this API. Index
|
|
270
|
+
selection reduces candidates where possible; filtering every supported scalar
|
|
271
|
+
field does not guarantee an efficient index for every filter combination.
|
|
272
|
+
|
|
273
|
+
## Migration and verification
|
|
274
|
+
|
|
275
|
+
Use `toPublicDto(row, true)` with `dto(true)` instead of manual ID spreads.
|
|
276
|
+
Replace copied pagination schemas with table/registry helpers. Move eligibility
|
|
277
|
+
predicates before bounded page filling, and replace unbounded relationship
|
|
278
|
+
collection with declared many pages and child continuation. See the diagnostics
|
|
279
|
+
and examples in [oxlint.md](./oxlint.md).
|
|
280
|
+
|
|
281
|
+
The [packed example](../../test/fixture-packed-querying/convex/) contains complete
|
|
282
|
+
schema, relation, engine, contract, Query-registry, auth-adapter, and child-endpoint
|
|
283
|
+
wiring. `bun run test:package:querying` exercises strict installed and minimum-peer
|
|
284
|
+
consumers, all consumer rules, and multi-entity/filter/order continuation tests.
|
|
285
|
+
The local native smoke runner exercises registered Convex validators and cursors;
|
|
286
|
+
these checks do not establish hosted production performance.
|
|
287
|
+
|
|
288
|
+
Run `bunx vp test run test/querying-relations-stress.test.ts --silent=false` to
|
|
289
|
+
report per-page p10, p50, and p99 latency for each filter/order/page-size case,
|
|
290
|
+
plus an aggregate for duplicate child continuations. Reports include sample
|
|
291
|
+
counts and use nearest-rank percentiles. A small sample's p99 is its maximum;
|
|
292
|
+
the measurements include `convex-test` and instrumentation overhead. Read
|
|
293
|
+
counters and ordered-result assertions remain separate from latency measurements.
|
package/docs/zod-table.md
CHANGED
|
@@ -49,19 +49,21 @@ unregistered foreign ID and does not throw; peer installation prevents the
|
|
|
49
49
|
supported package graph from creating that ambiguity. Verify critical ID
|
|
50
50
|
validators retain `kind === 'id'` and the expected `tableName` after installation.
|
|
51
51
|
|
|
52
|
-
| Property | What it is
|
|
53
|
-
| -------------------------------------------- |
|
|
54
|
-
| `table` | `defineTable(...)` from the storage shape
|
|
55
|
-
| `storage` | zod object of the raw row (no `_id`/`_creationTime`)
|
|
56
|
-
| `schema` | `storage` + `_id` + `_creationTime`
|
|
57
|
-
| `insertSchema` | storage minus timestamps minus `serverFields`
|
|
58
|
-
| `updateSchema` | `insertSchema.partial()`
|
|
59
|
-
| `commandInput` | strict pick of `commandFields`
|
|
60
|
-
| `publicDto` | strict pick of `publicFields`
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
52
|
+
| Property | What it is | Where you use it |
|
|
53
|
+
| -------------------------------------------- | ------------------------------------------------------ | ------------------------------ |
|
|
54
|
+
| `table` | `defineTable(...)` from the storage shape | `convex/schema.ts` |
|
|
55
|
+
| `storage` | zod object of the raw row (no `_id`/`_creationTime`) | validating rows |
|
|
56
|
+
| `schema` | `storage` + `_id` + `_creationTime` | full-document validation |
|
|
57
|
+
| `insertSchema` | storage minus timestamps minus `serverFields` | mutation args |
|
|
58
|
+
| `updateSchema` | `insertSchema.partial()` | patch mutation args |
|
|
59
|
+
| `commandInput` | strict pick of `commandFields` | command protocol inputs |
|
|
60
|
+
| `publicDto` | strict pick of `publicFields` | query return validators |
|
|
61
|
+
| `dto(options?)` | public return schema with optional ID and extensions | query return validators |
|
|
62
|
+
| `toPublicDto(row)` | projects **and re-parses** a row into the DTO | query handlers |
|
|
63
|
+
| `paginationOpts(selection)` | native pagination options, selected filters, and order | query input validators |
|
|
64
|
+
| `insert(omit?)` / `update(omit?)` | schema with per-callsite field omission | specialized mutations |
|
|
65
|
+
| `tools.insert` / `tools.update` / `tools.id` | ready-made LLM tool input schemas | agent tool definitions |
|
|
66
|
+
| `tableName` | the literal table name | indexes, triggers registration |
|
|
65
67
|
|
|
66
68
|
## Redaction is runtime, not just types
|
|
67
69
|
|
|
@@ -82,6 +84,84 @@ export const get = authQuery({
|
|
|
82
84
|
})
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
## Public DTO parameters
|
|
88
|
+
|
|
89
|
+
`publicDto` is the strict Zod schema selected by `publicFields`; it takes no
|
|
90
|
+
parameters. Use `dto(...)` to derive a return schema and `toPublicDto(row, ...)`
|
|
91
|
+
to project a row with the same options. These methods are also available on
|
|
92
|
+
`tenantTable` boundaries.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
documents.toPublicDto(row) // publicFields only
|
|
96
|
+
documents.toPublicDto(row, false) // same default projection
|
|
97
|
+
documents.toPublicDto(row, true) // publicFields plus _id
|
|
98
|
+
documents.toPublicDto(row, { _id: true }) // explicit equivalent
|
|
99
|
+
|
|
100
|
+
const withoutId = documents.dto() // same boundary as publicDto
|
|
101
|
+
const withId = documents.dto(true) // branded Id<'documents'>
|
|
102
|
+
const explicit = documents.dto({ _id: true })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Including `_id` requires a row containing an ID and validates it through the
|
|
106
|
+
table's ID schema. It does not expose `_creationTime`, tenant metadata, or other
|
|
107
|
+
storage fields. Do not spread an ID onto a parsed DTO manually; use the matching
|
|
108
|
+
`dto(true)` return validator.
|
|
109
|
+
|
|
110
|
+
Computed fields need both a schema and explicitly supplied values:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const extend = { url: z.url() }
|
|
114
|
+
const returnSchema = documents.dto({ _id: true, extend })
|
|
115
|
+
|
|
116
|
+
const output = documents.toPublicDto(row, {
|
|
117
|
+
_id: true,
|
|
118
|
+
extend,
|
|
119
|
+
values: { url: `https://example.test/documents/${row._id}` },
|
|
120
|
+
})
|
|
121
|
+
returnSchema.parse(output)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`extend` is a map of field names to Zod validators, not an `extendFields` wrapper.
|
|
125
|
+
`values` belongs only to the projector; `dto(...)` describes the result. Extension
|
|
126
|
+
values are parsed as a strict object: missing required fields, unknown values,
|
|
127
|
+
and invalid values fail. Values are never copied implicitly from storage.
|
|
128
|
+
|
|
129
|
+
Extensions cannot replace public fields or use `_id`, `_creationTime`, `tenant`,
|
|
130
|
+
`createdAt`, `updatedAt`, `archivedAt`, or `__proto__`. Their schemas must validate
|
|
131
|
+
without changing values: transforms, pipes, coercion, preprocess, defaults,
|
|
132
|
+
catches, and overwrites are rejected, including inside nested schemas.
|
|
133
|
+
Refinements must not mutate their input. Existing public-field schemas retain
|
|
134
|
+
their existing parsing behavior.
|
|
135
|
+
|
|
136
|
+
## Pagination options
|
|
137
|
+
|
|
138
|
+
Select filterable scalar fields independently of the public DTO mask:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const input = documents.paginationOpts({ title: true, ownerId: true })
|
|
142
|
+
const args = input.parse({
|
|
143
|
+
numItems: 25,
|
|
144
|
+
cursor: null,
|
|
145
|
+
order: 'desc',
|
|
146
|
+
filters: { title: 'Proposal' },
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The helper extends Convex's native pagination options with optional `filters`
|
|
151
|
+
and `order: 'asc' | 'desc'`. It returns a Zod object that can be used directly as
|
|
152
|
+
query args or extended with endpoint-specific fields. `numItems` must be a
|
|
153
|
+
positive integer at most 100; engine configuration may lower that ceiling.
|
|
154
|
+
Unknown filter fields fail. Stored scalar fields, including explicitly selected
|
|
155
|
+
server-owned fields and IDs, can be filterable even when absent from the DTO.
|
|
156
|
+
Arrays, objects, and other unsupported non-scalar fields cannot be selected.
|
|
157
|
+
This schema creates no authorization policy by itself.
|
|
158
|
+
|
|
159
|
+
For relationship filters and paginated public output, use
|
|
160
|
+
`relations.tables.documents.paginationOpts(selection)` and `.pageDto(selection)`.
|
|
161
|
+
See [querying.md](./querying.md) for declarations, conditional indexes, filters,
|
|
162
|
+
authorization, and child-page continuation. Existing native pagination can keep
|
|
163
|
+
using `paginated(dto)`; the querying engine is independent of `include`.
|
|
164
|
+
|
|
85
165
|
## Opinionated timestamps
|
|
86
166
|
|
|
87
167
|
Every `zodTable` bakes in three optional, **server-owned** fields:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component",
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"types": "./dist/zod-table.d.mts",
|
|
31
31
|
"import": "./dist/zod-table.mjs"
|
|
32
32
|
},
|
|
33
|
+
"./relations": {
|
|
34
|
+
"types": "./dist/relations-public.d.mts",
|
|
35
|
+
"import": "./dist/relations-public.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./querying": {
|
|
38
|
+
"types": "./dist/querying.d.mts",
|
|
39
|
+
"import": "./dist/querying.mjs"
|
|
40
|
+
},
|
|
33
41
|
"./effect": {
|
|
34
42
|
"types": "./dist/effect.d.mts",
|
|
35
43
|
"import": "./dist/effect.mjs"
|
|
@@ -134,6 +142,7 @@
|
|
|
134
142
|
"test:package:oxlint": "bun run build && node scripts/test-packed-oxlint.mjs",
|
|
135
143
|
"test:package:effect": "bun run build && node scripts/test-packed-effect.mjs",
|
|
136
144
|
"test:package:auth": "bun run build && node scripts/test-packed-auth.mjs",
|
|
145
|
+
"test:package:querying": "bun run build && node scripts/test-packed-querying.mjs",
|
|
137
146
|
"test:package:smoke:effect": "bun run build && node scripts/smoke-packed-effect.mjs",
|
|
138
147
|
"test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
|
|
139
148
|
"test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
|
|
@@ -145,6 +154,7 @@
|
|
|
145
154
|
},
|
|
146
155
|
"dependencies": {
|
|
147
156
|
"@convex-dev/workflow": "0.4.10",
|
|
157
|
+
"@noble/ciphers": "2.4.0",
|
|
148
158
|
"@oxlint/plugins": "1.87.0",
|
|
149
159
|
"@standard-schema/spec": "^1.1.0",
|
|
150
160
|
"convex-audit-log": "^0.2.0",
|