@rebasepro/types 0.13.0 → 0.13.1-canary.g249daa1

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,3 +1,5 @@
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
 
@@ -38,13 +40,19 @@ export interface FilterCondition {
38
40
  *
39
41
  * `limit`/`offset` and `page` describe the same window two ways. If **both
40
42
  * `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.
43
+ * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
44
+ * explicit `offset`. Pick one style per query.
43
45
  *
44
46
  * @group Data
45
47
  */
46
48
  export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
47
- /** Maximum number of items to return (default: 20). */
49
+ /**
50
+ * Maximum number of items to return.
51
+ *
52
+ * Defaults to {@link DEFAULT_LIST_LIMIT}, and is clamped to
53
+ * {@link MAX_LIST_LIMIT}. Both bounds are applied by the backend, so a
54
+ * read is never unbounded whether or not a limit was asked for.
55
+ */
48
56
  limit?: number;
49
57
  /**
50
58
  * Number of items to skip. Ignored when {@link FindParams.page} is also
@@ -53,7 +61,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
53
61
  offset?: number;
54
62
  /**
55
63
  * Page number (1-indexed), alternative to {@link FindParams.offset}.
56
- * When set, overrides `offset` as `(page - 1) * (limit ?? 20)`.
64
+ * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
57
65
  */
58
66
  page?: number;
59
67
  /**
@@ -80,7 +88,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
80
88
  * Sort order as a `[field, direction]` tuple.
81
89
  * @example orderBy: ["created_at", "desc"]
82
90
  */
83
- orderBy?: OrderByTuple<FieldPath<M>>;
91
+ orderBy?: OrderByTuple<FieldPath<M> | ComputedSortField>;
84
92
  /**
85
93
  * Relations to include in the response.
86
94
  *
@@ -90,11 +98,47 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
90
98
  */
91
99
  include?: string[];
92
100
  /**
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.
101
+ * Text search string, AND-ed with `where`/`logical`. This is the value
102
+ * behind the query builder's `.search()` method.
103
+ *
104
+ * What it compiles to depends on the collection. By default — matching
105
+ * every collection that has not said otherwise — it is a case-insensitive
106
+ * substring match OR-ed across the collection's top-level `string`
107
+ * properties: it does not reach inside `map` or `array` properties, it does
108
+ * not stem or rank, and it cannot use an index.
109
+ *
110
+ * A Postgres collection that declares a `search` block instead gets a
111
+ * ranked full-text match over exactly the fields it named, and rows come
112
+ * back with a {@link FindParams.orderBy}-able `_score`.
96
113
  */
97
114
  searchString?: string;
115
+
116
+ /**
117
+ * Nearest-neighbour search over a `vector` property.
118
+ *
119
+ * Postgres only, and only for a collection that declares a property of
120
+ * type `vector`. Rows come back ordered by distance, closest first, each
121
+ * carrying a `_distance`. Combines with `where` and `logical`, which are
122
+ * applied as filters before the ordering — so this is "the nearest rows
123
+ * that also match", not "the nearest rows, then filtered".
124
+ *
125
+ * Supplying the query vector is the caller's job: rebase stores and
126
+ * searches embeddings, it does not compute them.
127
+ */
128
+ vectorSearch?: VectorSearchParams;
129
+
130
+ /**
131
+ * Ask each returned row to explain itself: which declared search fields
132
+ * matched, with a highlighted snippet from each. Populates `_matches`.
133
+ *
134
+ * Off by default because it is not free — one `ts_headline` per declared
135
+ * field per returned row, and `ts_headline` re-parses the document rather
136
+ * than reading the index. Fine for a page of results, not for an export.
137
+ *
138
+ * Ignored unless the collection declares a `search` block and the query
139
+ * carries a `searchString`; there is nothing to explain otherwise.
140
+ */
141
+ searchExplain?: boolean;
98
142
  }
99
143
 
100
144
  /**
@@ -116,35 +160,50 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
116
160
 
117
161
 
118
162
  /**
119
- * Fluent query builder for the **admin admin** — resolves to `FindResponse<M>`
163
+ * Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`
120
164
  * (Snapshot-wrapped rows).
121
165
  *
122
166
  * @internal App developers should use {@link SDKQueryBuilderInterface}
123
167
  * (flat rows, returned by `client.data.*` / `context.data.*`). This
124
- * Snapshot-flavored variant backs the admin admin internals only.
168
+ * Snapshot-flavored variant backs the admin panel internals only.
125
169
  *
126
170
  * @group Data
127
171
  */
128
172
  export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
129
173
  where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
130
174
  where(logicalCondition: LogicalCondition): this;
131
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
175
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
132
176
  limit(count: number): this;
133
177
  offset(count: number): this;
134
- search(searchString: string): this;
178
+ search(searchString: string, options?: { explain?: boolean }): this;
179
+
180
+ /**
181
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
182
+ *
183
+ * Postgres only, over a property declared as `type: "vector"`. Each row
184
+ * comes back with a `_distance`. Any `where` on the same query filters
185
+ * before the ordering; distance decides the order.
186
+ *
187
+ * The query embedding is the caller's to produce.
188
+ */
189
+ vectorSearch(
190
+ property: string,
191
+ vector: number[],
192
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
193
+ ): this;
135
194
  include(...relations: string[]): this;
136
195
  find(): Promise<FindResponse<M>>;
137
196
  listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
138
197
  }
