@rebasepro/types 0.23.0 → 0.24.1-canary.g19596ef

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 (35) hide show
  1. package/dist/call_context.d.ts +8 -4
  2. package/dist/controllers/client.d.ts +113 -24
  3. package/dist/controllers/collection_registry.d.ts +17 -0
  4. package/dist/controllers/data.d.ts +570 -122
  5. package/dist/controllers/data_driver.d.ts +85 -1
  6. package/dist/controllers/index.d.ts +1 -0
  7. package/dist/controllers/storage-references.d.ts +114 -0
  8. package/dist/controllers/storage.d.ts +73 -13
  9. package/dist/errors.d.ts +15 -1
  10. package/dist/index.es.js +1228 -43
  11. package/dist/index.es.js.map +1 -1
  12. package/dist/types/admin_block.d.ts +1 -1
  13. package/dist/types/api_keys.d.ts +68 -36
  14. package/dist/types/auth_adapter.d.ts +97 -2
  15. package/dist/types/backend.d.ts +356 -9
  16. package/dist/types/backup.d.ts +102 -0
  17. package/dist/types/collection_patch.d.ts +74 -0
  18. package/dist/types/collections.d.ts +23 -0
  19. package/dist/types/cron.d.ts +58 -7
  20. package/dist/types/database_adapter.d.ts +34 -8
  21. package/dist/types/entities.d.ts +11 -0
  22. package/dist/types/history.d.ts +15 -1
  23. package/dist/types/index.d.ts +3 -0
  24. package/dist/types/project_manifest.d.ts +73 -1
  25. package/dist/types/properties.d.ts +39 -2
  26. package/dist/types/register.d.ts +43 -0
  27. package/dist/types/relations.d.ts +37 -1
  28. package/dist/types/resource_kinds.d.ts +20 -10
  29. package/dist/types/schema_editing.d.ts +39 -13
  30. package/dist/types/scopes.d.ts +272 -0
  31. package/dist/types/storage_authorize.d.ts +10 -1
  32. package/dist/types/storage_source.d.ts +2 -6
  33. package/dist/types/websockets.d.ts +32 -1
  34. package/dist/users/user.d.ts +7 -0
  35. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import type { VectorSearchParams } from "./data_driver.js";
2
2
  import type { ComputedSortField, SearchMatch } from "../types/search.js";
3
3
  import { Entity, EntityValues } from "../types/entities.js";
4
- import { WhereFilterOp, FieldPath, NonColumnFieldPath, FilterValues, NullsPlacement, OrderBySpec, RelationAggregateSort } from "../types/filter-operators.js";
4
+ import { WhereFilterOp, FieldPath, NonColumnFieldPath, FilterValues, NullsPlacement, OrderBySpec, OrderByTuple, RelationAggregateSort } from "../types/filter-operators.js";
5
5
  /**
6
6
  * The element type of an array column, and the column's own type otherwise.
7
7
  *
@@ -44,8 +44,10 @@ export type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;
44
44
  * - `array-contains` → one element of the column (or a related row's id).
45
45
  * - `in` / `not-in` / `array-contains-any` → a list of elements; a bare element
46
46
  * is read as the one-element list, and `null` is a null check.
47
- * - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern. Always a
48
- * string, including on numeric and date columns, which the driver casts.
47
+ * - `like` / `ilike` / `not-like` / `not-ilike` → a SQL pattern, always a
48
+ * string. It matches a text column, or an enum by its label; on a number,
49
+ * a date or any other column the Postgres driver refuses it with a 400
50
+ * (`INVALID_FILTER_OPERATOR`).
49
51
  * - `is-null` / `is-not-null` → nothing; the value is ignored everywhere.
50
52
  * - everything else → the column's own type, or `null` for a null comparison.
51
53
  *
@@ -54,6 +56,33 @@ export type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;
54
56
  * permissive as it was.
55
57
  */
56
58
  export type WhereValueFor<Op extends WhereFilterOp, T> = Op extends "array-contains" ? WhereElementOf<T> : Op extends "in" | "not-in" | "array-contains-any" ? readonly WhereElementOf<T>[] | WhereElementOf<T> | null : Op extends "like" | "ilike" | "not-like" | "not-ilike" ? string : Op extends "is-null" | "is-not-null" ? null | undefined : T | null;
59
+ /**
60
+ * One `[operator, value]` condition on a column of type `T`, with the value
61
+ * typed for the operator exactly as the fluent `where(column, op, value)` types
62
+ * it — a union with one tuple per operator, so `["array-contains", "featured"]`
63
+ * on a `string[]` column compiles and `["array-contains", ["featured"]]` (which
64
+ * builds `@> ARRAY[$1]` and matches nothing) does not.
65
+ */
66
+ export type WhereConditionFor<T> = {
67
+ [Op in WhereFilterOp]: [Op, WhereValueFor<Op, T>];
68
+ }[WhereFilterOp];
69
+ /**
70
+ * The object-form `where` for row type `M`: each column takes conditions typed
71
+ * against that column, as the fluent form does.
72
+ *
73
+ * `FilterValues` typed the key and left the value `unknown`, so the very
74
+ * shapes the fluent form was fixed to refuse — a wrong value type, an array
75
+ * where `array-contains` wants one element — compiled in `find({ where })`,
76
+ * the form `listen`, `observe`, `count` and `iterate` all take.
77
+ *
78
+ * A path that is not a column (`author.name`, `metadata->>tier`) has no type to
79
+ * check against and stays `unknown`, as in {@link FieldPath}. An untyped `M`
80
+ * (`Record<string, unknown>`, every key) collapses to plain `FilterValues`, so
81
+ * an untyped client is exactly as permissive as before.
82
+ */
83
+ export type WhereFor<M extends Record<string, unknown>> = string extends keyof M ? FilterValues<FieldPath<M>> : {
84
+ [K in Extract<keyof M, string>]?: WhereConditionFor<M[K]> | WhereConditionFor<M[K]>[];
85
+ } & FilterValues<NonColumnFieldPath>;
57
86
  /**
58
87
  * A group of conditions combined with `and`, `or`, or negated with `not`.
59
88
  *
@@ -79,6 +108,53 @@ export interface FilterCondition {
79
108
  operator: WhereFilterOp;
80
109
  value: unknown;
81
110
  }
111
+ /**
112
+ * One {@link FilterCondition} on row type `M`: the column is one of `M`'s and
113
+ * the value is typed for the operator against that column, exactly as the
114
+ * fluent `where(column, op, value)` types it. A path that is not a column
115
+ * (`author.name`, `metadata->>tier`) has nothing to check against and stays
116
+ * `unknown`, as in {@link WhereFor}.
117
+ *
118
+ * An untyped `M` (`Record<string, unknown>`) collapses to plain
119
+ * `FilterCondition`, so an untyped client is exactly as permissive as before.
120
+ */
121
+ export type FilterConditionFor<M extends Record<string, unknown>> = ColumnConditions<M>[keyof ColumnConditions<M>] | {
122
+ column: NonColumnFieldPath;
123
+ operator: WhereFilterOp;
124
+ value: unknown;
125
+ };
126
+ /**
127
+ * Per column, the union of its typed conditions; an index-signature key (an
128
+ * untyped row) maps to plain {@link FilterCondition}.
129
+ *
130
+ * A key-remapped mapped type rather than a conditional over `keyof M`, for the
131
+ * reason {@link NumericKeyOf} gives: tsc measures the conditional form as
132
+ * invariant in `M`, and a typed `SDKCollectionClient<Post>` stopped widening to
133
+ * the wide `SDKCollectionClient`.
134
+ */
135
+ type ColumnConditions<M extends Record<string, unknown>> = {
136
+ [K in keyof M as K extends string ? K : never]: string extends K ? FilterCondition : {
137
+ [Op in WhereFilterOp]: {
138
+ column: K;
139
+ operator: Op;
140
+ value: WhereValueFor<Op, M[K]>;
141
+ };
142
+ }[WhereFilterOp];
143
+ };
144
+ /**
145
+ * A {@link LogicalCondition} whose every condition, at any depth, is a
146
+ * {@link FilterConditionFor} row type `M`.
147
+ *
148
+ * `cond()` cannot know the row it will be used on, so it returns its column,
149
+ * operator and value exactly as written; this is where they are checked.
150
+ * `FindParams.logical` and the builders' `.where(group)` take it, so
151
+ * `posts.where(or(cond("titel", "==", 1)))` is a compile error on a typed
152
+ * client rather than the server's 400.
153
+ */
154
+ export interface LogicalConditionFor<M extends Record<string, unknown>> {
155
+ type: "and" | "or" | "not";
156
+ conditions: (FilterConditionFor<M> | LogicalConditionFor<M>)[];
157
+ }
82
158
  /**
83
159
  * How one relation is loaded by {@link FindParams.include}.
84
160
  *
@@ -123,6 +199,19 @@ export interface IncludeOptions {
123
199
  fields?: string[];
124
200
  /** Relations of the related row to load in turn. */
