@rebasepro/types 0.13.0 → 0.13.1-canary.g06dbe5b

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.
@@ -1,8 +1,80 @@
1
+ import type { VectorSearchParams } from "./data_driver";
2
+ import type { ComputedSortField, SearchMatch } from "../types/search";
1
3
  import { Entity, EntityValues } from "../types/entities";
2
4
  import { WhereFilterOp, FieldPath, FilterValues, OrderByTuple } from "../types/filter-operators";
3
5
 
6
+ /**
7
+ * Operator-blind filter value: whatever the column holds, a list of it, or null.
8
+ *
9
+ * @deprecated Superseded by {@link WhereValueFor}, which correlates the value
10
+ * with the operator. Kept exported because it is public API and downstream code
11
+ * annotates with it; every `where()` overload in this file uses `WhereValueFor`.
12
+ */
4
13
  export type WhereValue<T> = T | T[] | null;
5
14
 
15
+ /**
16
+ * The element type of an array column, and the column's own type otherwise.
17
+ *
18
+ * A generated SDK emits an `array` property as `Array<X>` and a to-many
19
+ * relation as `Array<TargetRow>`, so this is what `array-contains` compares
20
+ * against on either.
21
+ */
22
+ export type ElementOf<T> = T extends readonly (infer E)[] ? E : T;
23
+
24
+ /**
25
+ * The `id` of a row-shaped element, and `never` for anything else.
26
+ *
27
+ * A to-many relation is emitted as `Array<TargetRow>`, but the filter compilers
28
+ * compare a relation by **id** — `buildRelationFilterPredicate` in
29
+ * `@rebasepro/server-postgres` unwraps a relation value down to its id — so
30
+ * `where("tags", "array-contains", tagId)` is the call that works, and the
31
+ * element type alone would refuse it.
32
+ */
33
+ export type IdOf<E> = E extends { id: infer I } ? I : never;
34
+
35
+ /**
36
+ * One member of an array column: its element, or — when the element is a row —
37
+ * that row's id, which is what a relation filter is actually compared against.
38
+ */
39
+ export type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;
40
+
41
+ /**
42
+ * The value a given operator takes on a column of type `T`.
43
+ *
44
+ * `WhereValue<T>` was one value type for all sixteen operators, which made
45
+ * `array-contains` uncallable from a generated SDK — it is the one operator
46
+ * whose value is an *element* of the column rather than the column's own type,
47
+ * so on `tags: string[]` it wanted a `string[]` and the documented
48
+ * `.where("tags", "array-contains", "featured")` was a compile error. The
49
+ * spelling that did compile, `["featured"]`, builds `@> ARRAY[$1]` with the
50
+ * whole array bound as the single element and matches nothing: the correct
51
+ * query rejected, the accepted query silently wrong.
52
+ *
53
+ * The branches mirror `buildSingleFilterCondition` in `@rebasepro/server-postgres`:
54
+ *
55
+ * - `array-contains` → one element of the column (or a related row's id).
56
+ * - `in` / `not-in` / `array-contains-any` → a list of elements; a bare element
57
+ * is read as the one-element list, and `null` is a null check.
58
+ * - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern. Always a
59
+ * string, including on numeric and date columns, which the driver casts.
60
+ * - `is-null` / `is-not-null` → nothing; the value is ignored everywhere.
61
+ * - everything else → the column's own type, or `null` for a null comparison.
62
+ *
63
+ * Distributes over `Op`, so a caller holding an unnarrowed `WhereFilterOp`
64
+ * (a dynamic filter UI, say) gets the union of every branch and stays as
65
+ * permissive as it was.
66
+ */
67
+ export type WhereValueFor<Op extends WhereFilterOp, T> =
68
+ Op extends "array-contains"
69
+ ? WhereElementOf<T>
70
+ : Op extends "in" | "not-in" | "array-contains-any"
71
+ ? readonly WhereElementOf<T>[] | WhereElementOf<T> | null
72
+ : Op extends "like" | "ilike" | "not-like" | "not-ilike"
73
+ ? string
74
+ : Op extends "is-null" | "is-not-null"
75
+ ? null | undefined
76
+ : T | null;
77
+
6
78
  export interface LogicalCondition {
7
79
  type: "and" | "or";
8
80
  conditions: (FilterCondition | LogicalCondition)[];
@@ -38,13 +110,24 @@ export interface FilterCondition {
38
110
  *
39
111
  * `limit`/`offset` and `page` describe the same window two ways. If **both
40
112
  * `offset` and `page` are provided, `page` wins** — the backend computes
41
- * `offset = (page - 1) * (limit ?? 20)` and ignores the explicit `offset`.
42
- * Pick one style per query.
113
+ * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
114
+ * explicit `offset`. Pick one style per query.
43
115
  *
44
116
  * @group Data
45
117
  */
46
118
  export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
47
- /** Maximum number of items to return (default: 20). */
119
+ /**
120
+ * Maximum number of items to return.
121
+ *
122
+ * Omit it and the backend applies {@link DEFAULT_LIST_LIMIT}, so a read is
123
+ * never unbounded. Provide it and it must be a whole number between 1 and
124
+ * {@link MAX_LIST_LIMIT}: the backend **rejects** anything else with a 400
125
+ * rather than trimming it to fit, because a page quietly smaller than the
126
+ * one you asked for is indistinguishable from having reached the end of the
127
+ * collection. To read past the ceiling, page with `offset` — or let
128
+ * {@link SDKCollectionClient.iterate} / {@link SDKCollectionClient.findAll}
129
+ * do it for you.
130
+ */
48
131
  limit?: number;
49
132
  /**
50
133
  * Number of items to skip. Ignored when {@link FindParams.page} is also
@@ -53,7 +136,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
53
136
  offset?: number;
54
137
  /**
55
138
  * Page number (1-indexed), alternative to {@link FindParams.offset}.
56
- * When set, overrides `offset` as `(page - 1) * (limit ?? 20)`.
139
+ * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
57
140
  */
58
141
  page?: number;
59
142
  /**
@@ -80,7 +163,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
80
163
  * Sort order as a `[field, direction]` tuple.
81
164
  * @example orderBy: ["created_at", "desc"]
82
165
  */
83
- orderBy?: OrderByTuple<FieldPath<M>>;
166
+ orderBy?: OrderByTuple<FieldPath<M> | ComputedSortField>;
84
167
  /**
85
168
  * Relations to include in the response.
86
169
  *
@@ -90,11 +173,47 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
90
173
  */
91
174
  include?: string[];
92
175
  /**
93
- * Full-text search string. Matched (OR-ed) across the collection's
94
- * searchable columns, then AND-ed with `where`/`logical`. This is the
95
- * value behind the query builder's `.search()` method.
176
+ * Text search string, AND-ed with `where`/`logical`. This is the value
177
+ * behind the query builder's `.search()` method.
178
+ *
179
+ * What it compiles to depends on the collection. By default — matching
180
+ * every collection that has not said otherwise — it is a case-insensitive
181
+ * substring match OR-ed across the collection's top-level `string`
182
+ * properties: it does not reach inside `map` or `array` properties, it does
183
+ * not stem or rank, and it cannot use an index.
184
+ *
185
+ * A Postgres collection that declares a `search` block instead gets a
186
+ * ranked full-text match over exactly the fields it named, and rows come
187
+ * back with a {@link FindParams.orderBy}-able `_score`.
96
188
  */
97
189
  searchString?: string;
190
+
191
+ /**
192
+ * Nearest-neighbour search over a `vector` property.
193
+ *
194
+ * Postgres only, and only for a collection that declares a property of
195
+ * type `vector`. Rows come back ordered by distance, closest first, each
196
+ * carrying a `_distance`. Combines with `where` and `logical`, which are
197
+ * applied as filters before the ordering — so this is "the nearest rows
198
+ * that also match", not "the nearest rows, then filtered".
199
+ *
200
+ * Supplying the query vector is the caller's job: rebase stores and
201
+ * searches embeddings, it does not compute them.
202
+ */
203
+ vectorSearch?: VectorSearchParams;
204
+
205
+ /**
206
+ * Ask each returned row to explain itself: which declared search fields
207
+ * matched, with a highlighted snippet from each. Populates `_matches`.
208
+ *
209
+ * Off by default because it is not free — one `ts_headline` per declared
210
+ * field per returned row, and `ts_headline` re-parses the document rather
211
+ * than reading the index. Fine for a page of results, not for an export.
212
+ *
213
+ * Ignored unless the collection declares a `search` block and the query
214
+ * carries a `searchString`; there is nothing to explain otherwise.
215
+ */
216
+ searchExplain?: boolean;
98
217
  }
99
218
 
100
219
  /**
@@ -116,35 +235,50 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
116
235
 
117
236
 
118
237
  /**
119
- * Fluent query builder for the **admin admin** — resolves to `FindResponse<M>`
238
+ * Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`
120
239
  * (Snapshot-wrapped rows).
121
240
  *
122
241
  * @internal App developers should use {@link SDKQueryBuilderInterface}
123
242
  * (flat rows, returned by `client.data.*` / `context.data.*`). This
124
- * Snapshot-flavored variant backs the admin admin internals only.
243
+ * Snapshot-flavored variant backs the admin panel internals only.
125
244
  *
126
245
  * @group Data
127
246
  */
128
247
  export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
129
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
248
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
130
249
  where(logicalCondition: LogicalCondition): this;
131
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
250
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
132
251
  limit(count: number): this;
133
252
  offset(count: number): this;
134
- search(searchString: string): this;
253
+ search(searchString: string, options?: { explain?: boolean }): this;
254
+
255
+ /**
256
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
257
+ *
258
+ * Postgres only, over a property declared as `type: "vector"`. Each row
259
+ * comes back with a `_distance`. Any `where` on the same query filters
260
+ * before the ordering; distance decides the order.
261
+ *
262
+ * The query embedding is the caller's to produce.
263
+ */
264
+ vectorSearch(
265
+ property: string,
266
+ vector: number[],
267
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
268
+ ): this;
135
269
  include(...relations: string[]): this;
136
270
  find(): Promise<FindResponse<M>>;
137
271
  listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
138
272
  }