139
198
 
140
199
  /**
141
- * A single collection's CRUD accessor for the **admin admin** — every method
200
+ * A single collection's CRUD accessor for the **admin panel** — every method
142
201
  * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
143
202
  *
144
203
  * @internal App developers do **not** use this. The public, symmetric surface
145
204
  * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
146
205
  * in the SDK and `context.data.products` in framework callbacks. This
147
- * Snapshot-flavored accessor backs the admin admin view-model only.
206
+ * Snapshot-flavored accessor backs the admin panel view-model only.
148
207
  *
149
208
  * @group Data
150
209
  */
@@ -181,6 +240,20 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
181
240
  */
182
241
  update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
183
242
 
243
+ /**
244
+ * Update many records in a single transaction.
245
+ *
246
+ * See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.
247
+ */
248
+ updateMany?(updates: { id: string | number; data: Partial<EntityValues<M>> }[]): Promise<Entity<M>[]>;
249
+
250
+ /**
251
+ * Delete many records in a single transaction.
252
+ *
253
+ * See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.
254
+ */
255
+ deleteMany?(ids: (string | number)[]): Promise<void>;
256
+
184
257
  /**
185
258
  * Delete a record by ID.
186
259
  */
@@ -200,16 +273,37 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
200
273
 
201
274
  /**
202
275
  * Count the number of records matching the given filter.
276
+ *
277
+ * Optional on this contract because a data source need not support it, and
278
+ * required on `CollectionClient` — the HTTP implementation always has it.
279
+ * So `client.data.posts.count()` compiles in the browser while the same
280
+ * call through a `context.data` accessor needs `count?.()`, which is the
281
+ * one place the two halves of this API are not interchangeable.
203
282
  */
204
283
  count?(params?: FindParams<M>): Promise<number>;
205
284
 
206
285
  // Fluent Query Builder
207
286
  where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilderInterface<M>;
208
287
  where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
209
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
288
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
210
289
  limit(count: number): QueryBuilderInterface<M>;
211
290
  offset(count: number): QueryBuilderInterface<M>;
212
- search(searchString: string): QueryBuilderInterface<M>;
291
+ search(searchString: string, options?: { explain?: boolean }): QueryBuilderInterface<M>;
292
+
293
+ /**
294
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
295
+ *
296
+ * Postgres only, over a property declared as `type: "vector"`. Each row
297
+ * comes back with a `_distance`. Any `where` on the same query filters
298
+ * before the ordering; distance decides the order.
299
+ *
300
+ * The query embedding is the caller's to produce.
301
+ */
302
+ vectorSearch(
303
+ property: string,
304
+ vector: number[],
305
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
306
+ ): QueryBuilderInterface<M>;
213
307
  include(...relations: string[]): QueryBuilderInterface<M>;
