@lunora/server 1.0.0-alpha.2 → 1.0.0-alpha.200

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 (92) hide show
  1. package/LICENSE.md +219 -0
  2. package/README.md +57 -1
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/data-model.d.mts +490 -142
  5. package/dist/data-model.d.ts +490 -142
  6. package/dist/data-model.mjs +0 -1
  7. package/dist/drizzle.mjs +1 -1
  8. package/dist/in-process.d.mts +101 -0
  9. package/dist/in-process.d.ts +101 -0
  10. package/dist/in-process.mjs +1 -0
  11. package/dist/index.d.mts +3592 -1378
  12. package/dist/index.d.ts +3592 -1378
  13. package/dist/index.mjs +1 -24
  14. package/dist/otel.d.mts +554 -0
  15. package/dist/otel.d.ts +554 -0
  16. package/dist/otel.mjs +1 -0
  17. package/dist/packem_shared/ACTION_CACHE_DEFAULT_TTL_MS-BFmhdKJG.mjs +1 -0
  18. package/dist/packem_shared/DEFAULT_LIMIT-pT_tV8R0.mjs +1 -0
  19. package/dist/packem_shared/DOCUMENT_HISTORY_REDACTED_FIELDS-BeS7G0yM.mjs +1 -0
  20. package/dist/packem_shared/LunoraEnvError-bkdMu_-N.mjs +4 -0
  21. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-DFpy5J8G.mjs +1 -0
  22. package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
  23. package/dist/packem_shared/apply-output-C5wZ5EAL.mjs +1 -0
  24. package/dist/packem_shared/asBucketStorage-DoGYOjq3.mjs +1 -0
  25. package/dist/packem_shared/assertShapesDeclareReadPolicies-isd_Yupt.mjs +1 -0
  26. package/dist/packem_shared/beginDeferredDeletes-DhwFBbvs.mjs +1 -0
  27. package/dist/packem_shared/beginDeferredSchedules-n_pHHjPE.mjs +1 -0
  28. package/dist/packem_shared/bindOrm-BHq_oUP2.mjs +1 -0
  29. package/dist/packem_shared/buildMaskRegistry-DpWMtalG.mjs +1 -0
  30. package/dist/packem_shared/compose-middleware-BK9FlblN.mjs +1 -0
  31. package/dist/packem_shared/composePluginMiddleware-ZtDIkbvJ.mjs +1 -0
  32. package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
  33. package/dist/packem_shared/createPolicyDsl-BZa6SqLJ.mjs +1 -0
  34. package/dist/packem_shared/createSecrets-D6rLB42U.mjs +1 -0
  35. package/dist/packem_shared/createServices-IJvoqi0e.mjs +1 -0
  36. package/dist/packem_shared/defineAggregateIndex-DF8NiKHr.mjs +1 -0
  37. package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
  38. package/dist/packem_shared/defineIdentityGuard-Bt8N65Zs.mjs +1 -0
  39. package/dist/packem_shared/defineMigration-CXOS0Bvq.mjs +1 -0
  40. package/dist/packem_shared/defineModule-OecXWM3U.mjs +1 -0
  41. package/dist/packem_shared/defineMutator-B4ipDtDR.mjs +1 -0
  42. package/dist/packem_shared/defineShape-CX9Xs8AY.mjs +1 -0
  43. package/dist/packem_shared/defineStorageRule-Dv4nJE0H.mjs +1 -0
  44. package/dist/packem_shared/fnv1a-D3ueNTWB.mjs +1 -0
  45. package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
  46. package/dist/packem_shared/httpAction-CsKNC6op.mjs +5 -0
  47. package/dist/packem_shared/initLunora-B-7Xs3Qq.mjs +1 -0
  48. package/dist/packem_shared/isPerDispatchMiddleware-DDQPatSm.mjs +1 -0
  49. package/dist/packem_shared/mask-BdULkLAu.mjs +1 -0
  50. package/dist/packem_shared/middleware-DbhB1jRk.mjs +1 -0
  51. package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
  52. package/dist/packem_shared/onQueryChange-CatdYnH5.mjs +1 -0
  53. package/dist/packem_shared/onWhisper-CfDbFLQh.mjs +1 -0
  54. package/dist/packem_shared/platformAdmin-kczXQme8.mjs +1 -0
  55. package/dist/packem_shared/plugin-Bm9wIpiI.mjs +1 -0
  56. package/dist/packem_shared/policy-tag-V0lqFzgS.mjs +1 -0
  57. package/dist/packem_shared/protectPublic-Bi5_95aP.mjs +1 -0
  58. package/dist/packem_shared/rls-B3xf3YrN.mjs +1 -0
  59. package/dist/packem_shared/run-middleware-BefYq7Xd.mjs +1 -0
  60. package/dist/packem_shared/serveStorageObject-DNN3r8x3.mjs +1 -0
  61. package/dist/packem_shared/stable-key-B_BlboiY.mjs +1 -0
  62. package/dist/packem_shared/storageRules-CMfQu8yY.mjs +1 -0
  63. package/dist/packem_shared/types.d-B7FLdYUD.d.mts +148 -0
  64. package/dist/packem_shared/types.d-BCtjbIzl.d.ts +148 -0
  65. package/dist/packem_shared/wire-codec-CZ4obM6q.mjs +1 -0
  66. package/dist/rls/testing.d.mts +42 -34
  67. package/dist/rls/testing.d.ts +42 -34
  68. package/dist/rls/testing.mjs +1 -49
  69. package/dist/types.d.mts +2301 -512
  70. package/dist/types.d.ts +2301 -512
  71. package/dist/types.mjs +0 -31
  72. package/package.json +24 -6
  73. package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +0 -187
  74. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  75. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs +0 -114
  76. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  77. package/dist/packem_shared/bindOrm-DCuyr46L.mjs +0 -71
  78. package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +0 -100
  79. package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +0 -29
  80. package/dist/packem_shared/defineAggregateIndex-DxSso0rH.mjs +0 -236
  81. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
  82. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
  83. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
  84. package/dist/packem_shared/initLunora-CATvPsVt.mjs +0 -86
  85. package/dist/packem_shared/mask-Jc84C_hK.mjs +0 -211
  86. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  87. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
  88. package/dist/packem_shared/rls-BDKRbMCA.mjs +0 -551
  89. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
  90. package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
  91. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  92. 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;
