@lunora/server 1.0.0-alpha.1 → 1.0.0-alpha.100

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 (83) hide show
  1. package/LICENSE.md +219 -0
  2. package/README.md +51 -1
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/data-model.d.mts +324 -140
  5. package/dist/data-model.d.ts +324 -140
  6. package/dist/data-model.mjs +0 -1
  7. package/dist/drizzle.mjs +1 -1
  8. package/dist/index.d.mts +2917 -1356
  9. package/dist/index.d.ts +2917 -1356
  10. package/dist/index.mjs +1 -24
  11. package/dist/otel.d.mts +543 -0
  12. package/dist/otel.d.ts +543 -0
  13. package/dist/otel.mjs +1 -0
  14. package/dist/packem_shared/ACTION_CACHE_DEFAULT_TTL_MS-JWV1BkYm.mjs +1 -0
  15. package/dist/packem_shared/DEFAULT_LIMIT-DC-M6faS.mjs +1 -0
  16. package/dist/packem_shared/DOCUMENT_HISTORY_REDACTED_FIELDS-Lxhp5uwN.mjs +1 -0
  17. package/dist/packem_shared/LunoraEnvError-BurH8oOM.mjs +3 -0
  18. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-lsooaSaF.mjs +1 -0
  19. package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
  20. package/dist/packem_shared/apply-output-C5wZ5EAL.mjs +1 -0
  21. package/dist/packem_shared/asBucketStorage-DoGYOjq3.mjs +1 -0
  22. package/dist/packem_shared/beginDeferredSchedules-gJlbW6h9.mjs +1 -0
  23. package/dist/packem_shared/bindOrm-ChQydkdL.mjs +1 -0
  24. package/dist/packem_shared/buildMaskRegistry-DpWMtalG.mjs +1 -0
  25. package/dist/packem_shared/buildRlsReadRegistry-De9fP25P.mjs +1 -0
  26. package/dist/packem_shared/compose-middleware-3i5Qk3sb.mjs +1 -0
  27. package/dist/packem_shared/composePluginMiddleware-pe7Ncehp.mjs +1 -0
  28. package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
  29. package/dist/packem_shared/createPolicyDsl-BZa6SqLJ.mjs +1 -0
  30. package/dist/packem_shared/createSecrets-D6rLB42U.mjs +1 -0
  31. package/dist/packem_shared/defineAggregateIndex-DFVCeuX1.mjs +1 -0
  32. package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
  33. package/dist/packem_shared/defineMigration-CXOS0Bvq.mjs +1 -0
  34. package/dist/packem_shared/defineMutator-DKy8UtbB.mjs +1 -0
  35. package/dist/packem_shared/defineShape-BWgwFuMT.mjs +1 -0
  36. package/dist/packem_shared/defineStorageRule-Dv4nJE0H.mjs +1 -0
  37. package/dist/packem_shared/flushDeferredDeletes-DEJXlhop.mjs +1 -0
  38. package/dist/packem_shared/fnv1a-D3ueNTWB.mjs +1 -0
  39. package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
  40. package/dist/packem_shared/httpAction-CF2zTm9X.mjs +5 -0
  41. package/dist/packem_shared/initLunora-D5TSiy5j.mjs +1 -0
  42. package/dist/packem_shared/mask-D6hJ86fP.mjs +1 -0
  43. package/dist/packem_shared/middleware-BXIVIudl.mjs +1 -0
  44. package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
  45. package/dist/packem_shared/onQueryChange-CatdYnH5.mjs +1 -0
  46. package/dist/packem_shared/plugin-yKCbHnlj.mjs +1 -0
  47. package/dist/packem_shared/policy-tag-V0lqFzgS.mjs +1 -0
  48. package/dist/packem_shared/protectPublic-Csf6ObbJ.mjs +1 -0
  49. package/dist/packem_shared/rls-BVJokL_k.mjs +1 -0
  50. package/dist/packem_shared/run-middleware-CuM-Chm3.mjs +1 -0
  51. package/dist/packem_shared/serveStorageObject-Cof46mpm.mjs +1 -0
  52. package/dist/packem_shared/stable-key-B_BlboiY.mjs +1 -0
  53. package/dist/packem_shared/storageRules-BYT8_Re0.mjs +1 -0
  54. package/dist/packem_shared/types.d-B7FLdYUD.d.mts +148 -0
  55. package/dist/packem_shared/types.d-BCtjbIzl.d.ts +148 -0
  56. package/dist/packem_shared/wire-codec-BfIZrTJe.mjs +1 -0
  57. package/dist/rls/testing.d.mts +42 -34
  58. package/dist/rls/testing.d.ts +42 -34
  59. package/dist/rls/testing.mjs +1 -49
  60. package/dist/types.d.mts +1798 -439
  61. package/dist/types.d.ts +1798 -439
  62. package/dist/types.mjs +1 -31
  63. package/package.json +18 -5
  64. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  65. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  66. package/dist/packem_shared/bindTableFacade-DCuyr46L.mjs +0 -71
  67. package/dist/packem_shared/defineAggregateIndex-DzqxtAyV.mjs +0 -236
  68. package/dist/packem_shared/defineEnv-DjFkpkSP.mjs +0 -187
  69. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
  70. package/dist/packem_shared/definePolicy-De67zPDS.mjs +0 -29
  71. package/dist/packem_shared/definePresence-D5LtwGl0.mjs +0 -114
  72. package/dist/packem_shared/defineSchemaExtension-Ck5_TUO8.mjs +0 -100
  73. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
  74. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
  75. package/dist/packem_shared/initLunora-CATvPsVt.mjs +0 -86
  76. package/dist/packem_shared/mask-CkZJHHMM.mjs +0 -211
  77. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  78. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
  79. package/dist/packem_shared/rls-Zhf5wEeJ.mjs +0 -551
  80. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
  81. package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
  82. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  83. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