214
308
  }
215
309
 
@@ -240,12 +334,55 @@ export interface PaginationMeta {
240
334
  * @group Data
241
335
  */
242
336
  export interface FindResult<M extends Record<string, unknown> = Record<string, unknown>> {
243
- /** Array of flat rows matching the query */
244
- data: M[];
337
+ /**
338
+ * Flat rows matching the query, each carrying whatever the query computed
339
+ * for it — see {@link QueryComputedFields}.
340
+ */
341
+ data: (M & QueryComputedFields)[];
245
342
  /** Pagination metadata */
246
343
  meta: PaginationMeta;
247
344
  }
248
345
 
346
+ /**
347
+ * Values a query attaches to a row that are not columns of it.
348
+ *
349
+ * Both are absent unless the query asked for the thing that produces them, so
350
+ * both are optional — and reading one on a query that did not ask returns
351
+ * `undefined` rather than a wrong number.
352
+ *
353
+ * They live here rather than on the row type because a generated row type
354
+ * describes a *table*, and neither of these is in one. Without this, a caller
355
+ * who sorted by relevance could not then read the relevance.
356
+ *
357
+ * A `type` alias, deliberately, not an `interface`. TypeScript grants an
358
+ * implicit index signature to a type alias and withholds it from an interface,
359
+ * so `Row & QueryComputedFields` stops being assignable to
360
+ * `Record<string, unknown>` the moment this becomes an interface. Seven casts
361
+ * in one downstream app broke on exactly that.
362
+ *
363
+ * @group Data
364
+ */
365
+ export type QueryComputedFields = {
366
+ /**
367
+ * Relevance, when the collection declares a {@link SearchConfig} and the
368
+ * query carried a search string. Higher is better; the scale is not
369
+ * comparable between two different search strings.
370
+ */
371
+ _score?: number;
372
+ /**
373
+ * Which declared fields matched, and the text around each hit. Present only
374
+ * when the query asked for it — `.search(term, { explain: true })` — because
375
+ * it costs a `ts_headline` per field per row.
376
+ */
377
+ _matches?: SearchMatch[];
378
+ /**
379
+ * Distance to the query vector, when the query used
380
+ * {@link FindParams.vectorSearch}. Lower is closer, and the rows are
381
+ * already ordered by it.
382
+ */
383
+ _distance?: number;
384
+ };
385
+
249
386
  /**
250
387
  * Which column an iteration seeks on, for keyset ("seek") pagination.
251
388
  *
@@ -340,10 +477,25 @@ export type FindAllParams<M extends Record<string, unknown> = Record<string, unk
340
477
  export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
341
478
  where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): this;
342
479
  where(logicalCondition: LogicalCondition): this;
343
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): this;
480
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
344
481
  limit(count: number): this;
345
482
  offset(count: number): this;
346
- search(searchString: string): this;
483
+ search(searchString: string, options?: { explain?: boolean }): this;
484
+
485
+ /**
486
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
487
+ *
488
+ * Postgres only, over a property declared as `type: "vector"`. Each row
489
+ * comes back with a `_distance`. Any `where` on the same query filters
490
+ * before the ordering; distance decides the order.
491
+ *
492
+ * The query embedding is the caller's to produce.
493
+ */
494
+ vectorSearch(
495
+ property: string,
496
+ vector: number[],
497
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
498
+ ): this;
347
499
  include(...relations: string[]): this;
348
500
  find(): Promise<FindResult<M>>;
349
501
  count(): Promise<number>;
@@ -501,6 +653,12 @@ export interface SDKCollectionClient<
501
653
  * Batches are capped server-side (1000 rows by default) because one batch
502
654
  * holds its locks for the whole transaction — chunk larger jobs.
503
655
  *
656
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
657
+ * A client that never sees the response cannot know whether the batch
658
+ * committed, and without a key the server cannot tell the retry from a
659
+ * second genuine import — so it performs it again, duplicating every row in
660
+ * the batch rather than just one.
661
+ *
504
662
  * @returns The written rows, in the order given.
505
663
  *
506
664
  * @example
@@ -510,7 +668,7 @@ export interface SDKCollectionClient<
510
668
  * }