125
201
  include?: IncludeSpec;
202
+ /**
203
+ * Also serve how many related rows there are in all, per parent, under
204
+ * the row's `_count` ({@link INCLUDE_COUNT_KEY}) — `{ tags: [...], _count:
205
+ * { tags: 37 } }`. Counted by the same query that reads the rows, over the
206
+ * same rows a read without `limit` would return (the include's `where`,
207
+ * soft delete, row-level security), so `limit` caps what is sent and the
208
+ * count says what was left out.
209
+ *
210
+ * To-many relations only: a to-one relation is a 400.
211
+ *
212
+ * @since 0.24
213
+ */
214
+ count?: boolean;
126
215
  }
127
216
  /**
128
217
  * The relations a read loads, as a list of (possibly dotted) names or as a
@@ -131,7 +220,11 @@ export interface IncludeOptions {
131
220
  * - `["author", "comments.author"]` — a dotted path is the same thing as the
132
221
  * nested object form, spelled flat. It is what the REST `?include=` parameter
133
222
  * carries, and the two forms compile to the same request.
134
- * - `["*"]` — every relation, one hop deep. The admin panel's shape.
223
+ * - `["*"]` — every relation the collection declares, one hop deep, those
224
+ * declared only for a record's tabs included — so on a list it can embed
225
+ * every row of a to-many relation no column shows. Name the relations the
226
+ * view renders instead; the admin panel does, and sends `*` only when it
227
+ * cannot resolve a collection's relations in the browser.
135
228
  * - `{ comments: { limit: 5, include: { author: true } } }` — the parametrised
136
229
  * form.
137
230
  *
@@ -143,6 +236,61 @@ export interface IncludeOptions {
143
236
  * @group Data
144
237
  */
145
238
  export type IncludeSpec = string[] | Record<string, true | IncludeOptions>;
239
+ /**
240
+ * What a read of one record by id takes besides the id — `findById` and
241
+ * `listenById`.
242
+ *
243
+ * @group Data
244
+ */
245
+ export interface ByIdReadOptions<Inc = IncludeSpec> {
246
+ /**
247
+ * Relations to load into the row, as {@link FindParams.include} loads them
248
+ * into each row of a list. A to-many relation not named here is absent
249
+ * from the row — not empty.
250
+ */
251
+ include?: Inc;
252
+ }
253
+ /**
254
+ * An include checked against one generated collection's relations: `R` is its
255
+ * `Relations` map (relation name → the accessor it reaches) and `DB` the
256
+ * generated `Database`, so a nested `include` is checked against the *target's*
257
+ * relations in turn.
258
+ *
259
+ * A name, a dotted path starting with one, or `"*"`; or the tree, keyed by
260
+ * relation name. A name that is not a relation is a 400 `UNKNOWN_RELATION`
261
+ * from the server, and a compile error here.
262
+ *
263
+ * @group Data
264
+ */
265
+ export type IncludeSpecFor<DB, R> = readonly (Extract<keyof R, string> | "*" | `${Extract<keyof R, string>}.${string}`)[] | {
266
+ [K in Extract<keyof R, string>]?: true | IncludeOptionsFor<DB, R[K]>;
267
+ };
268
+ /**
269
+ * {@link IncludeOptions} for a relation that reaches the generated collection
270
+ * `T`: its nested `include` is checked against `T`'s relations.
271
+ *
272
+ * @group Data
273
+ */
274
+ export interface IncludeOptionsFor<DB, T> extends Omit<IncludeOptions, "include"> {
275
+ include?: T extends keyof DB ? IncludeOf<DB[T], DB> : IncludeSpec;
276
+ }
277
+ /**
278
+ * The include a generated `Database` entry takes — {@link IncludeSpecFor} over
279
+ * its `Relations` — or the unchecked {@link IncludeSpec} for anything that does
280
+ * not declare its relations (a hand-written row type, an older generated file).
281
+ *
282
+ * @group Data
283
+ */
284
+ export type IncludeOf<Entry, DB> = Entry extends {
285
+ Relations: infer R;
286
+ } ? IncludeSpecFor<DB, R> : IncludeSpec;
287
+ /**
288
+ * One argument of the fluent `.include(...)`: an element of the include list, a
289
+ * whole list, or the tree.
290
+ *
291
+ * @group Data
292
+ */
293
+ export type IncludeArgOf<Inc> = Inc extends readonly (infer E)[] ? E | Inc : Inc;
146
294
  /**
147
295
  * Hops an {@link IncludeSpec} may nest. `comments.author` is two.
148
296
  *
@@ -171,14 +319,16 @@ export declare const MAX_INCLUDE_DEPTH = 3;
171
319
  *
172
320
  * ## Pagination precedence
173
321
  *
174
- * `limit`/`offset` and `page` describe the same window two ways. If **both
175
- * `offset` and `page` are provided, `page` wins** — the backend computes
176
- * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
177
- * explicit `offset`. Pick one style per query.
322
+ * `limit`/`offset` and `page` describe the same window two ways, so send one.
323
+ * `page` is `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`. A read with
324
+ * **both** is refused with `PAGE_WITH_OFFSET` — a 400 from the server, and a
325
+ * `RebaseClientError` with that code from a client before any request — rather
326
+ * than quietly applying one of them. `after` is refused beside either
327
+ * (`CURSOR_WITH_OFFSET`).
178
328
  *
179
329
  * @group Data
180
330
  */