139
273
 
140
274
  /**
141
- * A single collection's CRUD accessor for the **admin admin** — every method
275
+ * A single collection's CRUD accessor for the **admin panel** — every method
142
276
  * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
143
277
  *
144
278
  * @internal App developers do **not** use this. The public, symmetric surface
145
279
  * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
146
280
  * in the SDK and `context.data.products` in framework callbacks. This
147
- * Snapshot-flavored accessor backs the admin admin view-model only.
281
+ * Snapshot-flavored accessor backs the admin panel view-model only.
148
282
  *
149
283
  * @group Data
150
284
  */
@@ -181,6 +315,20 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
181
315
  */
182
316
  update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
183
317
 
318
+ /**
319
+ * Update many records in a single transaction.
320
+ *
321
+ * See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.
322
+ */
323
+ updateMany?(updates: { id: string | number; data: Partial<EntityValues<M>> }[]): Promise<Entity<M>[]>;
324
+
325
+ /**
326
+ * Delete many records in a single transaction.
327
+ *
328
+ * See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.
329
+ */
330
+ deleteMany?(ids: (string | number)[]): Promise<void>;
331
+
184
332
  /**
185
333
  * Delete a record by ID.
186
334
  */
@@ -200,16 +348,37 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
200
348
 
201
349
  /**
202
350
  * Count the number of records matching the given filter.
351
+ *
352
+ * Optional on this contract because a data source need not support it, and
353
+ * required on `CollectionClient` — the HTTP implementation always has it.
354
+ * So `client.data.posts.count()` compiles in the browser while the same
355
+ * call through a `context.data` accessor needs `count?.()`, which is the
356
+ * one place the two halves of this API are not interchangeable.
203
357
  */
