@lunora/server 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (41) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +50 -0
  3. package/dist/data-model.d.mts +231 -156
  4. package/dist/data-model.d.ts +231 -156
  5. package/dist/index.d.mts +1191 -811
  6. package/dist/index.d.ts +1191 -811
  7. package/dist/index.mjs +20 -15
  8. package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
  9. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
  10. package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs → PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs} +5 -5
  11. package/dist/packem_shared/{bindOrm-Ce57S3N9.mjs → bindOrm-CaY7Wq9Z.mjs} +42 -23
  12. package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +107 -0
  13. package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-z62dttBo.mjs} +11 -8
  14. package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
  15. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
  16. package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs} +108 -8
  17. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
  18. package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
  19. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  20. package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
  21. package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
  22. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  23. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
  24. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-D0Wuki7S.mjs} +36 -7
  25. package/dist/packem_shared/{mask-eCUYOwhd.mjs → mask-BepaW7YN.mjs} +132 -9
  26. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  27. package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
  28. package/dist/packem_shared/{rls-Bi9HiyDC.mjs → rls-BaDQf7MG.mjs} +34 -6
  29. package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
  30. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
  31. package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
  32. package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
  33. package/dist/rls/testing.d.mts +31 -31
  34. package/dist/rls/testing.d.ts +31 -31
  35. package/dist/rls/testing.mjs +1 -1
  36. package/dist/types.d.mts +957 -433
  37. package/dist/types.d.ts +957 -433
  38. package/package.json +6 -5
  39. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  40. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  41. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/LICENSE.md CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