181
- export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
331
+ export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> {
182
332
  /**
183
333
  * Maximum number of items to return.
184
334
  *
@@ -193,13 +343,14 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
193
343
  */
194
344
  limit?: number;
195
345
  /**
196
- * Number of items to skip. Ignored when {@link FindParams.page} is also
197
- * set — `page` takes precedence.
346
+ * Number of items to skip. Not with {@link FindParams.page}: the pair is
347
+ * refused with `PAGE_WITH_OFFSET`.
198
348
  */
199
349
  offset?: number;
200
350
  /**
201
- * Page number (1-indexed), alternative to {@link FindParams.offset}.
202
- * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
351
+ * Page number (1-indexed), alternative to {@link FindParams.offset}: it
352
+ * skips `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` rows. Sent with
353
+ * `offset`, the read is refused with `PAGE_WITH_OFFSET`.
203
354
  */
204
355
  page?: number;
205
356
  /**
@@ -215,13 +366,13 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
215
366
  * { role: ["in", ["admin", "editor"]] }
216
367
  * { age: [[">=", 18], ["<", 65]] }
217
368
  */
218
- where?: FilterValues<FieldPath<M>>;
369
+ where?: WhereFor<M>;
219
370
  /**
220
371
  * Logical grouping conditions (AND/OR). Use this for anything `where`
221
372
  * can't express — notably OR-ing conditions. AND-ed with `where` and
222
373
  * `searchString` when present (see the interface docs).
223
374
  */
224
- logical?: LogicalCondition;
375
+ logical?: LogicalConditionFor<M>;
225
376
  /**
226
377
  * Sort order as a `[field, direction]` tuple, or a list of them applied in
227
378
  * order of significance — the second key breaks ties on the first, and so on.
@@ -236,11 +387,14 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
236
387
  * Not checked against `M` here: a relation name comes from the collection's
237
388
  * `relations`, not from its columns, so nothing in a *hand-written* row type
238
389
  * can validate one. A **generated** `Database` narrows this to the
239
- * collection's actual relation keys, recursively — see `rebase codegen`.
390
+ * collection's actual relation keys, recursively — see
391
+ * {@link IncludeSpecFor}. The row a read returns still types each relation
392
+ * as optional; the generated `RowWith<A, I>` names the row with the
393
+ * included ones required.
240
394
  *
241
395
  * An unknown name is a 400 `UNKNOWN_RELATION`. It used to be ignored.
242
396
  */
243
- include?: IncludeSpec;
397
+ include?: Inc;
244
398
  /**
245
399
  * Columns to return, instead of all of them.
246
400
  *
@@ -263,8 +417,10 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
263
417
  * projection (which it always is) every row is already distinct. Pair it
264
418
  * with `fields` naming the columns you actually want the distinct values of.
265
419
  *
266
- * `meta.total` counts distinct rows too, so a distinct listing's `hasMore`
267
- * describes the set it is paging.
420
+ * A distinct read is not counted, whichever door it goes through: over
421
+ * HTTP its `meta.total` is absent, and in-process it is the rows up to the
422
+ * end of the page — as for {@link FindParams.count} `"none"`. `hasMore`
423
+ * is exact either way.
268
424
  */
269
425
  distinct?: boolean;
270
426
  /**
@@ -324,6 +480,23 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
324
480
  * carries a `searchString`; there is nothing to explain otherwise.
325
481
  */
326
482
  searchExplain?: boolean;
483
+ /**
484
+ * Whether to count the whole matching set for `meta.total`.
485
+ *
486
+ * `"exact"`, the default, runs a `COUNT(*)` over every row the query
487
+ * matches beside the page — and on a large table that count, not the page,
488
+ * is most of what a list read costs: it reads every matching row to report
489
+ * one number. `"none"` skips it. `meta.hasMore` is still exact (the read
490
+ * asks for one row past the page and reports whether it came back), so
491
+ * nothing that pages — `iterate()`, `findAll()`, a "load more" button —
492
+ * needs the count.
493
+ *
494
+ * Without the count there is no `total` to report: over HTTP `meta.total`
495
+ * is absent, and in-process it is the rows up to the end of this page
496
+ * (`offset` + the page), the figure a driver that cannot count reports.
497
+ * Sent as `?count=none`.
498
+ */
499
+ count?: "exact" | "none";
327
500
  }
328
501
  /**
329
502
  * Paginated response from a collection query.
@@ -352,7 +525,7 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
352
525
  */
353
526
  export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
354
527
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
355
- where(logicalCondition: LogicalCondition): this;
528
+ where(logicalCondition: LogicalConditionFor<M>): this;
356
529
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
357
530
  limit(count: number): this;
358
531
  offset(count: number): this;
@@ -395,7 +568,7 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
395
568
  /**
396
569
  * Find a single record by its ID.
397
570
  */
398
- findById(id: string | number): Promise<Entity<M> | undefined>;
571
+ findById(id: string | number, options?: ByIdReadOptions): Promise<Entity<M> | undefined>;
399
572
  /**
400
573
  * Create a new record.
401
574
  * @param data The entity data to create.
@@ -415,9 +588,16 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
415
588
  }): Promise<Entity<M>[]>;
416
589
  /**
417
590
  * Update an existing record by ID.
591
+ *
592
+ * `options.ifMatch` is the version the edit was made against
593
+ * ({@link Entity.etag}): the write is refused with `412
594
+ * PRECONDITION_FAILED` when the row has changed since. A data source that
595
+ * cannot check one ignores it, and the write is unconditional.
418
596
  * @returns The updated entity
419
597
  */
420
- update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
598
+ update(id: string | number, data: Partial<EntityValues<M>>, options?: {
599
+ ifMatch?: string;
600
+ }): Promise<Entity<M>>;
421
601
  /**
422
602
  * Update many records in a single transaction.
423
603
  *
@@ -443,10 +623,23 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
443
623
  */
444
624
  listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
445
625
  /**
446
- * Subscribe to a single record for real-time updates.
626
+ * Subscribe to a single record for real-time updates: every frame is the
627
+ * record as `findById(id, options)` reads it.
447
628
  * Optional method.
448
629
  */
449
- listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void): () => void;
630
+ listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void, options?: ByIdReadOptions): () => void;
631
+ /**
632
+ * {@link SDKCollectionClient.listenCount}: the matching rows' count, live.
633
+ * Optional method; absent where the data source cannot subscribe.
634
+ */
635
+ listenCount?(params: FindParams<M> | undefined, onCount: (total: number) => void, onError?: (error: Error) => void): () => void;
636
+ /**
637
+ * The rows a soft delete stamped, and the delete that removes a row for
638
+ * good: what {@link SDKCollectionClient.trash} is built from. A restore is
639
+ * an ordinary {@link update}. Optional; absent where the data source
640
+ * cannot read a collection's trash.
641
+ */
642
+ trash?: CollectionTrashAccessor<M>;
450
643
  /**
451
644
  * Count the number of records matching the given filter.
452
645
  *
@@ -463,9 +656,9 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
463
656
  * absent implementation in a stub that says so rather than returning a
464
657
  * number nothing counted.
465
658
  */
466
- aggregate?(params: AggregateParams<M>): Promise<AggregateRow[]>;
659
+ aggregate?(params: AggregateParams<M>): Promise<AggregateResult>;
467
660
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;
468
- where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
661
+ where(logicalCondition: LogicalConditionFor<M>): QueryBuilderInterface<M>;
469
662
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
470
663
  limit(count: number): QueryBuilderInterface<M>;
471
664
  offset(count: number): QueryBuilderInterface<M>;
@@ -492,6 +685,11 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
492
685
  * @group Data
493
686
  */
494
687
  export interface PaginationMeta {
688
+ /**
689
+ * Every row the query matches, counted. Not counted — and absent from an
690
+ * HTTP response — when the query asked for {@link FindParams.count}
691
+ * `"none"`, and on a `distinct` read over HTTP.
692
+ */
495
693
  total: number;
496
694
  limit: number;
497
695
  offset: number;
@@ -587,9 +785,26 @@ export type AggregateSelect<M extends Record<string, unknown> = Record<string, u
587
785
  fn: "count";
588
786
  field?: Extract<keyof M, string>;
589
787
  } | {
590
- fn: "sum" | "avg" | "min" | "max";
788
+ fn: "sum" | "avg";
789
+ field: NumericKeyOf<M>;
790
+ } | {
791
+ fn: "min" | "max";
591
792
  field: Extract<keyof M, string>;
592
793
  };