@@ -1,25 +1,25 @@
1
1
  /**
2
- * Schema-independent type machinery for the generated data model.
3
- *
4
- * `@lunora/codegen` emits `lunora/_generated/dataModel.ts` with the
5
- * schema-specific pieces (the per-table `Doc_*` / `Insert_*` interfaces, the
6
- * `DataModel` / `Relations` / index-name maps) and then binds the generics
7
- * below to them. Everything here is identical for every project, so it lives
8
- * in the shipped package rather than in generated output — evolving the query
9
- * DSL or the table-facade API no longer regenerates a single line of a user's
10
- * `_generated` directory.
11
- *
12
- * The generics are parameterized over the generated maps:
13
- * - `DM` — `DataModel`: table name → document type
14
- * - `IM` — `InsertModel`: table name → insert shape
15
- * - `REL` — `Relations`: table name → relation-descriptor map
16
- * - `RANK` — `RankIndexNamesByTable`: table name → declared rank-index names
17
- * - `SEARCH` — `SearchIndexNamesByTable`: table name → declared search-index names
18
- *
19
- * Relation descriptors are matched structurally (`{ __relationKind; __target }`)
20
- * so this module needs no reference to the project-local `OneRelation` /
21
- * `ManyRelation` aliases the codegen still emits.
22
- */
2
+ * Schema-independent type machinery for the generated data model.
3
+ *
4
+ * `@lunora/codegen` emits `lunora/_generated/dataModel.ts` with the
5
+ * schema-specific pieces (the per-table `Doc_*` / `Insert_*` interfaces, the
6
+ * `DataModel` / `Relations` / index-name maps) and then binds the generics
7
+ * below to them. Everything here is identical for every project, so it lives
8
+ * in the shipped package rather than in generated output — evolving the query
9
+ * DSL or the table-facade API no longer regenerates a single line of a user's
10
+ * `_generated` directory.
11
+ *
12
+ * The generics are parameterized over the generated maps:
13
+ * - `DM` — `DataModel`: table name → document type
14
+ * - `IM` — `InsertModel`: table name → insert shape
15
+ * - `REL` — `Relations`: table name → relation-descriptor map
16
+ * - `RANK` — `RankIndexNamesByTable`: table name → declared rank-index names
17
+ * - `SEARCH` — `SearchIndexNamesByTable`: table name → declared search-index names
18
+ *
19
+ * Relation descriptors are matched structurally (`{ __relationKind; __target }`)
20
+ * so this module needs no reference to the project-local `OneRelation` /
21
+ * `ManyRelation` aliases the codegen still emits.
22
+ */
23
23
  /** A branded id for table `TName`. Structurally a `string` at runtime. */
