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.
Files changed (79) hide show
  1. package/AGENTS.md +47 -0
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +144 -0
  4. package/dist/auth.d.mts +3 -1
  5. package/dist/auth.mjs +1 -1
  6. package/dist/auth.mjs.map +1 -1
  7. package/dist/index.d.mts +5 -2
  8. package/dist/index.mjs +1 -1
  9. package/dist/modules/contracts/binding.d.mts +13 -1
  10. package/dist/modules/contracts/binding.mjs +1 -1
  11. package/dist/modules/contracts/binding.mjs.map +1 -1
  12. package/dist/modules/effect/api-zod.d.mts +1 -1
  13. package/dist/modules/effect/command.d.mts +13 -9
  14. package/dist/modules/effect/command.mjs.map +1 -1
  15. package/dist/modules/effect/domain.d.mts +31 -3
  16. package/dist/modules/effect/domain.mjs.map +1 -1
  17. package/dist/modules/effect/query.d.mts +6 -5
  18. package/dist/modules/effect/query.mjs.map +1 -1
  19. package/dist/modules/foundation/foundation.d.mts +14 -13
  20. package/dist/modules/foundation/foundation.mjs.map +1 -1
  21. package/dist/modules/foundation/operation.d.mts +20 -1
  22. package/dist/modules/foundation/operation.mjs.map +1 -1
  23. package/dist/oxlint/architecture.d.mts +2 -0
  24. package/dist/oxlint/architecture.mjs +1 -1
  25. package/dist/oxlint/architecture.mjs.map +1 -1
  26. package/dist/oxlint/ast.d.mts +10 -1
  27. package/dist/oxlint/ast.mjs +1 -1
  28. package/dist/oxlint/ast.mjs.map +1 -1
  29. package/dist/oxlint.d.mts +1 -1
  30. package/dist/oxlint.mjs +1 -1
  31. package/dist/oxlint.mjs.map +1 -1
  32. package/dist/querying/cursors.d.mts +45 -0
  33. package/dist/querying/cursors.mjs +2 -0
  34. package/dist/querying/cursors.mjs.map +1 -0
  35. package/dist/querying/execution.d.mts +30 -0
  36. package/dist/querying/execution.mjs +2 -0
  37. package/dist/querying/execution.mjs.map +1 -0
  38. package/dist/querying/filters.d.mts +136 -0
  39. package/dist/querying/filters.mjs +2 -0
  40. package/dist/querying/filters.mjs.map +1 -0
  41. package/dist/querying/planner.d.mts +50 -0
  42. package/dist/querying/planner.mjs +2 -0
  43. package/dist/querying/planner.mjs.map +1 -0
  44. package/dist/querying/relationships.d.mts +41 -0
  45. package/dist/querying/relationships.mjs +2 -0
  46. package/dist/querying/relationships.mjs.map +1 -0
  47. package/dist/querying/security.d.mts +107 -0
  48. package/dist/querying/security.mjs +2 -0
  49. package/dist/querying/security.mjs.map +1 -0
  50. package/dist/querying.d.mts +39 -0
  51. package/dist/querying.mjs +2 -0
  52. package/dist/querying.mjs.map +1 -0
  53. package/dist/relations-public.d.mts +2 -0
  54. package/dist/relations-public.mjs +1 -0
  55. package/dist/relations.d.mts +155 -0
  56. package/dist/relations.mjs +2 -0
  57. package/dist/relations.mjs.map +1 -0
  58. package/dist/zod-convex.d.mts +10 -0
  59. package/dist/zod-convex.mjs +2 -0
  60. package/dist/zod-convex.mjs.map +1 -0
  61. package/dist/zod-registration.d.mts +10 -0
  62. package/dist/zod-registration.mjs +2 -0
  63. package/dist/zod-registration.mjs.map +1 -0
  64. package/dist/zod-table.d.mts +63 -8
  65. package/dist/zod-table.mjs +1 -1
  66. package/dist/zod-table.mjs.map +1 -1
  67. package/docs/conventions.md +12 -0
  68. package/docs/domain-contracts.md +42 -9
  69. package/docs/llms.md +15 -5
  70. package/docs/oxlint.md +129 -25
  71. package/docs/querying.md +293 -0
  72. package/docs/zod-table.md +93 -13
  73. package/package.json +11 -1
  74. package/scripts/owned-process.mjs +140 -0
  75. package/scripts/owned-process.test.mjs +107 -0
  76. package/scripts/smoke-packed-querying.mjs +296 -0
  77. package/scripts/test-packed-effect.mjs +10 -1
  78. package/scripts/test-packed-oxlint.mjs +5 -1
  79. 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 `new Foundation` outside their root files, or repeated in one file. | `conventions.md`, Root files |
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` | Direct `ctx.db.query(...).collect()` chains and `ctx.include(...).resolve()` chains in host code. | `llms.md`, Bounded reads; `zod-table.md` |
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
- arbitrary variable reassignment, wrapper functions, destructured database
97
- methods, and calls split across variables are not traced. Database checks
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,
@@ -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 | 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
- | `toPublicDto(row)` | projects **and re-parses** a row into the DTO | query handlers |
62
- | `insert(omit?)` / `update(omit?)` | schema with per-callsite field omission | specialized mutations |
63
- | `tools.insert` / `tools.update` / `tools.id` | ready-made LLM tool input schemas | agent tool definitions |
64
- | `tableName` | the literal table name | indexes, triggers registration |
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.12",
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",