package/README.md CHANGED
@@ -93,8 +93,58 @@ The builder chain is `<builder>.input(validators).<kind>(handler)`, plus `.use(m
93
93
 
94
94
  > **Determinism:** `query` and `mutation` handlers must be deterministic — they may be re-run on OCC retry or subscription re-evaluation. Read the current time from **`ctx.now`** (epoch ms, captured once per execution — also on `ActionCtx`) instead of `Date.now()`; compute randomness and network results in an `action` (`crypto.randomUUID()`, `fetch`) and pass them into the mutation as arguments. The `nondeterministic_query_mutation` advisor flags `Date.now()`/`Math.random()`/`fetch` in query/mutation handlers.
95
95
 
96
+ ### Local-first sync engine
97
+
98
+ `defineShape` declares a **partial replication shape** (a named table view with a server-resolved `where` predicate, AND-composed with RLS) and `defineMutator` declares a **custom mutator** (an authoritative `server` impl plus an optional optimistic `client` twin). Both are discovered by codegen and drive the [local-first sync engine](https://lunora.sh/docs/concepts/local-first) — clients subscribe to shapes over the poke diff protocol and push optimistic writes rebased over the server's authoritative result. The client-side runtime lives in [`@lunora/db`](https://www.npmjs.com/package/@lunora/db).
99
+
96
100
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/packages/server)**.
97
101
 
102
+ ### Caching with Workers Cache
103
+
104
+ Lunora supports Cloudflare Workers Cache for HTTP actions (`httpRoute`). RPC queries and mutations are `POST /_lunora/rpc` and are not cacheable at the edge by design.
105
+
106
+ **Enable Workers Cache** in `wrangler.jsonc`:
107
+
108
+ ```jsonc
109
+ {
110
+ "cache": { "enabled": true },
111
+ }
112
+ ```
113
+
114
+ The dev server and CLI automatically bump `compatibility_date` to the minimum required when cache is enabled — you do not need to set it manually.
115
+
116
+ **Set cache headers declaratively** on an `httpRoute`:
117
+
118
+ ```ts
119
+ import { httpRoute } from "./_generated/server";
120
+
121
+ export const getProduct = httpRoute
122
+ .get("/api/products/:id")
123
+ .params({ id: v.string() })
124
+ .cacheControl("public, max-age=300, stale-while-revalidate=3600")
125
+ .cacheTag("products")
126
+ .handler(async ({ ctx, params }) => {
127
+ return { id: params.id, name: "Widget" };
128
+ });
129
+ ```
130
+
131
+ **Purge cache by tag** from an action handler:
132
+
133
+ ```ts
134
+ import { action } from "./_generated/server";
135
+
136
+ export const refreshProducts = action.action(async ({ ctx }) => {
137
+ if (!ctx.cache) {
138
+ throw new Error("Workers Cache is not enabled in wrangler.jsonc");
139
+ }
140
+
141
+ await ctx.cache.purge({ tags: ["products"] });
142
+ return { ok: true };
143
+ });
144
+ ```
145
+
146
+ The `ctx.cache.purge` API accepts `{ tags?: string[]; purgeEverything?: boolean }`. Only action handlers expose `ctx.cache`; queries and mutations run inside the Durable Object and do not have access to the Worker-level cache binding.
147
+
98
148
  ## Related
99
149
 
100
150
  - [`@lunora/values`](https://www.npmjs.com/package/@lunora/values) — the `v.*` validators re-exported here.
@@ -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>[];
@@ -50,53 +50,53 @@ interface QueryArgs<TDocument> {
50
50
  limit?: number;
51
51
  orderBy?: OrderBy<TDocument>[];
52
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
- */
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
58
  select?: ReadonlyArray<keyof TDocument & string>;
59
59
  where?: Where<TDocument>;
60
60
  }
61
61
  /**
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
- */
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
+ */
66
66
  interface OneRelationWhere<W> {
67
67
  is?: W;
68
68
  isNot?: W;
69
69
  }
70
70
  /**
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
- */
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
+ */
75
75
  interface ManyRelationWhere<W> {
76
76
  every?: W;
77
77
  none?: W;
78
78
  some?: W;
79
79
  }
80
80
  /**
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
- */
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
+ */
86
86
  type RelationWhere<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
87
87
  __relationKind: "one";
88
88
  __target: infer Target extends keyof DM;
89
89
  } ? OneRelationWhere<WhereOf<DM, REL, Target>> : REL[T][K] extends {
90
90
  __relationKind: "many";
91
91
  __target: infer Target extends keyof DM;
92
- } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never };
92
+ } ? ManyRelationWhere<WhereOf<DM, REL, Target>> : never; };
93
93
  /**
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&lt;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]> } & {
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&lt;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]>; } & {
100
100
  AND?: WhereOf<DM, REL, T>[];
101
101
  NOT?: WhereOf<DM, REL, T>;
102
102
  OR?: WhereOf<DM, REL, T>[];
@@ -105,10 +105,10 @@ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = Rel
105
105
  interface QueryArgsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
106
106
  cursor?: null | string;
107
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
- */
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
112
  includeDeleted?: boolean;
113
113
  limit?: number;
114
114
  orderBy?: OrderBy<DM[T]>[];
@@ -128,11 +128,11 @@ type NestedSelectArgument<WK> = WK extends {
128
128
  select: infer S;
129
129
  } ? S : undefined;
130
130
  /**
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
- */
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
+ */
136
136
  type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
137
137
  __relationKind: "many";
138
138
  __target: infer Target extends keyof DM;
@@ -144,15 +144,15 @@ type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [
144
144
  } ? boolean | {
145
145
  select?: ReadonlyArray<keyof DM[Target] & string>;
146
146
  with?: WithArg<DM, REL, Target>;
147
- } : never } & {
148
- _count?: { [K in keyof REL[T]]?: true };
147
+ } : never; } & {
148
+ _count?: { [K in keyof REL[T]]?: true; };
149
149
  };
150
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
- */
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
+ */
156
156
  type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
157
157
  __relationKind: "one";
158
158
  __target: infer Target extends keyof DM;
@@ -161,47 +161,47 @@ type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
161
161
  __target: infer Target extends keyof DM;
162
162
  } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>>[] : never;
163
163
  /** The relation keys of `W` that were actually requested (not `false`/`undefined`). */
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 };
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; };
165
165
  /** The `_count` projection of `W`, if any. */
166
166
  type LoadedCount<W> = W extends {
167
167
  _count: infer C;
168
168
  } ? {
169
- _count: { [K in keyof C]: number };
169
+ _count: { [K in keyof C]: number; };
170
170
  } : {};
171
171
  /** System columns a `select` projection always retains, so cursors and by-id reuse keep working. */
172
172
  type SelectAlwaysKeep<DM, T extends keyof DM> = ("_creationTime" | "_id") & keyof DM[T];
173
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
- */
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
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
178
  /**
179
- * `Doc&lt;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
- */
179
+ * `Doc&lt;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
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>;
185
185
  /** Reducer applied by an aggregate (`avg`/`count`/`max`/`min`/`sum`). */
186
186
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
187
187
  /**
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
- */
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
+ */
193
193
  interface RestrictableQueryOptions<TDocument> {
194
194
  baseWhere?: Where<TDocument>;
195
195
  restrictsCounts?: boolean;
196
196
  where?: Where<TDocument>;
197
197
  }
198
198
  /**
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
- */
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
+ */
205
205
  interface RestrictableQueryOptionsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
206
206
  baseWhere?: WhereOf<DM, REL, T>;
207
207
  restrictsCounts?: boolean;
@@ -259,41 +259,71 @@ interface RankPage<TDocument> {
259
259
  page: TDocument[];
260
260
  }
261
261
  /**
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
- */
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
+ */
267
267
  interface SearchFilterBuilder<TDocument> {
268
268
  eq: <F extends keyof TDocument & string>(field: F, value: TDocument[F]) => SearchFilterBuilder<TDocument>;
269
269
  search: (field: keyof TDocument & string, query: string) => SearchFilterBuilder<TDocument>;
270
270
  }
271
271
  /**
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
- */
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
+ */
276
276
  interface SearchReader<TDocument> {
277
277
  collect: () => Promise<TDocument[]>;
278
278
  first: () => Promise<TDocument | null>;
279
279
  take: (limit: number) => Promise<TDocument[]>;
280
280
  unique: () => Promise<TDocument | null>;
281
281
  }
282
+ /** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
283
+ interface GeoPointInput {
284
+ lat: number;
285
+ lng: number;
286
+ }
287
+ /** An axis-aligned latitude/longitude bounding box (`sw`/`ne` corners). */
288
+ interface GeoBoundingBox {
289
+ ne: GeoPointInput;
290
+ sw: GeoPointInput;
291
+ }
292
+ /**
293
+ * Builder passed to `.withGeoIndex(name, q => …)`. Call exactly one of
294
+ * `.near(point, radiusMeters)` (proximity, nearest-first) or `.within(box)`
295
+ * (bounding-box).
296
+ */
297
+ interface GeoFilterBuilder {
298
+ near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
299
+ within: (box: GeoBoundingBox) => GeoFilterBuilder;
300
+ }
301
+ /**
302
+ * Chainable reader returned by `.withGeoIndex()`. `.near()` results come back
303
+ * ordered nearest-first; `.within()` results by row creation time. `.paginate()`
304
+ * is intentionally absent — cap the result set with `.take(n)`.
305
+ */
306
+ interface GeoReader<TDocument> {
307
+ collect: () => Promise<TDocument[]>;
308
+ first: () => Promise<TDocument | null>;
309
+ take: (limit: number) => Promise<TDocument[]>;
310
+ unique: () => Promise<TDocument | null>;
311
+ }
282
312
  /** Read-only typed table accessor exposed on `QueryCtx.db.&lt;table>`. */
283
- 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> {
313
+ 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>> {
284
314
  /**
285
- * Reduce rows in this table to a scalar (`avg`/`max`/`min`/`sum` — `count`
286
- * lives on its own method). Routes through a declared `aggregateIndex` when
287
- * the planner can prove the request is answerable; otherwise scans.
288
- */
315
+ * Reduce rows in this table to a scalar (`avg`/`max`/`min`/`sum` — `count`
316
+ * lives on its own method). Routes through a declared `aggregateIndex` when
317
+ * the planner can prove the request is answerable; otherwise scans.
318
+ */
289
319
  aggregate: (options: TableAggregateOptionsOf<DM, REL, T>) => Promise<null | number>;
290
320
  /**
291
- * Count rows. The planner routes `where` keys that match a declared
292
- * `aggregateIndex.by` set to the indexed counter (no scan); otherwise
293
- * falls back to a SCAN. Accepts either a bare `where` tree or the broader
294
- * `RestrictableQueryOptions` shape; the latter is the seam the RLS layer
295
- * uses to inject `baseWhere` and `restrictsCounts`.
296
- */
321
+ * Count rows. The planner routes `where` keys that match a declared
322
+ * `aggregateIndex.by` set to the indexed counter (no scan); otherwise
323
+ * falls back to a SCAN. Accepts either a bare `where` tree or the broader
324
+ * `RestrictableQueryOptions` shape; the latter is the seam the RLS layer
325
+ * uses to inject `baseWhere` and `restrictsCounts`.
326
+ */
297
327
  count: (where?: RestrictableQueryOptionsOf<DM, REL, T> | WhereOf<DM, REL, T>) => Promise<number>;
298
328
  /** `true` when at least one row matches `where` (any row when omitted). RLS-filtered exactly like `findFirst`. */
299
329
  exists: (where?: WhereOf<DM, REL, T>) => Promise<boolean>;
@@ -311,53 +341,72 @@ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK exten
311
341
  }) => Promise<QueryPage<LoadWith<DM, REL, T, W, S>>>;
312
342
  get: (id: Id<string & T>) => Promise<DM[T] | null>;
313
343
  /**
314
- * Group rows by the named keys and apply `agg` per group (defaults to
315
- * `count`). Answered from the counter table when an aggregate index's
316
- * `by` matches `options.by` exactly; otherwise scans.
317
- */
344
+ * Group rows by the named keys and apply `agg` per group (defaults to
345
+ * `count`). Answered from the counter table when an aggregate index's
346
+ * `by` matches `options.by` exactly; otherwise scans.
347
+ */
318
348
  groupBy: (options: TableGroupByOptionsOf<DM, REL, T>) => Promise<ReadonlyArray<GroupByEntry<DM[T]>>>;
319
349
  /**
320
- * Return the 1-based position of `options.row` within its partition
321
- * under the declared rankIndex `indexName`, plus the partition's total
322
- * row count. `null` when the row isn't in the index. Honors the same
323
- * `baseWhere` / `restrictsCounts` RLS seam as `count()`.
324
- */
350
+ * Return the 1-based position of `options.row` within its partition
351
+ * under the declared rankIndex `indexName`, plus the partition's total
352
+ * row count. `null` when the row isn't in the index. Honors the same
353
+ * `baseWhere` / `restrictsCounts` RLS seam as `count()`.
354
+ */
325
355
  rank: (indexName: RANK[T], options: TableRankOptions<DM[T]>) => Promise<null | RankResult>;
326
356
  /**
327
- * Walk the rank companion in declared sort order — sorted pagination
328
- * accelerator. `options.where` may pin the partition; `cursor`/`take`
329
- * follow the Convex-style keyset shape.
330
- */
357
+ * Walk the rank companion in declared sort order — sorted pagination
358
+ * accelerator. `options.where` may pin the partition; `cursor`/`take`
359
+ * follow the Convex-style keyset shape.
360
+ */
331
361
  rankPage: (indexName: RANK[T], options?: TableRankPageOptions<DM[T]>) => Promise<RankPage<DM[T]>>;
332
362
  /**
333
- * Restrict the query to a declared `.searchIndex()` and run a full-text
334
- * match. `indexName` is constrained to this table's search indexes
335
- * (`never` when it declares none). Returns a relevance-ordered reader —
336
- * finish with `.take(n)` / `.collect()`.
337
- */
363
+ * Restrict the query to a declared `.geoIndex()` and run a proximity /
364
+ * bounding-box match. `indexName` is constrained to this table's geo indexes
365
+ * (`never` when it declares none). Returns a distance-ordered reader —
366
+ * finish with `.take(n)` / `.collect()`.
367
+ */
368
+ withGeoIndex: (indexName: GEO[T], build: (q: GeoFilterBuilder) => GeoFilterBuilder) => GeoReader<DM[T]>;
369
+ /**
370
+ * Restrict the query to a declared `.searchIndex()` and run a full-text
371
+ * match. `indexName` is constrained to this table's search indexes
372
+ * (`never` when it declares none). Returns a relevance-ordered reader —
373
+ * finish with `.take(n)` / `.collect()`.
374
+ */
338
375
  withSearchIndex: (indexName: SEARCH[T], search: (q: SearchFilterBuilder<DM[T]>) => SearchFilterBuilder<DM[T]>) => SearchReader<DM[T]>;
339
376
  }
340
377
  /** Read-write typed table accessor exposed on `MutationCtx.db.&lt;table>` / `ActionCtx.db.&lt;table>`. */
341
- 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> {
378
+ 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> {
342
379
  /**
343
- * Delete a row by id. On a `.softDelete()` table this flips the marker column
344
- * (and cascades as a soft delete) instead of removing the row; use
345
- * {@link TableWriterFacade.hardDelete} to force physical removal.
346
- */
380
+ * Delete a row by id. On a `.softDelete()` table this flips the marker column
381
+ * (and cascades as a soft delete) instead of removing the row; use
382
+ * {@link TableWriterFacade.hardDelete} to force physical removal.
383
+ */
347
384
  delete: (id: Id<string & T>) => Promise<void>;
348
- /** 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. */
349
- deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
350
- limit?: number;
351
- }) => Promise<{
352
- deleted: number;
353
- }>;
385
+ /**
386
+ * Delete many rows in this table. Pass an array of ids (requested count is
387
+ * returned; unknown ids are no-ops) or `{ where }` to delete matching rows
388
+ * (actual removed count is returned). Atomic within a mutation.
389
+ */
390
+ deleteMany: {
391
+ (ids: ReadonlyArray<Id<string & T>>, options?: {
392
+ limit?: number;
393
+ }): Promise<{
394
+ deleted: number;
395
+ }>;
396
+ (args: {
397
+ limit?: number;
398
+ where: Partial<DM[T]>;
399
+ }): Promise<{
400
+ deleted: number;
401
+ }>;
402
+ };
354
403
  /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
355
404
  hardDelete: (id: Id<string & T>) => Promise<void>;
356
405
  /**
357
- * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
358
- * a UNIQUE-constraint breach resolves to `null` (the row already exists)
359
- * instead of throwing — the return type widens to `Id | null` on that overload.
360
- */
406
+ * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
407
+ * a UNIQUE-constraint breach resolves to `null` (the row already exists)
408
+ * instead of throwing — the return type widens to `Id | null` on that overload.
409
+ */
361
410
  insert: {
362
411
  (values: IM[T], options: {
363
412
  skipDuplicates: true;
@@ -366,27 +415,53 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
366
415
  skipDuplicates?: boolean;
367
416
  }): Promise<Id<string & T>>;
368
417
  };
369
- /** 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. */
370
- insertMany: (values: ReadonlyArray<IM[T]>, options?: {
371
- limit?: number;
372
- }) => Promise<Id<string & T>[]>;
418
+ /**
419
+ * Insert many documents into this table in one call, returning the minted ids
420
+ * in input order. With `{ skipDuplicates: true }`, UNIQUE breaches resolve to
421
+ * `null` for that row instead of failing the batch. Atomic within a mutation.
422
+ */
423
+ insertMany: {
424
+ (values: ReadonlyArray<IM[T]>, options: {
425
+ limit?: number;
426
+ skipDuplicates: true;
427
+ }): Promise<(Id<string & T> | null)[]>;
428
+ (values: ReadonlyArray<IM[T]>, options?: {
429
+ limit?: number;
430
+ skipDuplicates?: boolean;
431
+ }): Promise<Id<string & T>[]>;
432
+ };
373
433
  patch: (id: Id<string & T>, values: Partial<IM[T]>) => Promise<void>;
374
- /** 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. */
375
- patchMany: (patches: ReadonlyArray<{
376
- id: Id<string & T>;
377
- values: Partial<IM[T]>;
378
- }>, options?: {
379
- limit?: number;
380
- }) => Promise<void>;
434
+ /**
435
+ * Patch many rows in this table. Pass an array of `{ id, values }` or
436
+ * `{ where, values }` to patch matching rows with the same values. Returns
437
+ * the actual patched count. Atomic within a mutation.
438
+ */
439
+ patchMany: {
440
+ (patches: ReadonlyArray<{
441
+ id: Id<string & T>;
442
+ values: Partial<IM[T]>;
443
+ }>, options?: {
444
+ limit?: number;
445
+ }): Promise<{
446
+ patched: number;
447
+ }>;
448
+ (args: {
449
+ limit?: number;
450
+ values: Partial<IM[T]>;
451
+ where: Partial<DM[T]>;
452
+ }): Promise<{
453
+ patched: number;
454
+ }>;
455
+ };
381
456
  replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
382
457
  /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
383
458
  restore: (id: Id<string & T>) => Promise<void>;
384
459
  /**
385
- * Insert when no existing row matches `target`, otherwise patch the match with
386
- * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
387
- * tuple) used to look it up. Returns the row id and whether it was `created`.
388
- * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
389
- */
460
+ * Insert when no existing row matches `target`, otherwise patch the match with
461
+ * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
462
+ * tuple) used to look it up. Returns the row id and whether it was `created`.
463
+ * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
464
+ */
390
465
  upsert: (args: {
391
466
  create: IM[T];
392
467
  target: UpsertTargetOf<DM, T>;
@@ -410,7 +485,7 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
410
485
  /** Conflict target for `upsert`/`upsertMany`: one column of table `T`, or a tuple of them. */
411
486
  type UpsertTargetOf<DM, T extends keyof DM> = ReadonlyArray<keyof DM[T] & string> | (keyof DM[T] & string);
412
487
  /** Per-table read facade — `ctx.db.&lt;table>` on a `QueryCtx`. */
413
- 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> };
488
+ 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>; };
414
489
  /** Per-table read-write facade — `ctx.db.&lt;table>` on a `MutationCtx` / `ActionCtx`. */
415
- 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> };
416
- 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, UpsertTargetOf, Where, WhereOf, WhereOperators, WithArg };
490
+ 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>; };
491
+ 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 };