511
669
  * ```
512
670
  */
513
- createMany(data: I[], options?: { upsert?: boolean }): Promise<M[]>;
671
+ createMany(data: I[], options?: { upsert?: boolean } & WriteOptions): Promise<M[]>;
514
672
 
515
673
  /**
516
674
  * Update an existing record by ID.
@@ -520,6 +678,46 @@ export interface SDKCollectionClient<
520
678
  */
521
679
  update(id: string | number, data: U): Promise<M>;
522
680
 
681
+ /**
682
+ * Update many records in a single request and a single transaction.
683
+ *
684
+ * The counterpart to {@link createMany}, and the reason it exists is the
685
+ * same: one call per row means one HTTP round trip and one transaction per
686
+ * row. Every record still runs the normal pipeline — callbacks, relations,
687
+ * row-level security — and the batch is all-or-nothing, so a rejected
688
+ * record leaves none of them written and the error names the offending
689
+ * index.
690
+ *
691
+ * Each entry is `{ id, data }` rather than a flat row carrying its own key.
692
+ * That is deliberate: on a table keyed on something other than `id` — a
693
+ * `sku`, a composite key — a flat row cannot say whether a column is the
694
+ * address or a value to write. Naming the address separately mirrors
695
+ * single-row `update(id, data)` exactly and leaves nothing to infer.
696
+ *
697
+ * An id that matches no row fails the batch with a 404 rather than being
698
+ * skipped, for the same reason `update()` does: silently updating four of
699
+ * five rows is worse than updating none.
700
+ *
701
+ * Batches share `createMany`'s server-side cap (1000 rows by default),
702
+ * because one batch holds its locks for the whole transaction.
703
+ *
704
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
705
+ * An update replayed in full is naturally idempotent, but one interleaved
706
+ * with another writer's is not — the key is what stops a lost ACK from
707
+ * re-applying a stale batch over newer data.
708
+ *
709
+ * @returns The updated rows, in the order given.
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * await client.data.orders.updateMany([
714
+ * { id: "o-1", data: { status: "shipped" } },
715
+ * { id: "o-2", data: { status: "shipped" } }
716
+ * ]);
717
+ * ```
718
+ */
719
+ updateMany(updates: { id: string | number; data: U }[], options?: WriteOptions): Promise<M[]>;
720
+
523
721
  /**
524
722
  * Delete a record by ID.
525
723
  * @throws {RebaseApiError} with status 404 when the record does not exist.
@@ -527,13 +725,48 @@ export interface SDKCollectionClient<
527
725
  delete(id: string | number): Promise<void>;
528
726
 
529
727
  /**
530
- * Subscribe to a collection for real-time updates.
728
+ * Delete many records in a single request and a single transaction.
729
+ *
730
+ * Takes ids, not a filter. A filter-shaped bulk delete is a different and
731
+ * far more dangerous operation — the failure mode is an omitted or
732
+ * mistyped condition emptying a table, and it cannot be reviewed at the
733
+ * call site the way an explicit list can. Read first, then pass the ids you
734
+ * meant.
735
+ *
736
+ * `beforeDelete` and `afterDelete` fire per row, exactly as they do for
737
+ * single deletes, and returning `false` from `beforeDelete` fails the batch
738
+ * rather than quietly dropping one row from it. All-or-nothing, so an id
739
+ * that matches no row 404s the whole call.
740
+ *
741
+ * Shares `createMany`'s row cap.
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * const stale = await client.data.sessions.findAll({
746
+ * where: { expires_at: ["<", cutoff] }
747
+ * });
748
+ * await client.data.sessions.deleteMany(stale.map(s => s.id as string));
749
+ * ```
531
750
  */
532
- listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
751
+ deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
533
752
 