@@ -37,59 +37,105 @@ interface WhereOperators<T> {
37
37
  ne?: T;
38
38
  notIn?: T[];
39
39
  }
40
+ /**
41
+ * A value stored as a plain JS object: not a scalar (an `Id<T>` is a branded string, which is an `object` to
42
+ * TypeScript but has no keys worth offering), an array, a date, bytes, a bigint or a function.
43
+ */
44
+ type IsPlainObject<V> = V extends string | number | boolean | bigint | symbol ? false : V extends ReadonlyArray<unknown> ? false : V extends Date | Uint8Array | ArrayBuffer | bigint | ((...arguments_: never[]) => unknown) ? false : V extends object ? true : false;
45
+ /** The dotted paths below an object, to three steps. */
46
+ type SubPaths<V, Depth extends unknown[]> = Depth["length"] extends 3 ? never : [IsPlainObject<V>] extends [true] ? { [K in keyof V & string]-?: K | `${K}.${SubPaths<NonNullable<V[K]>, [...Depth, 0]>}`; }[keyof V & string] : never;
47
+ /** The type at the end of dotted path `P` below `V`. */
48
+ type PathValue<V, P extends string> = P extends `${infer Head}.${infer Rest}` ? Head extends keyof V ? PathValue<NonNullable<V[Head]>, Rest> : never : P extends keyof V ? V[P] : never;
49
+ /**
50
+ * Every dotted path into an object-valued column of `Doc` (`"profile.address.city"`), at most four segments
51
+ * counting the column. A column that is not a plain object contributes none.
52
+ */
53
+ type ColumnPaths<Doc> = { [K in keyof Doc & string]-?: [IsPlainObject<NonNullable<Doc[K]>>] extends [true] ? `${K}.${SubPaths<NonNullable<Doc[K]>, []>}` : never; }[keyof Doc & string];
54
+ /** The type at the end of a {@link ColumnPaths} path. */
55
+ type DocPathValue<Doc, P extends string> = P extends `${infer Column}.${infer Rest}` ? Column extends keyof Doc ? PathValue<NonNullable<Doc[Column]>, Rest> : never : never;
56
+ type ScalarLeaf = boolean | number | string | null | undefined;
57
+ /** The dotted paths into an object column that end at a string, number or boolean. */
58
+ type ScalarPaths<Doc> = { [P in ColumnPaths<Doc>]: [DocPathValue<Doc, P>] extends [ScalarLeaf] ? P : never; }[ColumnPaths<Doc>];
59
+ /**
60
+ * `where` keys that reach into an object column: `{ "profile.plan": "pro" }`. Only paths that end at a string,
61
+ * number or boolean are offered — an object, array, date, bigint or bytes leaf has no single SQL comparison.
62
+ *
63
+ * `unknown` (the identity for `&`) when there are none, never `{}`: intersecting with an empty object type
64
+ * switches off the weak-type check that rejects a `where` key naming nothing.
65
+ */
66
+ type PathWhere<Doc, Paths extends string = ScalarPaths<Doc>> = [Paths] extends [never] ? unknown : { [P in Paths]?: DocPathValue<Doc, P> | WhereOperators<DocPathValue<Doc, P>>; };
67
+ /** `patch` keys that set one value inside an object column, keeping its siblings: `{ "profile.plan": "pro" }`. */
68
+ type PathPatch<Doc, Paths extends string = ColumnPaths<Doc>> = [Paths] extends [never] ? unknown : { [P in Paths]?: DocPathValue<Doc, P>; };
40
69
  /** A typed `where` tree over a document's columns. */