794
+ /**
795
+ * The columns of `M` that hold numbers — what `sum` and `avg` can add up. Any
796
+ * column used to be accepted, and `sum` of a text column was a 400. An untyped
797
+ * row (every key `string`) keeps every name.
798
+ *
799
+ * Written as the keys of a key-remapped mapped type, not as a conditional over
800
+ * `keyof M`: tsc measures the conditional form as invariant in `M`, which made
801
+ * a typed `SDKCollectionClient<Post>` unassignable to the wide
802
+ * `SDKCollectionClient` that helpers taking any collection accept. A mapped
803
+ * type is related structurally, so a typed client still widens.
804
+ */
805
+ export type NumericKeyOf<M extends Record<string, unknown>> = Extract<keyof {
806
+ [K in keyof M as string extends K ? K : NonNullable<M[K]> extends number ? K : never]: M[K];
807
+ }, string>;
593
808
  /**
594
809
  * One row of an aggregate result: the `groupBy` columns, plus one key per
595
810
  * {@link AggregateSelect} under its derived alias.
@@ -605,14 +820,15 @@ export type AggregateRow = Record<string, unknown>;
605
820
  * What {@link SDKCollectionClient.aggregate} takes: the same narrowing a
606
821
  * `find()` takes, minus the parts of it that describe a *page* of rows.
607
822
  *
608
- * `limit` survives and means what it means on the REST route — a bound on the
609
- * number of **groups**, because grouping by a high-cardinality column is a whole
610
- * table's worth of rows in one response. It is ignored when there is no
611
- * `groupBy`, since an ungrouped aggregate is one row.
823
+ * `limit` and `offset` page through the **groups**, as they page a listing's
824
+ * rows, because grouping by a high-cardinality column is a whole table's worth
825
+ * of rows in one response. Over HTTP a grouped aggregate with no `limit` gets
826
+ * the listing default (50 groups) — read {@link AggregateResult.meta} to know
827
+ * when there were more. Without `groupBy` there is one row: `limit` does nothing
828
+ * there and `offset` is refused.
612
829
  *
613
- * `orderBy`, `include`, `after` and the rest are absent on purpose: an
614
- * aggregate has no rows to sort, no relations to load and no page to continue.
615
- * They were silently ignored on the REST route; here they do not typecheck.
830
+ * `include`, `after` and the rest are absent on purpose: an aggregate has no
831
+ * relations to load and no row to continue after.
616
832
  *
617
833
  * @group Data
618
834
  */
@@ -622,20 +838,81 @@ export interface AggregateParams<M extends Record<string, unknown> = Record<stri
622
838
  /** Columns to group by. Omit for a single row over everything that matches. */
623
839
  groupBy?: Extract<keyof M, string>[];
624
840
  /** Filter conditions, as {@link FindParams.where}. */
625
- where?: FilterValues<FieldPath<M>>;
841
+ where?: WhereFor<M>;
626
842
  /** An `and`/`or`/`not` group, AND-ed with `where`. */
627
- logical?: LogicalCondition;
843
+ logical?: LogicalConditionFor<M>;
628
844
  /** Text search, AND-ed with the filters. */
629
845
  searchString?: string;
630
- /** Most groups to return. Ignored without `groupBy`. */
846
+ /** Most groups to return. Grouped aggregates only. */
631
847
  limit?: number;
848
+ /** Groups to skip, to page past `limit`. Grouped aggregates only. */
849
+ offset?: number;
850
+ /**
851
+ * Sort the groups — by a `groupBy` column or by an aggregate's alias
852
+ * (`"count"`, `"sum_total"`, see {@link AggregateAlias}). One
853
+ * `[key, "asc" | "desc"]` tuple, or several applied in order. Groups the
854
+ * sort leaves tied keep their `groupBy` order. Grouped aggregates only:
855
+ * without `groupBy` there is one row, and over HTTP an `orderBy` beside it is
856
+ * refused with `INVALID_AGGREGATE_WINDOW`. A key that is neither a group
857
+ * column nor a selected aggregate is refused with `INVALID_ORDER_BY`.
858
+ *
859
+ * ```ts
860
+ * await client.data.orders.aggregate({
861
+ * select: [{ fn: "sum", field: "total" }],
862
+ * groupBy: ["status"],
863
+ * orderBy: ["sum_total", "desc"]
864
+ * });
865
+ * ```
866
+ */
867
+ orderBy?: OrderByTuple<AggregateOrderKey<M>> | OrderByTuple<AggregateOrderKey<M>>[];
632
868
  }
633
869
  /**
634
- * Which column an iteration seeks on, for keyset ("seek") pagination.
870
+ * The key an {@link AggregateSelect} comes back under: `count` for `count()`,
871
+ * `<fn>_<field>` otherwise (`sum_total`, `max_created_at`).
872
+ *
873
+ * @group Data
874
+ */
875
+ export type AggregateAlias<M extends Record<string, unknown> = Record<string, unknown>> = "count" | `${"count" | "min" | "max"}_${Extract<keyof M, string>}` | `${"sum" | "avg"}_${NumericKeyOf<M>}`;
876
+ /**
877
+ * What {@link AggregateParams.orderBy} can sort the groups by: a column (to
878
+ * group by) or an aggregate's alias.
879
+ *
880
+ * @group Data
881
+ */
882
+ export type AggregateOrderKey<M extends Record<string, unknown> = Record<string, unknown>> = Extract<keyof M, string> | AggregateAlias<M>;
883
+ /**
884
+ * Where a grouped aggregate's page of groups sits: the `limit` it was cut at,
885
+ * the `offset` it started from, and whether more groups follow.
886
+ *
887
+ * @group Data
888
+ */
889
+ export interface AggregateMeta {
890
+ limit: number;
891
+ offset: number;
892
+ hasMore: boolean;
893
+ }
894
+ /**
895
+ * What {@link SDKCollectionClient.aggregate} resolves to: the rows, as an
896
+ * array, carrying the page they are as `meta`.
897
+ *
898
+ * `meta` is present on a grouped aggregate that was cut at a `limit` — which,
899
+ * over HTTP, is every grouped one, since the route applies the listing default
900
+ * when none is given. `hasMore: true` means the groups did not all fit: page on
901
+ * with `offset`, or raise `limit`. It is a non-enumerable property, so a spread,
902
+ * `JSON.stringify` or `Object.keys` of the result sees only the rows.
903
+ *
904
+ * @group Data
905
+ */
906
+ export interface AggregateResult extends Array<AggregateRow> {
907
+ readonly meta?: AggregateMeta;
908
+ }
909
+ /**
910
+ * Which column an iteration sorts by when it is asked to seek, for keyset
911
+ * ("seek") pagination.
635
912
  *
636
913
  * Either the column name on its own — sorted ascending — or the column plus an
637
- * explicit direction. The column must be **unique** and must be the column the
638
- * query is ordered by; see {@link PageWalkOptions.cursor}.
914
+ * explicit direction. It is used only when the query names no `orderBy`; see
915
+ * {@link PageWalkOptions.cursor}.
639
916
  *
640
917
  * @group Data
641
918
  */
@@ -657,23 +934,23 @@ export interface PageWalkOptions<M extends Record<string, unknown> = Record<stri
657
934
  */
658
935
  pageSize?: number;
659
936
  /**
660
- * Paginate by **seeking on a column** instead of by offset.
937
+ * **Require** seeking: fail rather than fall back to offset paging.
661
938
  *
662
- * Offset paging — the default — re-counts rows on every request, so a row
663
- * inserted or deleted *while the iteration runs* shifts the window and the
664
- * walk silently skips or repeats rows. Seeking is immune to that: each page
665
- * asks for rows strictly after the last one seen, so concurrent writes
666
- * before the cursor cannot move it.
939
+ * Every walk already seeks when it can. After the first page it hands the
940
+ * server's `meta.nextCursor` back as `after`, so each page asks for rows
941
+ * strictly after the last one seen — concurrent writes before that row
942
+ * cannot shift the window, and the database never skips rows by count.
943
+ * Where the server issues no cursor (a relevance-ordered search, a vector
944
+ * search, a `distinct` read, a server that predates cursors), the default
945
+ * walk pages by offset instead, which a concurrent insert or delete can
946
+ * make skip or repeat a row.
667
947
  *
668
- * Prefer this whenever the collection has a unique, sortable column
669
- * (typically its primary key). The column must be unique — a repeated value
670
- * at a page boundary either skips rows or stalls, and the iterator throws
671
- * rather than looping — and the query is ordered by it, so a `cursor` and a
672
- * conflicting `orderBy` is an error, not a silent override.
673
- *
674
- * Implemented with the parameters `find()` already takes (an `orderBy` plus
675
- * a `>` / `<` filter on the cursor column), so it works on every transport
676
- * and needs nothing new from the server.
948
+ * Passing `cursor` turns that fallback into an error: a listing with no
949
+ * cursor throws `cursor-missing`, and a cursor that stops advancing throws
950
+ * `cursor-stalled`. It also names the column to sort by when the query has
951
+ * no `orderBy`; an explicit `orderBy` wins, and the cursor follows whatever
952
+ * the query is sorted by, multi-key sorts and nullable keys included. The
953
+ * row id breaks the final tie, so the order is always total.
677
954
  *
678
955
  * @example
679
956
  * for await (const job of client.data.jobs.iterate({ cursor: "id" })) { … }
@@ -691,9 +968,12 @@ export interface PageWalkOptions<M extends Record<string, unknown> = Record<stri
691
968
  * `find()` takes except the window itself (`limit`, `offset`, `page`), which
692
969
  * the iterator owns, plus the walk options.
693
970
  *
971
+ * `count` is accepted and ignored: no page of a walk is counted, since nothing
972
+ * a walk returns carries a total (see {@link FindParams.count}).
973
+ *
694
974
  * @group Data
695
975
  */