534
753
  /**
535
- * Subscribe to a single record for real-time updates.
754
+ * The low-level realtime subscription: raw server pushes, nothing else.
755
+ *
756
+ * **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps
757
+ * this one and is what a UI actually wants — it emits from the local
758
+ * database first when offline is enabled, re-emits on local writes and
759
+ * rollbacks, and de-duplicates emissions so a refresh that changes nothing
760
+ * does not call back. `listen` does none of that; it forwards what the
761
+ * socket sends.
762
+ *
763
+ * Optional because it is only present when realtime is enabled. `observe()`
764
+ * is not — it degrades to a single fetch — which is the other reason to
765
+ * reach for it instead.
536
766
  */
767
+ listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
768
+
769
+ /** {@link listen} for a single row. Prefer `observeById()`. */
537
770
  listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
538
771
 
539
772
  /**
@@ -544,15 +777,25 @@ export interface SDKCollectionClient<
544
777
  // Fluent Query Builder
545
778
  where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): SDKQueryBuilderInterface<M>;
546
779
  where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
547
- orderBy(column: keyof M & string, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
780
+ orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
548
781
  limit(count: number): SDKQueryBuilderInterface<M>;
549
782
  offset(count: number): SDKQueryBuilderInterface<M>;
550
- search(searchString: string): SDKQueryBuilderInterface<M>;
783
+ search(searchString: string, options?: { explain?: boolean }): SDKQueryBuilderInterface<M>;
784
+ /**
785
+ * Order rows by nearest-neighbour distance to `vector`, closest first.
786
+ * Postgres only, over a `type: "vector"` property. See
787
+ * {@link SDKQueryBuilderInterface.vectorSearch}.
788
+ */
789
+ vectorSearch(
790
+ property: string,
791
+ vector: number[],
792
+ options?: { distance?: "cosine" | "l2" | "inner_product"; threshold?: number }
793
+ ): SDKQueryBuilderInterface<M>;
551
794
  include(...relations: string[]): SDKQueryBuilderInterface<M>;
552
795
  }
553
796
 
554
797
  /**
555
- * The unified data access object for the **admin admin** (Entity-shaped).
798
+ * The unified data access object for the **admin panel** (Entity-shaped).
556
799
  *
557
800
  * Access collections as dynamic properties: `data.products.find(...)`. Each
558
801
  * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
@@ -561,7 +804,7 @@ export interface SDKCollectionClient<
561
804
  *
562
805
  * @internal App developers do **not** use this — they use
563
806
  * {@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.
807
+ * `context.data` expose. This Entity-shaped map backs the admin panel only.
565
808
  *
566
809
  * @group Data
567
810
  */
@@ -584,10 +827,15 @@ export type RebaseData<DB = unknown> = {
584
827
  * Dynamic collection accessor.
585
828
  * Access any collection by its slug as a property.
586
829
  *
830
+ * The index signature is `CollectionAccessor` alone, for the reason
831
+ * spelled out on {@link RebaseSdkData}: unioning in the `collection`
832
+ * method's own signature is unnecessary across an intersection, and it
833
+ * costs `data.products.find()` — the access this `@example` documents.
834
+ *
587
835
  * @example
588
836
  * data.products.find({ where: { status: ["==", "published"] } })
589
837
  */
590
- [collectionSlug: string]: CollectionAccessor | ((slug: string) => CollectionAccessor);
838
+ [collectionSlug: string]: CollectionAccessor;
591
839
  }
592
840
  );
593
841
 