41
- type Where<TDocument> = { [K in keyof TDocument]?: TDocument[K] | WhereOperators<TDocument[K]> } & {
70
+ type Where<TDocument> = PathWhere<TDocument> & { [K in keyof TDocument]?: TDocument[K] | WhereOperators<TDocument[K]>; } & {
42
71
  AND?: Where<TDocument>[];
43
72
  NOT?: Where<TDocument>;
44
73
  OR?: Where<TDocument>[];
45
74
  };
46
- /** One `{ field: "asc" | "desc" }` ordering entry; `orderBy` is an ordered list. */
47
- type OrderBy<TDocument> = Partial<Record<keyof TDocument, "asc" | "desc">>;
75
+ /**
76
+ * One ordering entry; `orderBy` is an ordered list. `{ field: "asc" | "desc" }` sorts NULLs first
77
+ * ascending and last descending. `{ field: { sort: "desc", nulls: "first" } }` places them yourself.
78
+ *
79
+ * A placement other than the default can change the query plan (see Pagination in the docs), so
80
+ * measure a hot read. It has no effect on a column that cannot hold NULL. Keep the same `orderBy` for every page of a
81
+ * paginated read, as a cursor is only valid against the ordering that minted it.
82
+ */
83
+ type OrderBy<TDocument> = Partial<Record<keyof TDocument | ScalarPaths<TDocument>, "asc" | "desc" | {
84
+ nulls: "first" | "last";
85
+ sort: "asc" | "desc";
86
+ }>>;
48
87
  interface QueryArgs<TDocument> {
49
88
  cursor?: null | string;
50
89
  limit?: number;
51
90
  orderBy?: OrderBy<TDocument>[];
91
+ /**
92
+ * Project each returned row down to these columns (plus the system fields
93
+ * `_id`/`_creationTime`, always retained). Trims wire payload for wide rows;
94
+ * relations requested via `with` are still attached. Reactivity is unaffected —
95
+ * the engine still reads the whole row to track dependencies.
96
+ */
97
+ select?: ReadonlyArray<keyof TDocument & string>;
52
98
  where?: Where<TDocument>;
53
99
  }
54
100
  /**
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
- */
101
+ * A to-one relation predicate node. `is` matches rows whose related record
102
+ * satisfies `W`; `isNot` matches rows whose related record fails `W` *or* has
103
+ * no related record at all (a null/dangling FK) — Prisma's semantics.
104
+ */
59
105
  interface OneRelationWhere<W> {
60
106
  is?: W;
61
107
  isNot?: W;
62
108
  }
63
109
  /**
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
- */
110
+ * A to-many relation predicate node. `some` ⇒ at least one related row matches
111
+ * `W`; `none` ⇒ no related row matches (childless parents included); `every` ⇒
112
+ * every *readable* related row matches (vacuously true for childless parents).
113
+ */
68
114
  interface ManyRelationWhere<W> {
69
115
  every?: W;
70
116
  none?: W;
71
117
  some?: W;
72
118
  }
73
119
  /**
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
- */
120
+ * The relation-predicate portion of {@link WhereOf}: each declared relation on
121
+ * `T` contributes a kind-dispatched node — `one` → `{ is?; isNot? }`, `many` →
122
+ * `{ some?; none?; every? }` — whose inner type is the target table's own
123
+ * relation-aware `where` (so multi-hop predicates type-check inside-out).
124
+ */
79
125
  type RelationWhere<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
80
126
  __relationKind: "one";
81
127
  __target: infer Target extends keyof DM;
82
128
  } ? OneRelationWhere<WhereOf<DM, REL, Target>> : REL[T][K] extends {
83
129
  __relationKind: "many";
84
130
  __target: infer Target extends keyof DM;
85
- } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never };
131
+ } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never; };
86
132
  /**
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]> } & {
133
+ * Relation-aware `where` tree — the column predicates of {@link Where} plus
134
+ * Prisma-style relation predicates resolved by the `@lunora/do` pre-resolver.
135
+ * `Where<DM[T]>` stays the column-only structural mirror for back-compat; the
136
+ * table facade threads `REL` through this richer form.
137
+ */
138
+ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = PathWhere<DM[T]> & RelationWhere<DM, REL, T> & { [K in keyof DM[T]]?: DM[T][K] | WhereOperators<DM[T][K]>; } & {
93
139
  AND?: WhereOf<DM, REL, T>[];
94
140
  NOT?: WhereOf<DM, REL, T>;
95
141
  OR?: WhereOf<DM, REL, T>[];
@@ -97,6 +143,12 @@ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = Rel
97
143
  /** {@link QueryArgs} with the relation-aware {@link WhereOf} `where` typing. */
98
144
  interface QueryArgsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
99
145
  cursor?: null | string;
146
+ /**
147
+ * Include soft-deleted rows (`.softDelete()` tables only). Default hides them;
148
+ * `true` returns deleted rows alongside live ones. No effect on a table
149
+ * without `.softDelete()`.
150
+ */
151
+ includeDeleted?: boolean;
100
152
  limit?: number;
101
153
  orderBy?: OrderBy<DM[T]>[];
102
154
  where?: WhereOf<DM, REL, T>;
@@ -110,12 +162,16 @@ interface QueryPage<TDocument> {
110
162
  type NestedWithArgument<WK> = WK extends {
111
163
  with: infer NW;
112
164
  } ? NW : {};
165
+ /** The nested `select` tuple inside a relation's with-value, or `undefined` (no projection). */
166
+ type NestedSelectArgument<WK> = WK extends {
167
+ select: infer S;
168
+ } ? S : undefined;
113
169
  /**
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
- */
170
+ * The `with` argument for table `T`: each relation can be `true` (load with no
171
+ * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`/
172
+ * `select` plus a nested `with`; `one` relations accept `select` + a nested
173
+ * `with`. The reserved `_count` key requests per-relation aggregate counts.
174
+ */
119
175
  type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
120
176
  __relationKind: "many";
121
177
  __target: infer Target extends keyof DM;
@@ -125,54 +181,72 @@ type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [
125
181
  __relationKind: "one";
126
182
  __target: infer Target extends keyof DM;
127
183
  } ? boolean | {
184
+ select?: ReadonlyArray<keyof DM[Target] & string>;
128
185
  with?: WithArg<DM, REL, Target>;
129
- } : never } & {
130
- _count?: { [K in keyof REL[T]]?: true };
186
+ } : never; } & {
187
+ _count?: { [K in keyof REL[T]]?: true; };
131
188
  };
132
- /** Resolve a single relation descriptor + its with-value to the loaded type. */
189
+ /**
190
+ * Resolve a single relation descriptor + its with-value to the loaded type,
191
+ * threading the nested `select` tuple into the projected child shape (the 5th
192
+ * `LoadWith` arg) so `with: { author: { select: ["name"] } }` narrows the loaded
193
+ * `author` to the selected columns + system fields.
194
+ */
133
195
  type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
134
196
  __relationKind: "one";
135
197
  __target: infer Target extends keyof DM;
136
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>> | null : R extends {
198
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>> | null : R extends {
137
199
  __relationKind: "many";
138
200
  __target: infer Target extends keyof DM;
139
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>>[] : never;
201
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>>[] : never;
140
202
  /** 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 };
203
+ 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
204
  /** The `_count` projection of `W`, if any. */
143
205
  type LoadedCount<W> = W extends {
144
206
  _count: infer C;
145
207
  } ? {
146
- _count: { [K in keyof C]: number };
208
+ _count: { [K in keyof C]: number; };
147
209
  } : {};
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>;
210
+ /** System columns a `select` projection always retains, so cursors and by-id reuse keep working. */
211
+ type SelectAlwaysKeep<DM, T extends keyof DM> = ("_creationTime" | "_id") & keyof DM[T];
212
+ /**
213
+ * `DM[T]` narrowed to the columns named by a `select` tuple `S` (plus the system
214
+ * fields). `undefined` (the default — no `select`) keeps the full document.
215
+ */
216
+ 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];
217
+ /**
218
+ * `Doc<T>` narrowed to exactly the relations requested in the with-arg `W` and,
219
+ * when a `select` tuple `S` is supplied, to its projected columns. `S` defaults
220
+ * to `undefined` so the 4-argument form (the codegen-emitted callers) keeps the
221
+ * full document.
222
+ */
223
+ 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
224
  /** Reducer applied by an aggregate (`avg`/`count`/`max`/`min`/`sum`). */
151
225
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
152
226
  /**
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
- */
227
+ * Query-options shape shared by every aggregate reader. The RLS-aware ctx
228
+ * populates `baseWhere` so it composes here without a hard import.
229
+ * `restrictsCounts: true` flips `count()` into a thrown `COUNT_RLS_UNSUPPORTED`
230
+ * `LunoraError` rather than silently undercount.
231
+ */
158
232
  interface RestrictableQueryOptions<TDocument> {
159
233
  baseWhere?: Where<TDocument>;
160
234
  restrictsCounts?: boolean;
161
235
  where?: Where<TDocument>;
162
236
  }
163
237
  /**
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
- */
238
+ * Relation-aware twin of {@link RestrictableQueryOptions}. The `@lunora/do`
239
+ * pre-resolver now resolves relation predicates on the `count`/`aggregate`/
240
+ * `groupBy` paths too (semijoin), so the typed surface threads `REL` through
241
+ * `where`/`baseWhere` to match. `rank`/`rankPage` stay column-only — they use
242
+ * `where` solely to pin a partition and fail closed on a relation predicate.
243
+ */
170
244
  interface RestrictableQueryOptionsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
171
245
  baseWhere?: WhereOf<DM, REL, T>;
172
246
  restrictsCounts?: boolean;
173
247
  where?: WhereOf<DM, REL, T>;
174
248
  }
175
- /** Args for `ctx.db.&lt;table>.aggregate({ op, field?, where? })`. */
249
+ /** Args for `ctx.db.<table>.aggregate({ op, field?, where? })`. */
176
250
  interface TableAggregateOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
177
251
  field?: keyof TDocument & string;
178
252
  op: AggregateOp;
@@ -182,7 +256,7 @@ interface TableAggregateOptionsOf<DM, REL extends Record<keyof DM, object>, T ex
182
256
  field?: keyof DM[T] & string;
183
257
  op: AggregateOp;
184
258
  }
185
- /** Args for `ctx.db.&lt;table>.groupBy({ by, agg?, where? })`. */
259
+ /** Args for `ctx.db.<table>.groupBy({ by, agg?, where? })`. */
186
260
  interface TableGroupByOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
187
261
  agg?: {
188
262
  field?: keyof TDocument & string;
@@ -203,7 +277,7 @@ interface GroupByEntry<TDocument> {
203
277
  key: Partial<TDocument>;
204
278
  value: null | number;
205
279
  }
206
- /** Args for `ctx.db.&lt;table>.rank(name, args)`. `row` is either an id or a row doc. */
280
+ /** Args for `ctx.db.<table>.rank(name, args)`. `row` is either an id or a row doc. */
207
281
  interface TableRankOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
208
282
  row: string | TDocument;
209
283
  }
@@ -212,7 +286,7 @@ interface RankResult {
212
286
  position: number;
213
287
  total: number;
214
288
  }
215
- /** Args for `ctx.db.&lt;table>.rankPage(name, args)`. */
289
+ /** Args for `ctx.db.<table>.rankPage(name, args)`. */
216
290
  interface TableRankPageOptions<TDocument> extends RestrictableQueryOptions<TDocument> {
217
291
  cursor?: null | string;
218
292
  take?: number;
@@ -224,105 +298,379 @@ interface RankPage<TDocument> {
224
298
  page: TDocument[];
225
299
  }
226
300
  /**
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
- */
301
+ * Builder passed to `.withSearchIndex(name, q => …)`. `.search(field, query)`
302
+ * runs the full-text match against the index's searchable field; `.eq(field,
303
+ * value)` narrows by a declared filter field. Field names are constrained to
304
+ * the table's columns.
305
+ */
232
306
  interface SearchFilterBuilder<TDocument> {
233
307
  eq: <F extends keyof TDocument & string>(field: F, value: TDocument[F]) => SearchFilterBuilder<TDocument>;
234
308
  search: (field: keyof TDocument & string, query: string) => SearchFilterBuilder<TDocument>;
235
309
  }
236
310
  /**
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
- */
311
+ * Chainable reader returned by `.withSearchIndex()` — rows come back ordered
312
+ * by relevance. `.paginate()` is intentionally absent (a relevance-ordered
313
+ * search can't keyset-paginate); cap the result set with `.take(n)`.
314
+ */
241
315
  interface SearchReader<TDocument> {
242
316
  collect: () => Promise<TDocument[]>;
317
+ /**
318
+ * Like `.collect()`, but pairs each matched document with the relevance
319
+ * score the FTS engine already computed to produce the descending order —
320
+ * `.collect()` throws it away after sorting by it, this surfaces it
321
+ * instead. Ordered by `score` descending, same order as `.collect()`.
322
+ */
323
+ collectWithScores: () => Promise<{
324
+ document: TDocument;
325
+ score: number;
326
+ }[]>;
327
+ first: () => Promise<TDocument | null>;
328
+ take: (limit: number) => Promise<TDocument[]>;
329
+ unique: () => Promise<TDocument | null>;
330
+ }
331
+ /** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
332
+ interface GeoPointInput {
333
+ lat: number;
334
+ lng: number;
335
+ }
336
+ /** An axis-aligned latitude/longitude bounding box (`sw`/`ne` corners). */
337
+ interface GeoBoundingBox {
338
+ ne: GeoPointInput;
339
+ sw: GeoPointInput;
340
+ }
341
+ /**
342
+ * Builder passed to `.withGeoIndex(name, q => …)`. Call exactly one of
343
+ * `.near(point, radiusMeters)` (proximity, nearest-first) or `.within(box)`
344
+ * (bounding-box).
345
+ */
346
+ interface GeoFilterBuilder {
347
+ near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
348
+ within: (box: GeoBoundingBox) => GeoFilterBuilder;
349
+ }
350
+ /**
351
+ * Chainable reader returned by `.withGeoIndex()`. `.near()` results come back
352
+ * ordered nearest-first; `.within()` results by row creation time. `.paginate()`
353
+ * is intentionally absent — cap the result set with `.take(n)`.
354
+ */
355
+ interface GeoReader<TDocument> {
356
+ collect: () => Promise<TDocument[]>;
357
+ /**
358
+ * Like `.collect()`, but pairs each matched document with the distance
359
+ * (in meters) from `.near()`'s point that the read already computed to
360
+ * produce the nearest-first order. `.within()` box matches have no
361
+ * point-distance metric, so `distanceMeters` is `null` for those rows
362
+ * rather than a misleading `0`.
363
+ */
364
+ collectWithScores: () => Promise<{
365
+ distanceMeters: null | number;
366
+ document: TDocument;
367
+ }[]>;
243
368
  first: () => Promise<TDocument | null>;
244
369
  take: (limit: number) => Promise<TDocument[]>;
245
370
  unique: () => Promise<TDocument | null>;
246
371
  }
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> {
372
+ /** Read-only typed table accessor exposed on `QueryCtx.db.<table>`. */
373
+ 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
374
  /**
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
- */
375
+ * Reduce rows in this table to a scalar (`avg`/`max`/`min`/`sum` — `count`
376
+ * lives on its own method). Routes through a declared `aggregateIndex` when
377
+ * the planner can prove the request is answerable; otherwise scans.
378
+ */
254
379
  aggregate: (options: TableAggregateOptionsOf<DM, REL, T>) => Promise<null | number>;
255
380
  /**
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
- */
381
+ * Count rows. The planner routes `where` keys that match a declared
382
+ * `aggregateIndex.by` set to the indexed counter (no scan); otherwise
383
+ * falls back to a SCAN. Accepts either a bare `where` tree or the broader
384
+ * `RestrictableQueryOptions` shape; the latter is the seam the RLS layer
385
+ * uses to inject `baseWhere` and `restrictsCounts`.
386
+ */
262
387
  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> & {
388
+ /** `true` when at least one row matches `where` (any row when omitted). RLS-filtered exactly like `findFirst`. */
389
+ exists: (where?: WhereOf<DM, REL, T>) => Promise<boolean>;
390
+ /**
391
+ * Describe how this table would run `findMany(args)` without running it: the SQL, its bound
392
+ * parameters, and the database's own plan, one line per step. Use it to check that a filter is
393
+ * served by an index (`SEARCH … USING INDEX by_room`) rather than a scan (`SCAN …`).
394
+ *
395
+ * Honors RLS and soft delete exactly like the read, so the plan is the one your caller would get.
396
+ * Explaining does not subscribe a live query to this table. A relation filter in `where` is resolved
397
+ * first with an ordinary read of the related table, which does subscribe to that table, since the ids
398
+ * it matched are part of `params`. Takes no `with` or `select`: neither changes the statement.
399
+ *
400
+ * `params` carries values your RLS policies injected and, for a relation filter, the ids it matched.
401
+ * Keep the result on the server rather than returning it to a client.
402
+ */
403
+ explain: (args?: QueryArgsOf<DM, REL, T>) => Promise<QueryPlan>;
404
+ findFirst: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
405
+ select?: S;
406
+ with?: W;
407
+ }) => Promise<LoadWith<DM, REL, T, W, S> | null>;
408
+ findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
409
+ select?: S;
264
410
  with?: W;
265
- }) => Promise<LoadWith<DM, REL, T, W> | null>;
266
- findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
411
+ }) => Promise<LoadWith<DM, REL, T, W, S>>;
412
+ findMany: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
413
+ select?: S;
267
414
  with?: W;
268
- }) => Promise<LoadWith<DM, REL, T, W>>;
269
- findMany: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
415
+ }) => Promise<QueryPage<LoadWith<DM, REL, T, W, S>>>;
416
+ /**
417
+ * The one row matching `where`, or `null` when none does — and a `NOT_UNIQUE`
418
+ * error when a SECOND one matches, so a broken uniqueness invariant surfaces at
419
+ * the read rather than as a wrong answer downstream. `findFirst` drops that
420
+ * assertion; this is the ORM spelling of the fluent reader's `.unique()`.
421
+ *
422
+ * `limit`/`cursor` are not accepted rather than silently ignored: paging a read
423
+ * that must match at most one row is the assertion being dropped again.
424
+ */
425
+ findUnique: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: Omit<QueryArgsOf<DM, REL, T>, "cursor" | "limit"> & {
426
+ select?: S;
270
427
  with?: W;
271
- }) => Promise<QueryPage<LoadWith<DM, REL, T, W>>>;
428
+ }) => Promise<LoadWith<DM, REL, T, W, S> | null>;
272
429
  get: (id: Id<string & T>) => Promise<DM[T] | null>;
273
430
  /**
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
- */
431
+ * Group rows by the named keys and apply `agg` per group (defaults to
432
+ * `count`). Answered from the counter table when an aggregate index's
433
+ * `by` matches `options.by` exactly; otherwise scans.
434
+ */
278
435
  groupBy: (options: TableGroupByOptionsOf<DM, REL, T>) => Promise<ReadonlyArray<GroupByEntry<DM[T]>>>;
279
436
  /**
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
- */
437
+ * Return the 1-based position of `options.row` within its partition
438
+ * under the declared rankIndex `indexName`, plus the partition's total
439
+ * row count. `null` when the row isn't in the index. Honors the same
440
+ * `baseWhere` / `restrictsCounts` RLS seam as `count()`.
441
+ */
285
442
  rank: (indexName: RANK[T], options: TableRankOptions<DM[T]>) => Promise<null | RankResult>;
286
443
  /**
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
- */
444
+ * Walk the rank companion in declared sort order — sorted pagination
445
+ * accelerator. `options.where` may pin the partition; `cursor`/`take`
446
+ * follow the Convex-style keyset shape.
447
+ */
291
448
  rankPage: (indexName: RANK[T], options?: TableRankPageOptions<DM[T]>) => Promise<RankPage<DM[T]>>;
292
449
  /**
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
- */
450
+ * Restrict the query to a declared `.geoIndex()` and run a proximity /
451
+ * bounding-box match. `indexName` is constrained to this table's geo indexes
452
+ * (`never` when it declares none). Returns a distance-ordered reader —
453
+ * finish with `.take(n)` / `.collect()`.
454
+ */
455
+ withGeoIndex: (indexName: GEO[T], build: (q: GeoFilterBuilder) => GeoFilterBuilder) => GeoReader<DM[T]>;
456
+ /**
457
+ * Restrict the query to a declared `.searchIndex()` and run a full-text
458
+ * match. `indexName` is constrained to this table's search indexes
459
+ * (`never` when it declares none). Returns a relevance-ordered reader —
460
+ * finish with `.take(n)` / `.collect()`.
461
+ */
298
462
  withSearchIndex: (indexName: SEARCH[T], search: (q: SearchFilterBuilder<DM[T]>) => SearchFilterBuilder<DM[T]>) => SearchReader<DM[T]>;
299
463
  }
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> {
464
+ /** Read-write typed table accessor exposed on `MutationCtx.db.<table>` / `ActionCtx.db.<table>`. */
465
+ 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> {
466
+ /**
467
+ * Delete a row by id. On a `.softDelete()` table this flips the marker column
468
+ * (and cascades as a soft delete) instead of removing the row; use
469
+ * {@link TableWriterFacade.hardDelete} to force physical removal.
470
+ */
302
471
  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;
472
+ /**
473
+ * Delete many rows in this table. Pass an array of ids (requested count is
474
+ * returned; unknown ids are no-ops) or `{ where }` to delete matching rows
475
+ * (actual removed count is returned). Atomic within a mutation.
476
+ */
477
+ deleteMany: {
478
+ (ids: ReadonlyArray<Id<string & T>>, options?: {
479
+ limit?: number;
480
+ }): Promise<{
481
+ deleted: number;
482
+ }>;
483
+ (args: {
484
+ limit?: number;
485
+ where: Partial<DM[T]>;
486
+ }): Promise<{
487
+ deleted: number;
488
+ }>;
489
+ };
490
+ /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
491
+ hardDelete: (id: Id<string & T>) => Promise<void>;
492
+ /**
493
+ * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
494
+ * a UNIQUE-constraint breach resolves to `null` (the row already exists)
495
+ * instead of throwing — the return type widens to `Id | null` on that overload.
496
+ */
497
+ insert: {
498
+ (values: InsertValues<DM, IM, REL, T>, options: {
499
+ skipDuplicates: true;
500
+ }): Promise<Id<string & T> | null>;
501
+ (values: InsertValues<DM, IM, REL, T>, options?: {
502
+ skipDuplicates?: boolean;
503
+ }): Promise<Id<string & T>>;
504
+ };
505
+ /**
506
+ * Insert many documents into this table in one call, returning the minted ids
507
+ * in input order. With `{ skipDuplicates: true }`, UNIQUE breaches resolve to
508
+ * `null` for that row instead of failing the batch. Atomic within a mutation.
509
+ */
510
+ insertMany: {
511
+ (values: ReadonlyArray<IM[T]>, options: {
512
+ limit?: number;
513
+ skipDuplicates: true;
514
+ }): Promise<(Id<string & T> | null)[]>;
515
+ (values: ReadonlyArray<IM[T]>, options?: {
516
+ limit?: number;
517
+ skipDuplicates?: boolean;
518
+ }): Promise<Id<string & T>[]>;
519
+ };
520
+ patch: (id: Id<string & T>, values: PatchValues<IM[T]> & RelationWrites<DM, REL, T>) => Promise<void>;
521
+ /**
522
+ * Patch many rows in this table. Pass an array of `{ id, values }` or
523
+ * `{ where, values }` to patch matching rows with the same values. Returns
524
+ * the actual patched count. Atomic within a mutation.
525
+ */
526
+ patchMany: {
527
+ (patches: ReadonlyArray<{
528
+ id: Id<string & T>;
529
+ values: PatchValues<IM[T]>;
530
+ }>, options?: {
531
+ limit?: number;
532
+ }): Promise<{
533
+ patched: number;
534
+ }>;
535
+ (args: {
536
+ limit?: number;
537
+ values: PatchValues<IM[T]>;
538
+ where: Partial<DM[T]>;
539
+ }): Promise<{
540
+ patched: number;
541
+ }>;
542
+ };
543
+ replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
544
+ /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
545
+ restore: (id: Id<string & T>) => Promise<void>;
546
+ /**
547
+ * Insert when no existing row matches `target`, otherwise patch the match with
548
+ * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
549
+ * tuple) used to look it up. Returns the row id and whether it was `created`.
550
+ * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
551
+ */
552
+ upsert: (args: {
553
+ create: InsertValues<DM, IM, REL, T>;
554
+ target: UpsertTargetOf<DM, T>;
555
+ update?: PatchValues<IM[T]> & RelationWrites<DM, REL, T>;
306
556
  }) => Promise<{
307
- deleted: number;
557
+ created: boolean;
558
+ id: Id<string & T>;
308
559
  }>;
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<{
560
+ /** Sequential {@link TableWriterFacade.upsert} over many rows sharing one `target`; one result per input row, in order. */
561
+ upsertMany: (args: {
562
+ rows: ReadonlyArray<{
563
+ create: InsertValues<DM, IM, REL, T>;
564
+ update?: PatchValues<IM[T]> & RelationWrites<DM, REL, T>;
565
+ }>;
566
+ target: UpsertTargetOf<DM, T>;
567
+ }) => Promise<{
568
+ created: boolean;
317
569
  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>;
570
+ }[]>;
571
+ }
572
+ /** Conflict target for `upsert`/`upsertMany`: one column of table `T`, or a tuple of them. */
573
+ type UpsertTargetOf<DM, T extends keyof DM> = ReadonlyArray<keyof DM[T] & string> | (keyof DM[T] & string);
574
+ /**
575
+ * An atomic update for a number column, applied to the value the row holds when the
576
+ * write runs: `{ increment: 1 }`, `{ decrement: 1 }`, `{ multiply: 2 }`, `{ divide: 2 }`.
577
+ * The column must already hold a finite number; a result that is not finite is rejected.
578
+ */
579
+ type NumberFieldOperator = {
580
+ decrement: number;
581
+ divide?: never;
582
+ increment?: never;
583
+ multiply?: never;
584
+ } | {
585
+ decrement?: never;
586
+ divide: number;
587
+ increment?: never;
588
+ multiply?: never;
589
+ } | {
590
+ decrement?: never;
591
+ divide?: never;
592
+ increment: number;
593
+ multiply?: never;
594
+ } | {
595
+ decrement?: never;
596
+ divide?: never;
597
+ increment?: never;
598
+ multiply: number;
599
+ };
600
+ /** An atomic update for a boolean column: `{ toggle: true }` flips the stored value. */
601
+ interface BooleanFieldOperator {
602
+ toggle: true;
323
603
  }
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 };
604
+ /**
605
+ * The values a `patch` accepts: any column's own type, and for a number or boolean
606
+ * column also an atomic operator ({@link NumberFieldOperator}, {@link BooleanFieldOperator}).
607
+ */
608
+ type PatchValues<Row> = PathPatch<Row> & { [K in keyof Row]?: Row[K] | OperatorFor<NonNullable<Row[K]>>; };
609
+ /**
610
+ * The operator a column of type `T` takes: arithmetic when `T` is exactly `number` (so a literal union
611
+ * such as `1 | 2` is out — the result could leave the set — and so is `number | string`, which the
612
+ * store does not treat as a number column), `{ toggle: true }` when it is exactly `boolean`.
613
+ */
614
+ type OperatorFor<T> = [T] extends [number] ? [number] extends [T] ? NumberFieldOperator : never : [T] extends [boolean] ? [boolean] extends [T] ? BooleanFieldOperator : never : never;
615
+ /** What `explain` returns: the SQL a read compiles to, its bound parameters, and the database's plan for it. */
616
+ interface QueryPlan {
617
+ /** The bound parameters, in order. */
618
+ params: unknown[];
619
+ /** One line per plan step, as the database reports it. */
620
+ plan: string[];
621
+ /** The statement the read compiles to. */
622
+ sql: string;
623
+ }
624
+ /**
625
+ * Nested write for a to-one relation: point the row at `Target` (`connect`), or clear the link (`disconnect`, which
626
+ * needs a foreign key that can be empty). One or the other, not both.
627
+ */
628
+ type ToOneRelationWrite<TargetId> = {
629
+ connect: TargetId;
630
+ disconnect?: never;
631
+ } | {
632
+ connect?: never;
633
+ disconnect: true;
634
+ };
635
+ /**
636
+ * Nested write for a to-many relation: point `Target` rows at this one (`connect`), release them (`disconnect`, which
637
+ * needs a foreign key that can be empty), or replace the whole list (`set`: rows not named are released). `set`
638
+ * cannot be combined with the other two.
639
+ */
640
+ type ToManyRelationWrite<TargetId> = {
641
+ connect?: never;
642
+ disconnect?: never;
643
+ set: ReadonlyArray<TargetId>;
644
+ } | {
645
+ connect?: ReadonlyArray<TargetId>;
646
+ disconnect?: ReadonlyArray<TargetId>;
647
+ set?: never;
648
+ };
649
+ /**
650
+ * Nested relation writes accepted by `insert`, `patch` and `upsert`, keyed by relation name:
651
+ * `{ author: { connect: userId } }`, `{ posts: { set: [a, b] } }`. A table with no relations contributes none
652
+ * (`unknown`, the identity for `&`, never `{}`: an empty object type would switch off the weak-type check).
653
+ */
654
+ type RelationWrites<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = [keyof REL[T]] extends [never] ? unknown : { [K in keyof REL[T]]?: REL[T][K] extends {
655
+ __relationKind: "one";
656
+ __target: infer Target extends keyof DM;
657
+ } ? ToOneRelationWrite<Id<string & Target>> : REL[T][K] extends {
658
+ __relationKind: "many";
659
+ __target: infer Target extends keyof DM;
660
+ } ? ToManyRelationWrite<Id<string & Target>> : never; };
661
+ /** The foreign-key columns of a table's to-one relations — the keys a nested `connect` supplies. */
662
+ type OneRelationFields<RelT> = { [K in keyof RelT]: RelT[K] extends {
663
+ __field: infer Field extends string;
664
+ __relationKind: "one";
665
+ } ? Field : never; }[keyof RelT];
666
+ /**
667
+ * What `insert` accepts: the row as it is, or the row without its to-one foreign keys plus the nested writes that
668
+ * supply them. Leaving a key out with no `connect` to supply it is not caught by the type — the store's own
669
+ * validation rejects the missing field.
670
+ */
671
+ type InsertValues<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, T extends keyof DM> = IM[T] | (Omit<IM[T], OneRelationFields<REL[T]>> & RelationWrites<DM, REL, T>);
672
+ /** Per-table read facade — `ctx.db.<table>` on a `QueryCtx`. */
673
+ 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>; };
674
+ /** Per-table read-write facade — `ctx.db.<table>` on a `MutationCtx` / `ActionCtx`. */
675
+ 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>; };
676
+ export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GeoBoundingBox, GeoFilterBuilder, GeoPointInput, GeoReader, GroupByEntry, Id, InsertValues, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, PatchValues, QueryArgs, QueryArgsOf, QueryPage, QueryPlan, RankPage, RankResult, RelationWrites, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, ToManyRelationWrite, ToOneRelationWrite, UpsertTargetOf, Where, WhereOf, WhereOperators, WithArg };