204
358
  count?(params?: FindParams<M>): Promise<number>;
205
359
 
206
360
  // Fluent Query Builder
207
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilderInterface<M>;
361
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;
208
362
  where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
209
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
363
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
210
364
  limit(count: number): QueryBuilderInterface<M>;
211
365
  offset(count: number): QueryBuilderInterface<M>;
212
- search(searchString: string): QueryBuilderInterface<M>;
366
+ search(searchString: string, options?: { explain?: boolean }): QueryBuilderInterface<M>;
367
+
368
+ /**
369
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
370
+ *
371
+ * Postgres only, over a property declared as `type: "vector"`. Each row
372
+ * comes back with a `_distance`. Any `where` on the same query filters
373
+ * before the ordering; distance decides the order.
374
+ *
375
+ * The query embedding is the caller's to produce.
376
+ */
377
+ vectorSearch(
378
+ property: string,
379
+ vector: number[],
380
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
381
+ ): QueryBuilderInterface<M>;
213
382
  include(...relations: string[]): QueryBuilderInterface<M>;
214
383
  }
215
384
 
@@ -240,12 +409,55 @@ export interface PaginationMeta {
240
409
  * @group Data
241
410
  */
242
411
  export interface FindResult<M extends Record<string, unknown> = Record<string, unknown>> {
243
- /** Array of flat rows matching the query */
244
- data: M[];
412
+ /**
413
+ * Flat rows matching the query, each carrying whatever the query computed
414
+ * for it — see {@link QueryComputedFields}.
415
+ */
416
+ data: (M & QueryComputedFields)[];
245
417
  /** Pagination metadata */
246
418
  meta: PaginationMeta;
247
419
  }
248
420
 
421
+ /**
422
+ * Values a query attaches to a row that are not columns of it.
423
+ *
424
+ * Both are absent unless the query asked for the thing that produces them, so
425
+ * both are optional — and reading one on a query that did not ask returns
426
+ * `undefined` rather than a wrong number.
427
+ *
428
+ * They live here rather than on the row type because a generated row type
429
+ * describes a *table*, and neither of these is in one. Without this, a caller
430
+ * who sorted by relevance could not then read the relevance.
431
+ *
432
+ * A `type` alias, deliberately, not an `interface`. TypeScript grants an
433
+ * implicit index signature to a type alias and withholds it from an interface,
434
+ * so `Row & QueryComputedFields` stops being assignable to
435
+ * `Record<string, unknown>` the moment this becomes an interface. Seven casts
436
+ * in one downstream app broke on exactly that.
437
+ *
438
+ * @group Data
439
+ */
440
+ export type QueryComputedFields = {
441
+ /**
442
+ * Relevance, when the collection declares a {@link SearchConfig} and the
443
+ * query carried a search string. Higher is better; the scale is not
444
+ * comparable between two different search strings.
445
+ */
446
+ _score?: number;
447
+ /**
448
+ * Which declared fields matched, and the text around each hit. Present only
449
+ * when the query asked for it — `.search(term, { explain: true })` — because
450
+ * it costs a `ts_headline` per field per row.
451
+ */
452
+ _matches?: SearchMatch[];
453
+ /**
454
+ * Distance to the query vector, when the query used
455
+ * {@link FindParams.vectorSearch}. Lower is closer, and the rows are
456
+ * already ordered by it.
457
+ */
458
+ _distance?: number;
459
+ };
460
+
249
461
  /**
250
462
  * Which column an iteration seeks on, for keyset ("seek") pagination.
251
463
  *
@@ -338,12 +550,27 @@ export type FindAllParams<M extends Record<string, unknown> = Record<string, unk
338
550
  * @group Data
339
551
  */
340
552
  export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
341
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
553
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
342
554
  where(logicalCondition: LogicalCondition): this;
343
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
555
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
344
556
  limit(count: number): this;
345
557
  offset(count: number): this;
346
- search(searchString: string): this;
558
+ search(searchString: string, options?: { explain?: boolean }): this;
559
+
560
+ /**
561
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
562
+ *
563
+ * Postgres only, over a property declared as `type: "vector"`. Each row
564
+ * comes back with a `_distance`. Any `where` on the same query filters
565
+ * before the ordering; distance decides the order.
566
+ *
567
+ * The query embedding is the caller's to produce.
568
+ */
569
+ vectorSearch(
570
+ property: string,
571
+ vector: number[],
572
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
573
+ ): this;
347
574
  include(...relations: string[]): this;
348
575
  find(): Promise<FindResult<M>>;
349
576
  count(): Promise<number>;
@@ -389,9 +616,20 @@ export interface WriteOptions {
389
616
  * it performs it again. On a table with a server-assigned id that is a
390
617
  * duplicate row, because the id the client chose was never used.
391
618
  *
619
+ * A key names **one** request, not a job. It records the method, the path
620
+ * and the body it was claimed for, so re-sending that exact request replays
621
+ * its answer, while the same key on a different one is refused with
622
+ * `IDEMPOTENCY_KEY_REUSED` (422) rather than silently answered with the
623
+ * first request's result. Pass a fresh key — a uuid — per call; a reusable
624
+ * business id shared by the create and the delete of one import means the
625
+ * second of them never runs.
626
+ *
392
627
  * Set by the offline queue on every replay. Honoured for 24 hours and scoped
393
- * to the authenticated user; a server that cannot store keys ignores it
394
- * rather than refusing the write.
628
+ * to the authenticated user — an unauthenticated caller has no principal to
629
+ * scope it to, so the key is ignored there. A retry sent while the first
630
+ * attempt is still being answered gets `IDEMPOTENCY_KEY_IN_PROGRESS` (409)
631
+ * and should be sent again. A server that cannot store keys ignores the
632
+ * header rather than refusing the write.
395
633
  */
396
634
  idempotencyKey?: string;
397
635
  }
@@ -501,6 +739,12 @@ export interface SDKCollectionClient<
501
739
  * Batches are capped server-side (1000 rows by default) because one batch
502
740
  * holds its locks for the whole transaction — chunk larger jobs.
503
741
  *
742
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
743
+ * A client that never sees the response cannot know whether the batch
744
+ * committed, and without a key the server cannot tell the retry from a
745
+ * second genuine import — so it performs it again, duplicating every row in
746
+ * the batch rather than just one.
747
+ *
504
748
  * @returns The written rows, in the order given.
505
749
  *
506
750
  * @example
@@ -510,7 +754,7 @@ export interface SDKCollectionClient<
510
754
  * }