@@ -639,6 +887,26 @@ export type InsertOf<T> = T extends { Insert: infer I extends Record<string, unk
639
887
  */
640
888
  export type UpdateOf<T> = T extends { Update: infer U extends Record<string, unknown> } ? U : Partial<RowOf<T>>;
641
889
 
890
+ /**
891
+ * Note on the untyped branch below: its index signature is
892
+ * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
893
+ *
894
+ * The union looks like it is needed so `collection` — a method on this same
895
+ * object — satisfies the index signature. It is not, because `collection` is
896
+ * declared in a *separate* member of the intersection, and TypeScript only
897
+ * requires named properties to be assignable to an index signature declared
898
+ * alongside them. Including the function arm cost the documented accessor:
899
+ *
900
+ * rebase.dataAsAdmin.projects.find()
901
+ * // ^ Property 'find' does not exist on type
902
+ * // 'SDKCollectionClient | ((slug: string) => …)'
903
+ *
904
+ * Every project without a generated `Database` type lands on this branch, so
905
+ * property-style access — the form used by the `@example` below, by the
906
+ * scaffolded function template, and by the 0.13 migration note — did not
907
+ * compile for any of them. Do not restore the arm; use `collection(slug)` if a
908
+ * caller genuinely needs the by-slug function.
909
+ */
642
910
  export type RebaseSdkData<DB = unknown> = {
643
911
  /**
644
912
  * Get a flat collection accessor by slug.
@@ -659,6 +927,6 @@ export type RebaseSdkData<DB = unknown> = {
659
927
  * @example
660
928
  * data.products.find({ where: { status: ["==", "published"] } })
661
929
  */
662
- [collectionSlug: string]: SDKCollectionClient | ((slug: string) => SDKCollectionClient);
930
+ [collectionSlug: string]: SDKCollectionClient;
663
931
  }
664
932
  );
@@ -117,6 +117,8 @@ export interface FetchCollectionProps<M extends Record<string, unknown> = Record
117
117
  startAfter?: unknown;
118
118
  orderBy?: string;
119
119
  searchString?: string;
120
+ /** Ask each row which declared search field matched — populates `_matches`. */
121
+ searchExplain?: boolean;
120
122
  order?: "desc" | "asc";
121
123
  /** Vector similarity search configuration */
122
124
  vectorSearch?: VectorSearchParams;
@@ -169,6 +171,23 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
169
171
  upsert?: boolean;
170
172
  }
171
173
 
174
+ /**
175
+ * @internal
176
+ */
177
+ export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
178
+ path: string;
179
+ /**
180
+ * The rows to update, each named by its address.
181
+ *
182
+ * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
183
+ * values and is insert-shaped — `saveMany` passes `status: "new"` and no
184
+ * `id`, so it cannot express "update exactly this row". This can, and it is
185
+ * why bulk update is a separate driver method rather than a flag on that one.
186
+ */
187
+ updates: { id: string | number; values: Partial<EntityValues<M>> }[];
188
+ collection?: CollectionConfig<M>;
189
+ }
190
+
172
191
  /**
173
192
  * @internal
174
193
  */
@@ -177,6 +196,15 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
177
196
  collection?: CollectionConfig<M>;
178
197
  }
179
198
 