696
- export type IterateParams<M extends Record<string, unknown> = Record<string, unknown>> = Omit<FindParams<M>, "limit" | "offset" | "page"> & PageWalkOptions<M>;
976
+ export type IterateParams<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> = Omit<FindParams<M, Inc>, "limit" | "offset" | "page"> & PageWalkOptions<M>;
697
977
  /**
698
978
  * Parameters accepted by {@link SDKCollectionClient.findAll}: the iteration
699
979
  * parameters plus the ceiling that keeps a whole collection from being pulled
@@ -701,7 +981,7 @@ export type IterateParams<M extends Record<string, unknown> = Record<string, unk
701
981
  *
702
982
  * @group Data
703
983
  */
704
- export type FindAllParams<M extends Record<string, unknown> = Record<string, unknown>> = IterateParams<M> & {
984
+ export type FindAllParams<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> = IterateParams<M, Inc> & {
705
985
  /**
706
986
  * Most rows to materialise. Defaults to 10 000. Exceeding it **throws**
707
987
  * — a truncated array returned as if it were the whole answer is the
@@ -716,7 +996,7 @@ export type FindAllParams<M extends Record<string, unknown> = Record<string, unk
716
996
  *
717
997
  * @group Data
718
998
  */
719
- export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
999
+ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> {
720
1000
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
721
1001
  /**
722
1002
  * Filter on a relation path (`author.name`) or a JSON path
@@ -733,7 +1013,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
733
1013
  * errors on a typed client.
734
1014
  */
735
1015
  where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): this;
736
- where(logicalCondition: LogicalCondition): this;
1016
+ where(logicalCondition: LogicalConditionFor<M>): this;
737
1017
  /**
738
1018
  * Sort by a column, a relation or JSON path, `_score`, or an aggregate over
739
1019
  * a to-many relation — the same key set {@link FindParams.orderBy} takes.
@@ -753,7 +1033,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
753
1033
  *
754
1034
  * The query embedding is the caller's to produce.
755
1035
  */
756
- vectorSearch(property: string, vector: number[], options?: {
1036
+ vectorSearch(property: Extract<keyof M, string>, vector: number[], options?: {
757
1037
  distance?: "cosine" | "l2" | "inner_product";
758
1038
  threshold?: number;
759
1039
  }): this;
@@ -761,12 +1041,12 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
761
1041
  * Load relations — names, dotted paths (`"comments.author"`), or the
762
1042
  * parametrised tree. Repeated calls merge rather than replace.
763
1043
  */
764
- include(...relations: (string | IncludeSpec)[]): this;
1044
+ include(...relations: IncludeArgOf<Inc>[]): this;
765
1045
  /**
766
1046
  * Return only these columns. A real projection: the columns are what is
767
1047
  * read from the database, not what survives a trim of the response.
768
1048
  */
769
- fields(...columns: (FieldPath<M> | string)[]): this;
1049
+ fields(...columns: Extract<keyof M, string>[]): this;
770
1050
  /** `SELECT DISTINCT` over the projection — see {@link FindParams.distinct}. */
771
1051
  distinct(enabled?: boolean): this;
772
1052
  /** Continue after a previous page's `meta.nextCursor`. */
@@ -777,9 +1057,10 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
777
1057
  *
778
1058
  * The builder's `where`/`logical`/`search` narrow which rows are
779
1059
  * aggregated; its `orderBy`, `include` and window do not apply and are
780
- * ignored, exactly as they are on the REST route.
1060
+ * ignored, exactly as they are on the REST route. Page the groups with the
1061
+ * `limit`/`offset` passed here.
781
1062
  */
782
- aggregate(params: Omit<AggregateParams<M>, "where" | "logical" | "searchString">): Promise<AggregateRow[]>;
1063
+ aggregate(params: Omit<AggregateParams<M>, "where" | "logical" | "searchString">): Promise<AggregateResult>;
783
1064
  /**
784
1065
  * Page through everything this query matches, one row at a time.
785
1066
  *
@@ -867,10 +1148,13 @@ export declare const BATCH_REF_KEY = "$ref";
867
1148
  * one, which is the case worth catching.
868
1149
  *
869
1150
  * Any `$`-prefixed key counts, because `{ $increment: 1 }` written to a number
870
- * column as a JSON document is the failure this exists to prevent. No collection
871
- * can declare a column whose value legitimately has a key beginning with `$`: a
872
- * `map` property's sub-keys are declared, and `$` is not valid in the
873
- * identifiers the DDL generators emit.
1151
+ * column as a JSON document is the failure this exists to prevent.
1152
+ *
1153
+ * Except on a `map` (pass `propertyType`): a map's value is a JSON document,
1154
+ * and one may legitimately carry `$` keys — a JSON Schema's `$schema`, a
1155
+ * MongoDB-style export. There, only a single-key object whose key is one of
1156
+ * {@link FIELD_OPERATORS} is an operation, and anything else is data. A caller
1157
+ * that does not know the property type gets the strict reading.
874
1158
  *
875
1159
  * The one exception is `{ $ref: … }`, the batch's backward reference. It stands
876
1160
  * where a *value* goes and is resolved to one before the row is written, so it
@@ -879,7 +1163,7 @@ export declare const BATCH_REF_KEY = "$ref";
879
1163
  *
880
1164
  * @group Data
881
1165
  */
882
- export declare function isFieldOperation(value: unknown): boolean;
1166
+ export declare function isFieldOperation(value: unknown, propertyType?: string): boolean;
883
1167
  /** True when any value in a write payload is (or is attempting to be) one. @group Data */
884
1168
  export declare function hasFieldOperation(values: Record<string, unknown> | undefined): boolean;
885
1169
  export type FieldOperation =
@@ -906,8 +1190,23 @@ export type FieldOperation =
906
1190
  * @group Data
907
1191
  */
908
1192
  export type UpdateValues<U> = {
909
- [K in keyof U]?: U[K] | FieldOperation;
1193
+ [K in keyof U]?: U[K] | FieldOperationFor<U[K]>;
910
1194
  };
1195
+ /**
1196
+ * The field operations a column of type `T` takes: `$inc` on a number, `$push`
1197
+ * / `$pull` on an array, `$merge` on a map. Any operation on any column used to
1198
+ * compile — `update(id, { title: { $inc: 1 } })` — and the server answered 400.
1199
+ * A column whose type says nothing (`unknown`, an untyped client) takes them all.
1200
+ */
1201
+ export type FieldOperationFor<T> = unknown extends T ? FieldOperation : (NonNullable<T> extends number ? {
1202
+ $inc: number;
1203
+ } : never) | (NonNullable<T> extends readonly unknown[] ? {
1204
+ $push: unknown;
1205
+ } | {
1206
+ $pull: unknown;
1207
+ } : never) | (NonNullable<T> extends readonly unknown[] ? never : NonNullable<T> extends Record<string, unknown> ? {
1208
+ $merge: Record<string, unknown>;
1209
+ } : never);
911
1210
  /**
912
1211
  * Where an upsert looks for the row it might be replacing.
913
1212
  *
@@ -944,22 +1243,40 @@ export interface BatchRef {
944
1243
  /** `<ref name>.<field>`, e.g. `order.id`. */
945
1244
  $ref: string;
946
1245
  }
947
- /** One entry of a batch request. @group Data */
1246
+ /**
1247
+ * A row's values in a batch: the collection's own columns, each of which may
1248
+ * instead be a {@link BatchRef} to a value an earlier operation produces.
1249
+ *
1250
+ * Homomorphic over the row type, so a column the row type requires is required
1251
+ * here too — a create in a batch is the same insert `create()` is. There used
1252
+ * to be an `& Record<string, unknown>` on the end, which let a misspelt column
1253
+ * compile and come back as a 400 `VALIDATION_UNKNOWN_FIELDS`.
1254
+ */
1255
+ type BatchValues<T> = {
1256
+ [F in keyof T]: T[F] | BatchRef;
1257
+ };
1258
+ /**
1259
+ * One entry of a batch request.
1260
+ *
1261
+ * `collection` is the collection's **accessor** on a typed client — the name
1262
+ * `client.data.<accessor>` takes, and the key of the generated `Database`
1263
+ * (`orderItems`). The client resolves it to the slug the route knows
1264
+ * (`order_items`) through its `collections` dictionary; a slug that is not an
1265
+ * accessor is sent as written.
1266
+ *
1267
+ * @group Data
1268
+ */
948
1269
  export type BatchOperation<DB = Record<string, unknown>> = {
949
1270
  [K in Extract<keyof DB, string>]: {
950
1271
  op: "create";
951
1272
  collection: K;
952
- values: {
953
- [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef;
954
- } & Record<string, unknown>;
1273
+ values: BatchValues<InsertOf<DB[K]>>;
955
1274
  /** Name this row so a later operation can reference its columns. */
956
1275
  ref?: string;
957
1276
  } | {
958
1277
  op: "upsert";
959
1278
  collection: K;
960
- values: {
961
- [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef;
962
- } & Record<string, unknown>;
1279
+ values: BatchValues<InsertOf<DB[K]>>;
963
1280
  /** See {@link UpsertOptions.onConflict}. Defaults to the primary key. */
964
1281
  onConflict?: readonly string[];
965
1282
  ref?: string;
@@ -968,8 +1285,8 @@ export type BatchOperation<DB = Record<string, unknown>> = {
968
1285
  collection: K;
969
1286
  id: string | number | BatchRef;
970
1287
  values: {
971
- [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperation | BatchRef;
972
- } & Record<string, unknown>;
1288
+ [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperationFor<UpdateOf<DB[K]>[F]> | BatchRef;
1289
+ };
973
1290
  ref?: string;
974
1291
  } | {
975
1292
  op: "delete";
@@ -1021,6 +1338,10 @@ export interface WriteOptions {
1021
1338
  * attempt is still being answered gets `IDEMPOTENCY_KEY_IN_PROGRESS` (409)
1022
1339
  * and should be sent again. A server that cannot store keys ignores the
1023
1340
  * header rather than refusing the write.
1341
+ *
1342
+ * The in-process accessor (`context.data`, `rebase.data`) has no store to
1343
+ * keep keys in, and refuses one with `UNSUPPORTED_OPTION` rather than
1344
+ * writing as though it had been checked.
1024
1345
  */
1025
1346
  idempotencyKey?: string;
1026
1347
  /**
@@ -1035,7 +1356,9 @@ export interface WriteOptions {
1035
1356
  *
1036
1357
  * Reach for it on imports and fire-and-forget writes. The method resolves
1037
1358
  * to `undefined` (or `[]`) when it is set, so a caller cannot accidentally
1038
- * use a row the server never sent.
1359
+ * use a row the server never sent — and `create`, `update` and `upsert`
1360
+ * are typed `M | undefined` for a literal `returning: false`, so reading a
1361
+ * field off the result is a compile error rather than a `TypeError`.
1039
1362
  */
1040
1363
  returning?: boolean;
1041
1364
  /**
@@ -1051,11 +1374,75 @@ export interface WriteOptions {
1051
1374
  *
1052
1375
  * `"*"` asserts only that the row exists.
1053
1376
  *
1054
- * Honoured on `update` and `delete`.
1377
+ * Honoured on `update` and `delete` over HTTP. The in-process accessor
1378
+ * (`context.data`, `rebase.data`) cannot check a precondition yet, and
1379
+ * refuses one with `UNSUPPORTED_OPTION` rather than writing unconditionally.
1055
1380
  */
1056
1381
  ifMatch?: string;
1057
1382
  }
1058
- export interface SDKCollectionClient<M extends Record<string, unknown> = Record<string, unknown>, I = Partial<M>, U = Partial<M>> {
1383
+ /**
1384
+ * The entity-shaped half of {@link TrashClient}, on a {@link CollectionAccessor}.
1385
+ */
1386
+ export interface CollectionTrashAccessor<M extends Record<string, unknown> = Record<string, unknown>> {
1387
+ /** The trashed rows matching `params`. */
1388
+ find(params?: FindParams<M>): Promise<FindResponse<M>>;
1389
+ /** How many trashed rows match `params`. */
1390
+ count(params?: FindParams<M>): Promise<number>;
1391
+ /** Delete a row for good, trashed or live. */
1392
+ purge(id: string | number): Promise<void>;
1393
+ /** {@link purge} for several rows, as one transaction, where the data source can. */
1394
+ purgeMany?(ids: (string | number)[]): Promise<void>;
1395
+ }
1396
+ /**
1397
+ * Options for {@link TrashClient.restore} and {@link TrashClient.restoreMany}.
1398
+ */
1399
+ export interface RestoreOptions<M extends Record<string, unknown> = Record<string, unknown>> extends WriteOptions {
1400
+ /**
1401
+ * The collection's soft-delete field, as declared in `softDelete`.
1402
+ * Defaults to `deletedAt`, the field `softDelete: true` uses; pass the
1403
+ * name a `softDelete: { field: "archivedAt" }` collection declares.
1404
+ *
1405
+ * A restore is an ordinary update setting this field to `null`, which is
1406
+ * the one update the server lets reach a trashed row.
1407
+ */
1408
+ field?: keyof M & string;
1409
+ }
1410
+ /**
1411
+ * The trash of a collection that declares `softDelete`: the rows a delete
1412
+ * stamped rather than removed. `client.data.<slug>.trash` in the browser,
1413
+ * `context.data.<slug>.trash` and `rebase.data.<slug>.trash` on the server.
1414
+ *
1415
+ * For a collection that declares `softDelete`. On one that does not, `find`
1416
+ * and `count` answer for the live rows, `restore` is refused for a field the
1417
+ * collection does not have, and `purge` is an ordinary delete.
1418
+ *
1419
+ * Where the data source cannot read a trash, every method is a stub that
1420
+ * throws when called.
1421
+ */
1422
+ export interface TrashClient<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> {
1423
+ /** The trashed rows matching `params` — `GET /data/<slug>?deleted=only`. */
1424
+ find(params?: FindParams<M, Inc>): Promise<FindResult<M>>;
1425
+ /** How many trashed rows match `params` — `GET /data/<slug>/count?deleted=only`. */
1426
+ count(params?: FindParams<M, Inc>): Promise<number>;
1427
+ /**
1428
+ * Bring a trashed row back: the update setting the soft-delete field to
1429
+ * `null` (`PATCH /data/<slug>/<id>`). Resolves with the restored row.
1430
+ */
1431
+ restore(id: string | number, options?: RestoreOptions<M>): Promise<M>;
1432
+ /** {@link TrashClient.restore} for several rows, as one transaction (`PATCH /data/<slug>/bulk`). */
1433
+ restoreMany(ids: (string | number)[], options?: RestoreOptions<M>): Promise<M[]>;
1434
+ /**
1435
+ * Delete a row for good — `DELETE /data/<slug>/<id>?hard=true`. It cannot
1436
+ * be restored afterwards. Needs the same permission an ordinary delete
1437
+ * does, and works on a live row as well as a trashed one.
1438
+ */
1439
+ purge(id: string | number, options?: WriteOptions): Promise<void>;
1440
+ /** {@link TrashClient.purge} for several rows, as one transaction (`POST /data/<slug>/bulk/delete?hard=true`). */
1441
+ purgeMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
1442
+ }
1443
+ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<string, unknown>, I = Partial<M>, U = Partial<M>,
1444
+ /** The include this collection takes; {@link IncludeOf} for a generated `Database`. */
1445
+ Inc = IncludeSpec> {
1059
1446
  /**
1060
1447
  * Find multiple records with optional filtering, pagination, and sorting.
1061
1448
  *
@@ -1079,7 +1466,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1079
1466
  * it is where the pagination metadata lives, and it is present exactly when
1080
1467
  * there is some.
1081
1468
  */
1082
- find(params?: FindParams<M>): Promise<FindResult<M>>;
1469
+ find(params?: FindParams<M, Inc>): Promise<FindResult<M>>;
1083
1470
  /**
1084
1471
  * Walk every record matching a query, one row at a time, fetching pages as
1085
1472
  * the consumer consumes them.
@@ -1099,14 +1486,15 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1099
1486
  *
1100
1487
  * ## Consistency
1101
1488
  *
1102
- * By default this pages by **offset**, which is only as stable as the table
1103
- * is still: a row inserted or deleted ahead of the cursor between two
1104
- * requests shifts every later window, so the walk can skip a row or hand
1105
- * back the same one twice. That is inherent to offset paging, not a bug
1106
- * here. On a collection with a unique sortable column, pass
1107
- * {@link PageWalkOptions.cursor} to seek on it instead — the walk then
1108
- * asks for rows strictly after the last one it saw, which concurrent writes
1109
- * cannot perturb.
1489
+ * After the first page the walk **seeks**: it hands the server's
1490
+ * `meta.nextCursor` back as `after`, so each page asks for rows strictly
1491
+ * after the last one it saw, which concurrent writes cannot perturb. A
1492
+ * listing the server issues no cursor for — relevance, a vector search, a
1493
+ * `distinct` read — is paged by **offset** instead, which is only as stable
1494
+ * as the table is still: a row inserted or deleted ahead of the window
1495
+ * between two requests shifts every later one, so the walk can skip a row
1496
+ * or hand back the same one twice. Pass {@link PageWalkOptions.cursor} to
1497
+ * make seeking a requirement rather than a preference.
1110
1498
  *
1111
1499
  * @example
1112
1500
  * for await (const job of client.data.jobs.iterate({
@@ -1117,7 +1505,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1117
1505
  * await handle(job);
1118
1506
  * }
1119
1507
  */
1120
- iterate(params?: IterateParams<M>): AsyncIterableIterator<M>;
1508
+ iterate(params?: IterateParams<M, Inc>): AsyncIterableIterator<M>;
1121
1509
  /**
1122
1510
  * {@link iterate}, collected into an array.
1123
1511
  *
@@ -1128,7 +1516,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1128
1516
  * {@link FindAllParams.maxRows} when the data really is bigger, or switch to
1129
1517
  * `iterate()` and stream it.
1130
1518
  *
1131
- * The offset-drift caveat on {@link iterate} applies here too.
1519
+ * The consistency notes on {@link iterate} apply here too.
1132
1520
  *
1133
1521
  * @throws When more rows match than `maxRows` allows.
1134
1522
  *
@@ -1138,11 +1526,12 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1138
1526
  * cursor: "id"
1139
1527
  * });
1140
1528
  */
1141
- findAll(params?: FindAllParams<M>): Promise<M[]>;
1529
+ findAll(params?: FindAllParams<M, Inc>): Promise<M[]>;
1142
1530
  /**
1143
- * Find a single record by its ID.
1531
+ * Find a single record by its ID. `options.include` loads its relations,
1532
+ * as `find({ include })` does for each row of a list.
1144
1533
  */
1145
- findById(id: string | number): Promise<M | undefined>;
1534
+ findById(id: string | number, options?: ByIdReadOptions<Inc>): Promise<M | undefined>;
1146
1535
  /**
1147
1536
  * Read one record by its ID, or throw if it is not there.
1148
1537
  *
@@ -1183,8 +1572,11 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1183
1572
  * on anything else (a `sku`, a composite key), there is no `id` column to
1184
1573
  * receive it — put the key in `data` instead, where it belongs among the
1185
1574
  * columns.
1186
- * @returns The created row
1575
+ * @returns The created row — or nothing, with `returning: false`.
1187
1576
  */
1577
+ create(data: I, id: string | number | undefined, options: WriteOptions & {
1578
+ returning: false;
1579
+ }): Promise<M | undefined>;
1188
1580
  create(data: I, id?: string | number, options?: WriteOptions): Promise<M>;
1189
1581
  /**
1190
1582
  * Write many records in a single request and a single transaction.
@@ -1236,6 +1628,9 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1236
1628
  * that retry from a second deliberate edit — which on a `PATCH` that
1237
1629
  * increments or appends is a second edit applied.
1238
1630
  */
1631
+ update(id: string | number, data: U | UpdateValues<U>, options: WriteOptions & {
1632
+ returning: false;
1633
+ }): Promise<M | undefined>;
1239
1634
  update(id: string | number, data: U | UpdateValues<U>, options?: WriteOptions): Promise<M>;
1240
1635
  /**
1241
1636
  * Insert the row, or replace the one already occupying its key.
@@ -1253,6 +1648,10 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1253
1648
  * `unique` index. Anything else is a 400 rather than a Postgres error
1254
1649
  * raised half-way through a transaction.
1255
1650
  *
1651
+ * Refused (400 `INVALID_CONFLICT_TARGET`) on the auth collection, whose
1652
+ * rows are the users: a create and an update of a user follow different
1653
+ * rules, and an upsert only learns which it is inside the write.
1654
+ *
1256
1655
  * The `on_create` timestamp of a row that already existed is left alone: a
1257
1656
  * conflict means the row's creation is a fact about the past, and a nightly
1258
1657
  * re-import that reset `createdAt` on everything it touched would take
@@ -1260,12 +1659,15 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1260
1659
  *
1261
1660
  * @example
1262
1661
  * ```ts
1263
- * await client.data.users.upsert(
1264
- * { email: "a@b.c", name: "Ada" },
1265
- * { onConflict: ["email"] }
1662
+ * await client.data.products.upsert(
1663
+ * { sku: "LAMP-01", name: "Desk lamp" },
1664
+ * { onConflict: ["sku"] }
1266
1665
  * );
1267
1666
  * ```
1268
1667
  */
1668
+ upsert(data: I, options: UpsertOptions & {
1669
+ returning: false;
1670
+ }): Promise<M | undefined>;
1269
1671
  upsert(data: I, options?: UpsertOptions): Promise<M>;
1270
1672
  /**
1271
1673
  * Update many records in a single request and a single transaction.
@@ -1367,16 +1769,36 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1367
1769
  * `observe()` degrades to a single fetch instead of throwing, which is the
1368
1770
  * other reason to reach for it instead.
1369
1771
  */
1370
- listen(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
1371
- /** {@link listen} for a single row. Prefer `observeById()`. */
1372
- listenById(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
1772
+ listen(params: FindParams<M, Inc> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
1773
+ /**
1774
+ * {@link listen} for a single row. Prefer `observeById()`. Every frame is
1775
+ * the row as `findById(id, options)` reads it, relations included.
1776
+ */
1777
+ listenById(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void, options?: ByIdReadOptions<Inc>): () => void;
1778
+ /**
1779
+ * The number of rows matching `params`, kept current: `onCount` is called
1780
+ * with the total once it is known, and again whenever a write changes it.
1781
+ * The live counterpart of {@link count} — a list's total, or a "select all
1782
+ * N" offer, that does not go stale while other people add and remove rows.
1783
+ * Only the filter (`where`, `logical`, `searchString`) counts; the window
1784
+ * and sort are ignored.
1785
+ *
1786
+ * Always present; on a client that cannot subscribe it is a stub that
1787
+ * throws, as {@link listen} is.
1788
+ */
1789
+ listenCount(params: FindParams<M, Inc> | undefined, onCount: (total: number) => void, onError?: (error: Error) => void): () => void;
1790
+ /**
1791
+ * The collection's trash: list, count, restore and purge the rows a soft
1792
+ * delete stamped. See {@link TrashClient}.
1793
+ */
1794
+ trash: TrashClient<M, Inc>;
1373
1795
  /**
1374
1796
  * Count the number of records matching the given filter.
1375
1797
  *
1376
1798
  * Always present; see {@link listen} for what a transport that cannot serve
1377
1799
  * it does instead.
1378
1800
  */
1379
- count(params?: FindParams<M>): Promise<number>;
1801
+ count(params?: FindParams<M, Inc>): Promise<number>;
1380
1802
  /**
1381
1803
  * `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally
1382
1804
  * grouped — the SDK half of `GET /<collection>/aggregate`.
@@ -1401,33 +1823,33 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
1401
1823
  * naming the capability rather than an empty result set, which would read
1402
1824
  * as "nothing matched".
1403
1825
  */
1404
- aggregate(params: AggregateParams<M>): Promise<AggregateRow[]>;
1405
- where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;
1826
+ aggregate(params: AggregateParams<M>): Promise<AggregateResult>;
1827
+ where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M, Inc>;
1406
1828
  /** A relation path (`author.name`) or a JSON path (`metadata->>tier`). */
1407
- where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M>;
1408
- where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
1409
- orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc", nulls?: NullsPlacement): SDKQueryBuilderInterface<M>;
1410
- limit(count: number): SDKQueryBuilderInterface<M>;
1411
- offset(count: number): SDKQueryBuilderInterface<M>;
1829
+ where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M, Inc>;
1830
+ where(logicalCondition: LogicalConditionFor<M>): SDKQueryBuilderInterface<M, Inc>;
1831
+ orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc", nulls?: NullsPlacement): SDKQueryBuilderInterface<M, Inc>;
1832
+ limit(count: number): SDKQueryBuilderInterface<M, Inc>;
1833
+ offset(count: number): SDKQueryBuilderInterface<M, Inc>;
1412
1834
  search(searchString: string, options?: {
1413
1835
  explain?: boolean;
1414
- }): SDKQueryBuilderInterface<M>;
1836
+ }): SDKQueryBuilderInterface<M, Inc>;
1415
1837
  /**
1416
1838
  * Order rows by nearest-neighbour distance to `vector`, closest first.
1417
1839
  * Postgres only, over a `type: "vector"` property. See
1418
1840
  * {@link SDKQueryBuilderInterface.vectorSearch}.
1419
1841
  */
1420
- vectorSearch(property: string, vector: number[], options?: {
1842
+ vectorSearch(property: Extract<keyof M, string>, vector: number[], options?: {
1421
1843
  distance?: "cosine" | "l2" | "inner_product";
1422
1844
  threshold?: number;
1423
- }): SDKQueryBuilderInterface<M>;
1424
- include(...relations: (string | IncludeSpec)[]): SDKQueryBuilderInterface<M>;
1845
+ }): SDKQueryBuilderInterface<M, Inc>;
1846
+ include(...relations: IncludeArgOf<Inc>[]): SDKQueryBuilderInterface<M, Inc>;
1425
1847
  /** {@link SDKQueryBuilderInterface.fields} */
1426
- fields(...columns: (FieldPath<M> | string)[]): SDKQueryBuilderInterface<M>;
1848
+ fields(...columns: Extract<keyof M, string>[]): SDKQueryBuilderInterface<M, Inc>;
1427
1849
  /** {@link SDKQueryBuilderInterface.distinct} */
1428
- distinct(enabled?: boolean): SDKQueryBuilderInterface<M>;
1850
+ distinct(enabled?: boolean): SDKQueryBuilderInterface<M, Inc>;
1429
1851
  /** {@link SDKQueryBuilderInterface.after} */
1430
- after(cursor: string): SDKQueryBuilderInterface<M>;
1852
+ after(cursor: string): SDKQueryBuilderInterface<M, Inc>;
1431
1853
  }
1432
1854
  /**
1433
1855
  * The unified data access object for the **admin panel** (Entity-shaped).
@@ -1524,6 +1946,31 @@ export type InsertOf<T> = T extends {
1524
1946
  export type UpdateOf<T> = T extends {
1525
1947
  Update: infer U extends Record<string, unknown>;
1526
1948
  } ? U : Partial<RowOf<T>>;
1949
+ /**
1950
+ * The slugs a generated `Database` declares, from the `Slug` each entry
1951
+ * carries beside the accessor codegen chose for it.
1952
+ * @group Data
1953
+ */
1954
+ export type SlugOf<DB> = {
1955
+ [K in keyof DB]: DB[K] extends {
1956
+ Slug: infer S extends string;
1957
+ } ? S : never;
1958
+ }[keyof DB];
1959
+ /** The generated `Database` entry whose `Slug` is `S`. */
1960
+ type EntryOfSlug<DB, S extends string> = DB[{
1961
+ [K in keyof DB]: DB[K] extends {
1962
+ Slug: S;
1963
+ } ? K : never;
1964
+ }[keyof DB]];
1965
+ /**
1966
+ * `collection(slug)` for a slug the registered `Database` declares: the same
1967
+ * typed client as the slug's accessor (`collection("my-notes")` is
1968
+ * `myNotes`). A slug it does not declare, or one computed at runtime, is
1969
+ * served by the untyped overload.
1970
+ */
1971
+ type TypedSlugCollection<DB> = {
1972
+ collection<S extends SlugOf<DB>>(slug: S): SDKCollectionClient<RowOf<EntryOfSlug<DB, S>>, InsertOf<EntryOfSlug<DB, S>>, UpdateOf<EntryOfSlug<DB, S>>, IncludeOf<EntryOfSlug<DB, S>, DB>>;
1973
+ };
1527
1974
  /**
1528
1975
  * Note on the untyped branch below: its index signature is
1529
1976
  * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
@@ -1544,7 +1991,7 @@ export type UpdateOf<T> = T extends {
1544
1991
  * compile for any of them. Do not restore the arm; use `collection(slug)` if a
1545
1992
  * caller genuinely needs the by-slug function.
1546
1993
  */
1547
- export type RebaseSdkData<DB = unknown> = {
1994
+ export type RebaseSdkData<DB = unknown> = (unknown extends DB ? unknown : TypedSlugCollection<DB>) & {
1548
1995
  /**
1549
1996
  * Get a flat collection accessor by slug.
1550
1997
  *
@@ -1553,9 +2000,7 @@ export type RebaseSdkData<DB = unknown> = {
1553
2000
  * await accessor.find({ limit: 10 });
1554
2001
  */
1555
2002
  collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): SDKCollectionClient<M>;
1556
- } & (DB extends Record<string, unknown> ? {
1557
- [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>>;
1558
- } : {
2003
+ } & (unknown extends DB ? {
1559
2004
  /**
1560
2005
  * Dynamic flat collection accessor.
1561
2006
  * Access any collection by its slug as a property.
@@ -1564,4 +2009,7 @@ export type RebaseSdkData<DB = unknown> = {
1564
2009
  * data.products.find({ where: { status: ["==", "published"] } })
1565
2010
  */
1566
2011
  [collectionSlug: string]: SDKCollectionClient;
2012
+ } : {
2013
+ [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>, IncludeOf<DB[K], DB>>;
1567
2014
  });
2015
+ export {};