511
755
  * ```
512
756
  */
513
- createMany(data: I[], options?: { upsert?: boolean }): Promise<M[]>;
757
+ createMany(data: I[], options?: { upsert?: boolean } & WriteOptions): Promise<M[]>;
514
758
 
515
759
  /**
516
760
  * Update an existing record by ID.
@@ -520,6 +764,46 @@ export interface SDKCollectionClient<
520
764
  */
521
765
  update(id: string | number, data: U): Promise<M>;
522
766
 
767
+ /**
768
+ * Update many records in a single request and a single transaction.
769
+ *
770
+ * The counterpart to {@link createMany}, and the reason it exists is the
771
+ * same: one call per row means one HTTP round trip and one transaction per
772
+ * row. Every record still runs the normal pipeline — callbacks, relations,
773
+ * row-level security — and the batch is all-or-nothing, so a rejected
774
+ * record leaves none of them written and the error names the offending
775
+ * index.
776
+ *
777
+ * Each entry is `{ id, data }` rather than a flat row carrying its own key.
778
+ * That is deliberate: on a table keyed on something other than `id` — a
779
+ * `sku`, a composite key — a flat row cannot say whether a column is the
780
+ * address or a value to write. Naming the address separately mirrors
781
+ * single-row `update(id, data)` exactly and leaves nothing to infer.
782
+ *
783
+ * An id that matches no row fails the batch with a 404 rather than being
784
+ * skipped, for the same reason `update()` does: silently updating four of
785
+ * five rows is worse than updating none.
786
+ *
787
+ * Batches share `createMany`'s server-side cap (1000 rows by default),
788
+ * because one batch holds its locks for the whole transaction.
789
+ *
790
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
791
+ * An update replayed in full is naturally idempotent, but one interleaved
792
+ * with another writer's is not — the key is what stops a lost ACK from
793
+ * re-applying a stale batch over newer data.
794
+ *
795
+ * @returns The updated rows, in the order given.
796
+ *
797
+ * @example
798
+ * ```ts
799
+ * await client.data.orders.updateMany([
800
+ * { id: "o-1", data: { status: "shipped" } },
801
+ * { id: "o-2", data: { status: "shipped" } }
802
+ * ]);
803
+ * ```
804
+ */
805
+ updateMany(updates: { id: string | number; data: U }[], options?: WriteOptions): Promise<M[]>;
806
+
523
807
  /**
524
808
  * Delete a record by ID.
525
809
  * @throws {RebaseApiError} with status 404 when the record does not exist.
@@ -527,13 +811,48 @@ export interface SDKCollectionClient<
527
811
  delete(id: string | number): Promise<void>;
528
812
 
529
813
  /**
530
- * Subscribe to a collection for real-time updates.
814
+ * Delete many records in a single request and a single transaction.
815
+ *
816
+ * Takes ids, not a filter. A filter-shaped bulk delete is a different and
817
+ * far more dangerous operation — the failure mode is an omitted or
818
+ * mistyped condition emptying a table, and it cannot be reviewed at the
819
+ * call site the way an explicit list can. Read first, then pass the ids you
820
+ * meant.
821
+ *
822
+ * `beforeDelete` and `afterDelete` fire per row, exactly as they do for
823
+ * single deletes, and returning `false` from `beforeDelete` fails the batch
824
+ * rather than quietly dropping one row from it. All-or-nothing, so an id
825
+ * that matches no row 404s the whole call.
826
+ *
827
+ * Shares `createMany`'s row cap.
828
+ *
829
+ * @example
830
+ * ```ts
831
+ * const stale = await client.data.sessions.findAll({
832
+ * where: { expires_at: ["<", cutoff] }
833
+ * });
834
+ * await client.data.sessions.deleteMany(stale.map(s => s.id as string));
835
+ * ```
531
836
  */
532
- listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
837
+ deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
533
838
 
534
839
  /**
535
- * Subscribe to a single record for real-time updates.
840
+ * The low-level realtime subscription: raw server pushes, nothing else.
841
+ *
842
+ * **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps
843
+ * this one and is what a UI actually wants — it emits from the local
844
+ * database first when offline is enabled, re-emits on local writes and
845
+ * rollbacks, and de-duplicates emissions so a refresh that changes nothing
846
+ * does not call back. `listen` does none of that; it forwards what the
847
+ * socket sends.
848
+ *
849
+ * Optional because it is only present when realtime is enabled. `observe()`
850
+ * is not — it degrades to a single fetch — which is the other reason to
851
+ * reach for it instead.
536
852
  */
853
+ listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
854
+
855
+ /** {@link listen} for a single row. Prefer `observeById()`. */
537
856
  listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
538
857
 
539
858
  /**
@@ -542,17 +861,27 @@ export interface SDKCollectionClient<
542
861
  count?(params?: FindParams<M>): Promise<number>;
543
862
 
544
863
  // Fluent Query Builder
545
- where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): SDKQueryBuilderInterface<M>;
864
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;
546
865
  where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
547
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
866
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
548
867
  limit(count: number): SDKQueryBuilderInterface<M>;
549
868
  offset(count: number): SDKQueryBuilderInterface<M>;
550
- search(searchString: string): SDKQueryBuilderInterface<M>;
869
+ search(searchString: string, options?: { explain?: boolean }): SDKQueryBuilderInterface<M>;
870
+ /**
871
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
872
+ * Postgres only, over a `type: "vector"` property. See
873
+ * {@link SDKQueryBuilderInterface.vectorSearch}.
874
+ */
875
+ vectorSearch(
876
+ property: string,
877
+ vector: number[],
878
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
879
+ ): SDKQueryBuilderInterface<M>;
551
880
  include(...relations: string[]): SDKQueryBuilderInterface<M>;
552
881
  }
553
882
 
554
883
  /**
555
- * The unified data access object for the **admin admin** (Entity-shaped).
884
+ * The unified data access object for the **admin panel** (Entity-shaped).
556
885
  *
557
886
  * Access collections as dynamic properties: `data.products.find(...)`. Each
558
887
  * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
@@ -561,7 +890,7 @@ export interface SDKCollectionClient<
561
890
  *
562
891
  * @internal App developers do **not** use this — they use
563
892
  * {@link RebaseSdkData} (flat rows), which is what the SDK client and backend
564
- * `context.data` expose. This Entity-shaped map backs the admin admin only.
893
+ * `context.data` expose. This Entity-shaped map backs the admin panel only.
565
894
  *
566
895
  * @group Data
567
896
  */
@@ -584,10 +913,15 @@ export type RebaseData<DB = unknown> = {
584
913
  * Dynamic collection accessor.
585
914
  * Access any collection by its slug as a property.
586
915
  *
916
+ * The index signature is `CollectionAccessor` alone, for the reason
917
+ * spelled out on {@link RebaseSdkData}: unioning in the `collection`
918
+ * method's own signature is unnecessary across an intersection, and it
919
+ * costs `data.products.find()` — the access this `@example` documents.
920
+ *
587
921
  * @example
588
922
  * data.products.find({ where: { status: ["==", "published"] } })
589
923
  */
590
- [collectionSlug: string]: CollectionAccessor | ((slug: string) => CollectionAccessor);
924
+ [collectionSlug: string]: CollectionAccessor;
591
925
  }
592
926
  );
593
927
 
@@ -639,6 +973,26 @@ export type InsertOf<T> = T extends { Insert: infer I extends Record<string, unk
639
973
  */
640
974
  export type UpdateOf<T> = T extends { Update: infer U extends Record<string, unknown> } ? U : Partial<RowOf<T>>;
641
975
 
976
+ /**
977
+ * Note on the untyped branch below: its index signature is
978
+ * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
979
+ *
980
+ * The union looks like it is needed so `collection` — a method on this same
981
+ * object — satisfies the index signature. It is not, because `collection` is
982
+ * declared in a *separate* member of the intersection, and TypeScript only
983
+ * requires named properties to be assignable to an index signature declared
984
+ * alongside them. Including the function arm cost the documented accessor:
985
+ *
986
+ * rebase.dataAsAdmin.projects.find()
987
+ * // ^ Property 'find' does not exist on type
988
+ * // 'SDKCollectionClient | ((slug: string) => …)'
989
+ *
990
+ * Every project without a generated `Database` type lands on this branch, so
991
+ * property-style access — the form used by the `@example` below, by the
992
+ * scaffolded function template, and by the 0.13 migration note — did not
993
+ * compile for any of them. Do not restore the arm; use `collection(slug)` if a
994
+ * caller genuinely needs the by-slug function.
995
+ */
642
996
  export type RebaseSdkData<DB = unknown> = {
643
997
  /**
644
998
  * Get a flat collection accessor by slug.
@@ -659,6 +1013,6 @@ export type RebaseSdkData<DB = unknown> = {
659
1013
  * @example
660
1014
  * data.products.find({ where: { status: ["==", "published"] } })
661
1015
  */
662
- [collectionSlug: string]: SDKCollectionClient | ((slug: string) => SDKCollectionClient);
1016
+ [collectionSlug: string]: SDKCollectionClient;
663
1017
  }
664
1018
  );