199
+ /**
200
+ * @internal
201
+ */
202
+ export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
203
+ path: string;
204
+ ids: (string | number)[];
205
+ collection?: CollectionConfig<M>;
206
+ }
207
+
180
208
  export type FilterCombinationValidProps = {
181
209
  path: string;
182
210
  databaseId?: string;
@@ -258,6 +286,17 @@ export interface DataDriver {
258
286
  */
259
287
  saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
260
288
 
289
+ /**
290
+ * Update many rows in one transaction, each addressed by id.
291
+ *
292
+ * Optional for the same reason `saveMany` is: a driver that cannot make the
293
+ * batch atomic should not pretend to. The REST layer reports
294
+ * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
295
+ * writes, which would be neither atomic nor one round trip — the two things
296
+ * a caller reaches for a batch to get.
297
+ */
298
+ updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
299
+
261
300
  /**
262
301
  * Delete a entity
263
302
  * @param props
@@ -271,6 +310,14 @@ export interface DataDriver {
271
310
  */
272
311
  deleteAll?(path: string): Promise<void>;
273
312
 
313
+ /**
314
+ * Delete many rows in one transaction, addressed by id.
315
+ *
316
+ * Ids rather than a filter, deliberately — see
317
+ * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
318
+ */
319
+ deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
320
+
274
321
  /**
275
322
  * Check if the given property is unique in the given collection
276
323
  * @param path Collection path
@@ -383,6 +430,8 @@ export interface RestFetchService {
383
430
  offset?: number;
384
431
  startAfter?: Record<string, unknown>;
385
432
  searchString?: string;
433
+ /** Ask each row which declared search fields matched — populates `_matches`. */
434
+ searchExplain?: boolean;
386
435
  databaseId?: string;
387
436
  vectorSearch?: VectorSearchParams;
388
437
  },
package/src/errors.ts CHANGED
@@ -1,3 +1,42 @@
1
+ /**
2
+ * The error codes every route can produce, as `RebaseApiError.code`.
3
+ *
4
+ * These are the defaults on `ApiError`'s static constructors server-side, so
5
+ * any endpoint can answer with one. They are **not** the complete set: routes
6
+ * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,
7
+ * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.
8
+ *
9
+ * Hence the union is deliberately open rather than closed. It exists to give
10
+ * autocomplete and to catch a typo in the common cases — `code` was a bare
11
+ * `string`, so `e.code === "NOT_FOUND"` and `e.code === "NOTFOUND"` were
12
+ * equally valid and only one of them worked. Closing it would be a lie that
13
+ * broke the moment a route added a code.
14
+ *
15
+ * @example
16
+ * if (e instanceof RebaseApiError) {
17
+ * switch (e.code) {
18
+ * case "NOT_FOUND": return null; // completed
19
+ * case "FORBIDDEN": return redirect();
20
+ * default: throw e; // routes' own codes land here
21
+ * }
22
+ * }
23
+ *
24
+ * @group Errors
25
+ */
26
+ export type RebaseErrorCode =
27
+ | "BAD_REQUEST"
28
+ | "UNAUTHORIZED"
29
+ | "FORBIDDEN"
30
+ | "NOT_FOUND"
31
+ | "CONFLICT"
32
+ | "INTERNAL_ERROR"
33
+ | "SERVICE_UNAVAILABLE"
34
+ | "DB_PERMISSION_DENIED"
35
+ | "SCHEMA_DRIFT"
36
+ // `string & {}` keeps the union open while preserving completion on the
37
+ // literals above — a bare `| string` would collapse them and offer nothing.
38
+ | (string & {});
39
+
1
40
  /**
2
41
  * Structured initializer for {@link RebaseApiError}.
3
42
  *
@@ -10,8 +49,8 @@ export interface RebaseErrorInit {
10
49
  * logic errors that have no HTTP status.
11
50
  */
12
51
  status?: number;
13
- /** Stable, machine-readable error code (e.g. `"NOT_FOUND"`, `"BAD_REQUEST"`). */
14
- code?: string;
52
+ /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
53
+ code?: RebaseErrorCode;
15
54
  /** Structured error payload returned by the server, when present. */
16
55
  details?: unknown;
17
56
  /** The underlying error this one wraps, if any. */
@@ -45,8 +84,8 @@ export interface RebaseErrorInit {
45
84
  export class RebaseApiError extends Error {
46
85
  /** HTTP status code, or `undefined` for non-HTTP errors. */
47
86
  readonly status?: number;
48
- /** Stable machine-readable error code, when the server supplied one. */
49
- readonly code?: string;
87
+ /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
88
+ readonly code?: RebaseErrorCode;
50
89
  /** Structured error payload from the server, when present. */
51
90
  readonly details?: unknown;
52
91
 
@@ -41,6 +41,7 @@ export const ADMIN_COLLECTION_KEYS = [
41
41
  "defaultSize",
42
42
  "defaultViewMode",
43
43
  "disableDefaultActions",
44
+ "display",
44
45
  "enabledViews",
45
46
  "entityActions",
46
47
  "entityViews",
@@ -51,6 +52,7 @@ export const ADMIN_COLLECTION_KEYS = [
51
52
  "formAutoSave",
52
53
  "formView",
53
54
  "group",
55
+ "hideFromEntityViews",
54
56
  "hideFromNavigation",
55
57
  "hideIdFromCollection",
56
58
  "hideIdFromForm",