24
24
  type Id<TName extends string> = string & {
25
25
  readonly __table: TName;
@@ -38,7 +38,7 @@ interface WhereOperators<T> {
38
38
  notIn?: T[];
39
39
  }
40
40
  /** A typed `where` tree over a document's columns. */
41
- type Where<TDocument> = { [K in keyof TDocument]?: TDocument[K] | WhereOperators<TDocument[K]> } & {
41
+ type Where<TDocument> = { [K in keyof TDocument]?: TDocument[K] | WhereOperators<TDocument[K]>; } & {
42
42
  AND?: Where<TDocument>[];
43
43
  NOT?: Where<TDocument>;
44
44
  OR?: Where<TDocument>[];
@@ -49,47 +49,54 @@ interface QueryArgs<TDocument> {
49
49
  cursor?: null | string;
50
50
  limit?: number;
51
51
  orderBy?: OrderBy<TDocument>[];
52
+ /**
53
+ * Project each returned row down to these columns (plus the system fields
54
+ * `_id`/`_creationTime`, always retained). Trims wire payload for wide rows;
55
+ * relations requested via `with` are still attached. Reactivity is unaffected —
56
+ * the engine still reads the whole row to track dependencies.
57
+ */
58
+ select?: ReadonlyArray<keyof TDocument & string>;
52
59
  where?: Where<TDocument>;
53
60
  }
54
61
  /**
55
- * A to-one relation predicate node. `is` matches rows whose related record
56
- * satisfies `W`; `isNot` matches rows whose related record fails `W` *or* has
57
- * no related record at all (a null/dangling FK) — Prisma's semantics.
58
- */
62
+ * A to-one relation predicate node. `is` matches rows whose related record
63
+ * satisfies `W`; `isNot` matches rows whose related record fails `W` *or* has
64
+ * no related record at all (a null/dangling FK) — Prisma's semantics.
65
+ */
59
66
  interface OneRelationWhere<W> {
60
67
  is?: W;
61
68
  isNot?: W;
62
69
  }
63
70
  /**
64
- * A to-many relation predicate node. `some` ⇒ at least one related row matches
65
- * `W`; `none` ⇒ no related row matches (childless parents included); `every` ⇒
66
- * every *readable* related row matches (vacuously true for childless parents).
67
- */
71
+ * A to-many relation predicate node. `some` ⇒ at least one related row matches
72
+ * `W`; `none` ⇒ no related row matches (childless parents included); `every` ⇒
73
+ * every *readable* related row matches (vacuously true for childless parents).
74
+ */
68
75
  interface ManyRelationWhere<W> {
69
76
  every?: W;
70
77
  none?: W;
71
78
  some?: W;
72
79
  }
73
80
  /**
74
- * The relation-predicate portion of {@link WhereOf}: each declared relation on
75
- * `T` contributes a kind-dispatched node — `one` → `{ is?; isNot? }`, `many` →
76
- * `{ some?; none?; every? }` — whose inner type is the target table's own
77
- * relation-aware `where` (so multi-hop predicates type-check inside-out).
78
- */
81
+ * The relation-predicate portion of {@link WhereOf}: each declared relation on
82
+ * `T` contributes a kind-dispatched node — `one` → `{ is?; isNot? }`, `many` →
83
+ * `{ some?; none?; every? }` — whose inner type is the target table's own
84
+ * relation-aware `where` (so multi-hop predicates type-check inside-out).
85
+ */
79
86
  type RelationWhere<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
80
87
  __relationKind: "one";
81
88
  __target: infer Target extends keyof DM;
82
89
  } ? OneRelationWhere<WhereOf<DM, REL, Target>> : REL[T][K] extends {
83
90
  __relationKind: "many";
84
91
  __target: infer Target extends keyof DM;
85
- } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never };
92
+ } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never; };
86
93
  /**
87
- * Relation-aware `where` tree — the column predicates of {@link Where} plus
88
- * Prisma-style relation predicates resolved by the `@lunora/do` pre-resolver.
89
- * `Where&lt;DM[T]>` stays the column-only structural mirror for back-compat; the
90
- * table facade threads `REL` through this richer form.
91
- */
92
- type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = RelationWhere<DM, REL, T> & { [K in keyof DM[T]]?: DM[T][K] | WhereOperators<DM[T][K]> } & {
94
+ * Relation-aware `where` tree — the column predicates of {@link Where} plus
95
+ * Prisma-style relation predicates resolved by the `@lunora/do` pre-resolver.
96
+ * `Where<DM[T]>` stays the column-only structural mirror for back-compat; the
97
+ * table facade threads `REL` through this richer form.
98
+ */
99
+ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = RelationWhere<DM, REL, T> & { [K in keyof DM[T]]?: DM[T][K] | WhereOperators<DM[T][K]>; } & {
93
100
  AND?: WhereOf<DM, REL, T>[];
94
101
  NOT?: WhereOf<DM, REL, T>;
95
102
  OR?: WhereOf<DM, REL, T>[];
@@ -97,6 +104,12 @@ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = Rel
97
104
  /** {@link QueryArgs} with the relation-aware {@link WhereOf} `where` typing. */
98
105
  interface QueryArgsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
99
106
  cursor?: null | string;
107
+ /**
108
+ * Include soft-deleted rows (`.softDelete()` tables only). Default hides them;
109
+ * `true` returns deleted rows alongside live ones. No effect on a table
110
+ * without `.softDelete()`.
111
+ */
112
+ includeDeleted?: boolean;
100
113
  limit?: number;
101
114
  orderBy?: OrderBy<DM[T]>[];
102
115
  where?: WhereOf<DM, REL, T>;
@@ -110,12 +123,16 @@ interface QueryPage<TDocument> {
110
123
  type NestedWithArgument<WK> = WK extends {
111
124
  with: infer NW;
112
125
  } ? NW : {};
126
+ /** The nested `select` tuple inside a relation's with-value, or `undefined` (no projection). */
127
+ type NestedSelectArgument<WK> = WK extends {
128
+ select: infer S;
129
+ } ? S : undefined;
113
130
  /**
114
- * The `with` argument for table `T`: each relation can be `true` (load with no
115
- * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`
116
- * plus a nested `with`; `one` relations accept only a nested `with`. The
117
- * reserved `_count` key requests per-relation aggregate counts.
118
- */
131
+ * The `with` argument for table `T`: each relation can be `true` (load with no
132
+ * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`/
133
+ * `select` plus a nested `with`; `one` relations accept `select` + a nested
134
+ * `with`. The reserved `_count` key requests per-relation aggregate counts.
135
+ */
119
136
  type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
120
137
  __relationKind: "many";
121
138
  __target: infer Target extends keyof DM;
@@ -125,54 +142,72 @@ type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [
125
142
  __relationKind: "one";
126
143
  __target: infer Target extends keyof DM;
127
144
  } ? boolean | {
145
+ select?: ReadonlyArray<keyof DM[Target] & string>;
128
146
  with?: WithArg<DM, REL, Target>;
129
- } : never } & {
130
- _count?: { [K in keyof REL[T]]?: true };
147
+ } : never; } & {
148
+ _count?: { [K in keyof REL[T]]?: true; };
131
149
  };
132
- /** Resolve a single relation descriptor + its with-value to the loaded type. */
150
+ /**
151
+ * Resolve a single relation descriptor + its with-value to the loaded type,
152
+ * threading the nested `select` tuple into the projected child shape (the 5th
153
+ * `LoadWith` arg) so `with: { author: { select: ["name"] } }` narrows the loaded
154
+ * `author` to the selected columns + system fields.
155
+ */
133
156
  type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
134
157
  __relationKind: "one";
135
158
  __target: infer Target extends keyof DM;
136
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>> | null : R extends {
159
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>> | null : R extends {
137
160
  __relationKind: "many";
138
161
  __target: infer Target extends keyof DM;
139
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>>[] : never;
162
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>>[] : never;
140
163
  /** The relation keys of `W` that were actually requested (not `false`/`undefined`). */
141
- type LoadedRelations<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = { [K in keyof W as K extends keyof REL[T] ? (W[K] extends false | undefined ? never : K) : never]: K extends keyof REL[T] ? LoadRelation<DM, REL, REL[T][K], W[K]> : never };
164
+ type LoadedRelations<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = { [K in keyof W as K extends keyof REL[T] ? (W[K] extends false | undefined ? never : K) : never]: K extends keyof REL[T] ? LoadRelation<DM, REL, REL[T][K], W[K]> : never; };
142
165
  /** The `_count` projection of `W`, if any. */
143
166
  type LoadedCount<W> = W extends {
144
167
  _count: infer C;
145
168
  } ? {
146
- _count: { [K in keyof C]: number };
169
+ _count: { [K in keyof C]: number; };
147
170
  } : {};
148
- /** `Doc&lt;T>` narrowed to exactly the relations requested in the with-arg `W`. */
149
- type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = DM[T] & LoadedCount<W> & LoadedRelations<DM, REL, T, W>;
171
+ /** System columns a `select` projection always retains, so cursors and by-id reuse keep working. */
172
+ type SelectAlwaysKeep<DM, T extends keyof DM> = ("_creationTime" | "_id") & keyof DM[T];
173
+ /**
174
+ * `DM[T]` narrowed to the columns named by a `select` tuple `S` (plus the system
175
+ * fields). `undefined` (the default — no `select`) keeps the full document.
176
+ */
177
+ type ProjectDoc<DM, T extends keyof DM, S> = S extends ReadonlyArray<infer K> ? (K extends keyof DM[T] ? Pick<DM[T], (K & keyof DM[T]) | SelectAlwaysKeep<DM, T>> : DM[T]) : DM[T];
178
+ /**
179
+ * `Doc<T>` narrowed to exactly the relations requested in the with-arg `W` and,
180
+ * when a `select` tuple `S` is supplied, to its projected columns. `S` defaults
181
+ * to `undefined` so the 4-argument form (the codegen-emitted callers) keeps the
182
+ * full document.
183
+ */
184
+ type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W, S = undefined> = LoadedCount<W> & LoadedRelations<DM, REL, T, W> & ProjectDoc<DM, T, S>;
150
185
  /** Reducer applied by an aggregate (`avg`/`count`/`max`/`min`/`sum`). */
151
186
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
152
187
  /**
153
- * Query-options shape shared by every aggregate reader. The RLS-aware ctx
154
- * populates `baseWhere` so it composes here without a hard import.
155
- * `restrictsCounts: true` flips `count()` into a thrown `COUNT_RLS_UNSUPPORTED`
156
- * `LunoraError` rather than silently undercount.
157
- */
188
+ * Query-options shape shared by every aggregate reader. The RLS-aware ctx
189
+ * populates `baseWhere` so it composes here without a hard import.
190
+ * `restrictsCounts: true` flips `count()` into a thrown `COUNT_RLS_UNSUPPORTED`
191
+ * `LunoraError` rather than silently undercount.
192
+ */
158
193
  interface RestrictableQueryOptions<TDocument> {
159
194
  baseWhere?: Where<TDocument>;
160
195
  restrictsCounts?: boolean;
161
196
  where?: Where<TDocument>;
162
197
  }
163
198
  /**
164
- * Relation-aware twin of {@link RestrictableQueryOptions}. The `@lunora/do`
165
- * pre-resolver now resolves relation predicates on the `count`/`aggregate`/
166
- * `groupBy` paths too (semijoin), so the typed surface threads `REL` through
167
- * `where`/`baseWhere` to match. `rank`/`rankPage` stay column-only — they use
168
- * `where` solely to pin a partition and fail closed on a relation predicate.
169
- */
199
+ * Relation-aware twin of {@link RestrictableQueryOptions}. The `@lunora/do`
200
+ * pre-resolver now resolves relation predicates on the `count`/`aggregate`/
201
+ * `groupBy` paths too (semijoin), so the typed surface threads `REL` through
202
+ * `where`/`baseWhere` to match. `rank`/`rankPage` stay column-only — they use
203
+ * `where` solely to pin a partition and fail closed on a relation predicate.
204
+ */
170
205
  interface RestrictableQueryOptionsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
171
206
  baseWhere?: WhereOf<DM, REL, T>;
172
207
  restrictsCounts?: boolean;
173
208
  where?: WhereOf<DM, REL, T>;
174
209
  }
175
- /** Args for `ctx.db.&lt;table>.aggregate({ op, field?, where? })`. */
210
+ /** Args for `ctx.db.<table>.aggregate({ op, field?, where? })`. */
176
211
  interface TableAggregateOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
177
212
  field?: keyof TDocument & string;
178
213
  op: AggregateOp;
@@ -182,7 +217,7 @@ interface TableAggregateOptionsOf<DM, REL extends Record<keyof DM, object>, T ex
182
217
  field?: keyof DM[T] & string;
183
218
  op: AggregateOp;
184
219
  }
185
- /** Args for `ctx.db.&lt;table>.groupBy({ by, agg?, where? })`. */
220
+ /** Args for `ctx.db.<table>.groupBy({ by, agg?, where? })`. */
186
221
  interface TableGroupByOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
187
222
  agg?: {
188
223
  field?: keyof TDocument & string;
@@ -203,7 +238,7 @@ interface GroupByEntry<TDocument> {
203
238
  key: Partial<TDocument>;
204
239
  value: null | number;
205
240
  }
206
- /** Args for `ctx.db.&lt;table>.rank(name, args)`. `row` is either an id or a row doc. */
241
+ /** Args for `ctx.db.<table>.rank(name, args)`. `row` is either an id or a row doc. */
207
242
  interface TableRankOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
208
243
  row: string | TDocument;
209
244
  }
@@ -212,7 +247,7 @@ interface RankResult {
212
247
  position: number;
213
248
  total: number;
214
249
  }
215
- /** Args for `ctx.db.&lt;table>.rankPage(name, args)`. */
250
+ /** Args for `ctx.db.<table>.rankPage(name, args)`. */
216
251
  interface TableRankPageOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
217
252
  cursor?: null | string;
218
253
  take?: number;
@@ -224,105 +259,254 @@ interface RankPage<TDocument> {
224
259
  page: TDocument[];
225
260
  }
226
261
  /**
227
- * Builder passed to `.withSearchIndex(name, q => …)`. `.search(field, query)`
228
- * runs the full-text match against the index's searchable field; `.eq(field,
229
- * value)` narrows by a declared filter field. Field names are constrained to
230
- * the table's columns.
231
- */
262
+ * Builder passed to `.withSearchIndex(name, q => …)`. `.search(field, query)`
263
+ * runs the full-text match against the index's searchable field; `.eq(field,
264
+ * value)` narrows by a declared filter field. Field names are constrained to
265
+ * the table's columns.
266
+ */
232
267
  interface SearchFilterBuilder<TDocument> {
233
268
  eq: <F extends keyof TDocument & string>(field: F, value: TDocument[F]) => SearchFilterBuilder<TDocument>;
234
269
  search: (field: keyof TDocument & string, query: string) => SearchFilterBuilder<TDocument>;
235
270
  }
236
271
  /**
237
- * Chainable reader returned by `.withSearchIndex()` — rows come back ordered
238
- * by relevance. `.paginate()` is intentionally absent (a relevance-ordered
239
- * search can't keyset-paginate); cap the result set with `.take(n)`.
240
- */
272
+ * Chainable reader returned by `.withSearchIndex()` — rows come back ordered
273
+ * by relevance. `.paginate()` is intentionally absent (a relevance-ordered
274
+ * search can't keyset-paginate); cap the result set with `.take(n)`.
275
+ */
241
276
  interface SearchReader<TDocument> {
242
277
  collect: () => Promise<TDocument[]>;
278
+ /**
279
+ * Like `.collect()`, but pairs each matched document with the relevance
280
+ * score the FTS engine already computed to produce the descending order —
281
+ * `.collect()` throws it away after sorting by it, this surfaces it
282
+ * instead. Ordered by `score` descending, same order as `.collect()`.
283
+ */
284
+ collectWithScores: () => Promise<{
285
+ document: TDocument;
286
+ score: number;
287
+ }[]>;
288
+ first: () => Promise<TDocument | null>;
289
+ take: (limit: number) => Promise<TDocument[]>;
290
+ unique: () => Promise<TDocument | null>;
291
+ }
292
+ /** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
293
+ interface GeoPointInput {
294
+ lat: number;
295
+ lng: number;
296
+ }
297
+ /** An axis-aligned latitude/longitude bounding box (`sw`/`ne` corners). */
298
+ interface GeoBoundingBox {
299
+ ne: GeoPointInput;
300
+ sw: GeoPointInput;
301
+ }
302
+ /**
303
+ * Builder passed to `.withGeoIndex(name, q => …)`. Call exactly one of
304
+ * `.near(point, radiusMeters)` (proximity, nearest-first) or `.within(box)`
305
+ * (bounding-box).
306
+ */
307
+ interface GeoFilterBuilder {
308
+ near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
309
+ within: (box: GeoBoundingBox) => GeoFilterBuilder;
310
+ }
311
+ /**
312
+ * Chainable reader returned by `.withGeoIndex()`. `.near()` results come back
313
+ * ordered nearest-first; `.within()` results by row creation time. `.paginate()`
314
+ * is intentionally absent — cap the result set with `.take(n)`.
315
+ */
316
+ interface GeoReader<TDocument> {
317
+ collect: () => Promise<TDocument[]>;
318
+ /**
319
+ * Like `.collect()`, but pairs each matched document with the distance
320
+ * (in meters) from `.near()`'s point that the read already computed to
321
+ * produce the nearest-first order. `.within()` box matches have no
322
+ * point-distance metric, so `distanceMeters` is `null` for those rows
323
+ * rather than a misleading `0`.
324
+ */
325
+ collectWithScores: () => Promise<{
326
+ distanceMeters: null | number;
327
+ document: TDocument;
328
+ }[]>;
243
329
  first: () => Promise<TDocument | null>;
244
330
  take: (limit: number) => Promise<TDocument[]>;
245
331
  unique: () => Promise<TDocument | null>;
246
332
  }
247
- /** Read-only typed table accessor exposed on `QueryCtx.db.&lt;table>`. */
248
- interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM> {
333
+ /** Read-only typed table accessor exposed on `QueryCtx.db.<table>`. */
334
+ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM, GEO extends Record<keyof DM, string> = Record<keyof DM, never>> {
249
335
  /**
250
- * Reduce rows in this table to a scalar (`avg`/`max`/`min`/`sum` — `count`
251
- * lives on its own method). Routes through a declared `aggregateIndex` when
252
- * the planner can prove the request is answerable; otherwise scans.
253
- */
336
+ * Reduce rows in this table to a scalar (`avg`/`max`/`min`/`sum` — `count`
337
+ * lives on its own method). Routes through a declared `aggregateIndex` when
338
+ * the planner can prove the request is answerable; otherwise scans.
339
+ */
254
340
  aggregate: (options: TableAggregateOptionsOf<DM, REL, T>) => Promise<null | number>;
255
341
  /**
256
- * Count rows. The planner routes `where` keys that match a declared
257
- * `aggregateIndex.by` set to the indexed counter (no scan); otherwise
258
- * falls back to a SCAN. Accepts either a bare `where` tree or the broader
259
- * `RestrictableQueryOptions` shape; the latter is the seam the RLS layer
260
- * uses to inject `baseWhere` and `restrictsCounts`.
261
- */
342
+ * Count rows. The planner routes `where` keys that match a declared
343
+ * `aggregateIndex.by` set to the indexed counter (no scan); otherwise
344
+ * falls back to a SCAN. Accepts either a bare `where` tree or the broader
345
+ * `RestrictableQueryOptions` shape; the latter is the seam the RLS layer
346
+ * uses to inject `baseWhere` and `restrictsCounts`.
347
+ */
262
348
  count: (where?: RestrictableQueryOptionsOf<DM, REL, T> | WhereOf<DM, REL, T>) => Promise<number>;
263
- findFirst: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
349
+ /** `true` when at least one row matches `where` (any row when omitted). RLS-filtered exactly like `findFirst`. */
350
+ exists: (where?: WhereOf<DM, REL, T>) => Promise<boolean>;
351
+ findFirst: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
352
+ select?: S;
264
353
  with?: W;
265
- }) => Promise<LoadWith<DM, REL, T, W> | null>;
266
- findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
354
+ }) => Promise<LoadWith<DM, REL, T, W, S> | null>;
355
+ findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
356
+ select?: S;
267
357
  with?: W;
268
- }) => Promise<LoadWith<DM, REL, T, W>>;
269
- findMany: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
358
+ }) => Promise<LoadWith<DM, REL, T, W, S>>;
359
+ findMany: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
360
+ select?: S;
270
361
  with?: W;
271
- }) => Promise<QueryPage<LoadWith<DM, REL, T, W>>>;
362
+ }) => Promise<QueryPage<LoadWith<DM, REL, T, W, S>>>;
272
363
  get: (id: Id<string & T>) => Promise<DM[T] | null>;
273
364
  /**
274
- * Group rows by the named keys and apply `agg` per group (defaults to
275
- * `count`). Answered from the counter table when an aggregate index's
276
- * `by` matches `options.by` exactly; otherwise scans.
277
- */
365
+ * Group rows by the named keys and apply `agg` per group (defaults to
366
+ * `count`). Answered from the counter table when an aggregate index's
367
+ * `by` matches `options.by` exactly; otherwise scans.
368
+ */
278
369
  groupBy: (options: TableGroupByOptionsOf<DM, REL, T>) => Promise<ReadonlyArray<GroupByEntry<DM[T]>>>;
279
370
  /**
280
- * Return the 1-based position of `options.row` within its partition
281
- * under the declared rankIndex `indexName`, plus the partition's total
282
- * row count. `null` when the row isn't in the index. Honors the same
283
- * `baseWhere` / `restrictsCounts` RLS seam as `count()`.
284
- */
371
+ * Return the 1-based position of `options.row` within its partition
372
+ * under the declared rankIndex `indexName`, plus the partition's total
373
+ * row count. `null` when the row isn't in the index. Honors the same
374
+ * `baseWhere` / `restrictsCounts` RLS seam as `count()`.
375
+ */
285
376
  rank: (indexName: RANK[T], options: TableRankOptions<DM[T]>) => Promise<null | RankResult>;
286
377
  /**
287
- * Walk the rank companion in declared sort order — sorted pagination
288
- * accelerator. `options.where` may pin the partition; `cursor`/`take`
289
- * follow the Convex-style keyset shape.
290
- */
378
+ * Walk the rank companion in declared sort order — sorted pagination
379
+ * accelerator. `options.where` may pin the partition; `cursor`/`take`
380
+ * follow the Convex-style keyset shape.
381
+ */
291
382
  rankPage: (indexName: RANK[T], options?: TableRankPageOptions<DM[T]>) => Promise<RankPage<DM[T]>>;
292
383
  /**
293
- * Restrict the query to a declared `.searchIndex()` and run a full-text
294
- * match. `indexName` is constrained to this table's search indexes
295
- * (`never` when it declares none). Returns a relevance-ordered reader —
296
- * finish with `.take(n)` / `.collect()`.
297
- */
384
+ * Restrict the query to a declared `.geoIndex()` and run a proximity /
385
+ * bounding-box match. `indexName` is constrained to this table's geo indexes
386
+ * (`never` when it declares none). Returns a distance-ordered reader —
387
+ * finish with `.take(n)` / `.collect()`.
388
+ */
389
+ withGeoIndex: (indexName: GEO[T], build: (q: GeoFilterBuilder) => GeoFilterBuilder) => GeoReader<DM[T]>;
390
+ /**
391
+ * Restrict the query to a declared `.searchIndex()` and run a full-text
392
+ * match. `indexName` is constrained to this table's search indexes
393
+ * (`never` when it declares none). Returns a relevance-ordered reader —
394
+ * finish with `.take(n)` / `.collect()`.
395
+ */
298
396
  withSearchIndex: (indexName: SEARCH[T], search: (q: SearchFilterBuilder<DM[T]>) => SearchFilterBuilder<DM[T]>) => SearchReader<DM[T]>;
299
397
  }
300
- /** Read-write typed table accessor exposed on `MutationCtx.db.&lt;table>` / `ActionCtx.db.&lt;table>`. */
301
- interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM> extends TableReaderFacade<DM, REL, RANK, SEARCH, T> {
398
+ /** Read-write typed table accessor exposed on `MutationCtx.db.<table>` / `ActionCtx.db.<table>`. */
399
+ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM, GEO extends Record<keyof DM, string> = Record<keyof DM, never>> extends TableReaderFacade<DM, REL, RANK, SEARCH, T, GEO> {
400
+ /**
401
+ * Delete a row by id. On a `.softDelete()` table this flips the marker column
402
+ * (and cascades as a soft delete) instead of removing the row; use
403
+ * {@link TableWriterFacade.hardDelete} to force physical removal.
404
+ */
302
405
  delete: (id: Id<string & T>) => Promise<void>;
303
- /** Delete many rows in this table by id in one call; returns the *requested* id count (unknown ids are no-ops). Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
304
- deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
305
- limit?: number;
406
+ /**
407
+ * Delete many rows in this table. Pass an array of ids (requested count is
408
+ * returned; unknown ids are no-ops) or `{ where }` to delete matching rows
409
+ * (actual removed count is returned). Atomic within a mutation.
410
+ */
411
+ deleteMany: {
412
+ (ids: ReadonlyArray<Id<string & T>>, options?: {
413
+ limit?: number;
414
+ }): Promise<{
415
+ deleted: number;
416
+ }>;
417
+ (args: {
418
+ limit?: number;
419
+ where: Partial<DM[T]>;
420
+ }): Promise<{
421
+ deleted: number;
422
+ }>;
423
+ };
424
+ /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
425
+ hardDelete: (id: Id<string & T>) => Promise<void>;
426
+ /**
427
+ * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
428
+ * a UNIQUE-constraint breach resolves to `null` (the row already exists)
429
+ * instead of throwing — the return type widens to `Id | null` on that overload.
430
+ */
431
+ insert: {
432
+ (values: IM[T], options: {
433
+ skipDuplicates: true;
434
+ }): Promise<Id<string & T> | null>;
435
+ (values: IM[T], options?: {
436
+ skipDuplicates?: boolean;
437
+ }): Promise<Id<string & T>>;
438
+ };
439
+ /**
440
+ * Insert many documents into this table in one call, returning the minted ids
441
+ * in input order. With `{ skipDuplicates: true }`, UNIQUE breaches resolve to
442
+ * `null` for that row instead of failing the batch. Atomic within a mutation.
443
+ */
444
+ insertMany: {
445
+ (values: ReadonlyArray<IM[T]>, options: {
446
+ limit?: number;
447
+ skipDuplicates: true;
448
+ }): Promise<(Id<string & T> | null)[]>;
449
+ (values: ReadonlyArray<IM[T]>, options?: {
450
+ limit?: number;
451
+ skipDuplicates?: boolean;
452
+ }): Promise<Id<string & T>[]>;
453
+ };
454
+ patch: (id: Id<string & T>, values: Partial<IM[T]>) => Promise<void>;
455
+ /**
456
+ * Patch many rows in this table. Pass an array of `{ id, values }` or
457
+ * `{ where, values }` to patch matching rows with the same values. Returns
458
+ * the actual patched count. Atomic within a mutation.
459
+ */
460
+ patchMany: {
461
+ (patches: ReadonlyArray<{
462
+ id: Id<string & T>;
463
+ values: Partial<IM[T]>;
464
+ }>, options?: {
465
+ limit?: number;
466
+ }): Promise<{
467
+ patched: number;
468
+ }>;
469
+ (args: {
470
+ limit?: number;
471
+ values: Partial<IM[T]>;
472
+ where: Partial<DM[T]>;
473
+ }): Promise<{
474
+ patched: number;
475
+ }>;
476
+ };
477
+ replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
478
+ /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
479
+ restore: (id: Id<string & T>) => Promise<void>;
480
+ /**
481
+ * Insert when no existing row matches `target`, otherwise patch the match with
482
+ * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
483
+ * tuple) used to look it up. Returns the row id and whether it was `created`.
484
+ * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
485
+ */
486
+ upsert: (args: {
487
+ create: IM[T];
488
+ target: UpsertTargetOf<DM, T>;
489
+ update?: Partial<IM[T]>;
306
490
  }) => Promise<{
307
- deleted: number;
491
+ created: boolean;
492
+ id: Id<string & T>;
308
493
  }>;
309
- insert: (values: IM[T]) => Promise<Id<string & T>>;
310
- /** Insert many documents into this table in one call, returning the minted ids in input order. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
311
- insertMany: (values: ReadonlyArray<IM[T]>, options?: {
312
- limit?: number;
313
- }) => Promise<Id<string & T>[]>;
314
- patch: (id: Id<string & T>, values: Partial<IM[T]>) => Promise<void>;
315
- /** Patch many rows in this table by id in one call. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
316
- patchMany: (patches: ReadonlyArray<{
494
+ /** Sequential {@link TableWriterFacade.upsert} over many rows sharing one `target`; one result per input row, in order. */
495
+ upsertMany: (args: {
496
+ rows: ReadonlyArray<{
497
+ create: IM[T];
498
+ update?: Partial<IM[T]>;
499
+ }>;
500
+ target: UpsertTargetOf<DM, T>;
501
+ }) => Promise<{
502
+ created: boolean;
317
503
  id: Id<string & T>;
318
- values: Partial<IM[T]>;
319
- }>, options?: {
320
- limit?: number;
321
- }) => Promise<void>;
322
- replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
504
+ }[]>;
323
505
  }
324
- /** Per-table read facade `ctx.db.&lt;table>` on a `QueryCtx`. */
325
- type DatabaseReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableReaderFacade<DM, REL, RANK, SEARCH, T> };
326
- /** Per-table read-write facade — `ctx.db.&lt;table>` on a `MutationCtx` / `ActionCtx`. */
327
- type DatabaseWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableWriterFacade<DM, IM, REL, RANK, SEARCH, T> };
328
- export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, Where, WhereOf, WhereOperators, WithArg };
506
+ /** Conflict target for `upsert`/`upsertMany`: one column of table `T`, or a tuple of them. */
507
+ type UpsertTargetOf<DM, T extends keyof DM> = ReadonlyArray<keyof DM[T] & string> | (keyof DM[T] & string);
508
+ /** Per-table read facade — `ctx.db.<table>` on a `QueryCtx`. */
509
+ type DatabaseReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, GEO extends Record<keyof DM, string> = Record<keyof DM, never>> = { readonly [T in keyof DM]: TableReaderFacade<DM, REL, RANK, SEARCH, T, GEO>; };
510
+ /** Per-table read-write facade `ctx.db.<table>` on a `MutationCtx` / `ActionCtx`. */
511
+ type DatabaseWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, GEO extends Record<keyof DM, string> = Record<keyof DM, never>> = { readonly [T in keyof DM]: TableWriterFacade<DM, IM, REL, RANK, SEARCH, T, GEO>; };
512
+ export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GeoBoundingBox, GeoFilterBuilder, GeoPointInput, GeoReader, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, UpsertTargetOf, Where, WhereOf, WhereOperators, WithArg };