@rebasepro/types 0.24.0 → 0.24.1-canary.gcb918fc

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,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,20 @@ 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
+ }
146
253
  /**
147
254
  * An include checked against one generated collection's relations: `R` is its
148
255
  * `Relations` map (relation name → the accessor it reaches) and `DB` the
@@ -212,10 +319,12 @@ export declare const MAX_INCLUDE_DEPTH = 3;
212
319
  *
213
320
  * ## Pagination precedence
214
321
  *
215
- * `limit`/`offset` and `page` describe the same window two ways. If **both
216
- * `offset` and `page` are provided, `page` wins** — the backend computes
217
- * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
218
- * 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`).
219
328
  *
220
329
  * @group Data
221
330
  */
@@ -234,13 +343,14 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
234
343
  */
235
344
  limit?: number;
236
345
  /**
237
- * Number of items to skip. Ignored when {@link FindParams.page} is also
238
- * set — `page` takes precedence.
346
+ * Number of items to skip. Not with {@link FindParams.page}: the pair is
347
+ * refused with `PAGE_WITH_OFFSET`.
239
348
  */
240
349
  offset?: number;
241
350
  /**
242
- * Page number (1-indexed), alternative to {@link FindParams.offset}.
243
- * 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`.
244
354
  */
245
355
  page?: number;
246
356
  /**
@@ -256,13 +366,13 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
256
366
  * { role: ["in", ["admin", "editor"]] }
257
367
  * { age: [[">=", 18], ["<", 65]] }
258
368
  */
259
- where?: FilterValues<FieldPath<M>>;
369
+ where?: WhereFor<M>;
260
370
  /**
261
371
  * Logical grouping conditions (AND/OR). Use this for anything `where`
262
372
  * can't express — notably OR-ing conditions. AND-ed with `where` and
263
373
  * `searchString` when present (see the interface docs).
264
374
  */
265
- logical?: LogicalCondition;
375
+ logical?: LogicalConditionFor<M>;
266
376
  /**
267
377
  * Sort order as a `[field, direction]` tuple, or a list of them applied in
268
378
  * order of significance — the second key breaks ties on the first, and so on.
@@ -307,8 +417,10 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
307
417
  * projection (which it always is) every row is already distinct. Pair it
308
418
  * with `fields` naming the columns you actually want the distinct values of.
309
419
  *
310
- * `meta.total` counts distinct rows too, so a distinct listing's `hasMore`
311
- * 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.
312
424
  */
313
425
  distinct?: boolean;
314
426
  /**
@@ -368,6 +480,23 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
368
480
  * carries a `searchString`; there is nothing to explain otherwise.
369
481
  */
370
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";
371
500
  }
372
501
  /**
373
502
  * Paginated response from a collection query.
@@ -396,7 +525,7 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
396
525
  */
397
526
  export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
398
527
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): this;
399
- where(logicalCondition: LogicalCondition): this;
528
+ where(logicalCondition: LogicalConditionFor<M>): this;
400
529
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): this;
401
530
  limit(count: number): this;
402
531
  offset(count: number): this;
@@ -439,7 +568,7 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
439
568
  /**
440
569
  * Find a single record by its ID.
441
570
  */
442
- findById(id: string | number): Promise<Entity<M> | undefined>;
571
+ findById(id: string | number, options?: ByIdReadOptions): Promise<Entity<M> | undefined>;
443
572
  /**
444
573
  * Create a new record.
445
574
  * @param data The entity data to create.
@@ -459,9 +588,16 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
459
588
  }): Promise<Entity<M>[]>;
460
589
  /**
461
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.
462
596
  * @returns The updated entity
463
597
  */
464
- 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>>;
465
601
  /**
466
602
  * Update many records in a single transaction.
467
603
  *
@@ -487,10 +623,23 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
487
623
  */
488
624
  listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
489
625
  /**
490
- * 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.
491
628
  * Optional method.
492
629
  */
493
- 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>;
494
643
  /**
495
644
  * Count the number of records matching the given filter.
496
645
  *
@@ -509,7 +658,7 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
509
658
  */
510
659
  aggregate?(params: AggregateParams<M>): Promise<AggregateResult>;
511
660
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;
512
- where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
661
+ where(logicalCondition: LogicalConditionFor<M>): QueryBuilderInterface<M>;
513
662
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
514
663
  limit(count: number): QueryBuilderInterface<M>;
515
664
  offset(count: number): QueryBuilderInterface<M>;
@@ -536,6 +685,11 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
536
685
  * @group Data
537
686
  */
538
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
+ */
539
693
  total: number;
540
694
  limit: number;
541
695
  offset: number;
@@ -631,9 +785,26 @@ export type AggregateSelect<M extends Record<string, unknown> = Record<string, u
631
785
  fn: "count";
632
786
  field?: Extract<keyof M, string>;
633
787
  } | {
634
- fn: "sum" | "avg" | "min" | "max";
788
+ fn: "sum" | "avg";
789
+ field: NumericKeyOf<M>;
790
+ } | {
791
+ fn: "min" | "max";
635
792
  field: Extract<keyof M, string>;
636
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>;
637
808
  /**
638
809
  * One row of an aggregate result: the `groupBy` columns, plus one key per
639
810
  * {@link AggregateSelect} under its derived alias.
@@ -667,16 +838,48 @@ export interface AggregateParams<M extends Record<string, unknown> = Record<stri
667
838
  /** Columns to group by. Omit for a single row over everything that matches. */
668
839
  groupBy?: Extract<keyof M, string>[];
669
840
  /** Filter conditions, as {@link FindParams.where}. */
670
- where?: FilterValues<FieldPath<M>>;
841
+ where?: WhereFor<M>;
671
842
  /** An `and`/`or`/`not` group, AND-ed with `where`. */
672
- logical?: LogicalCondition;
843
+ logical?: LogicalConditionFor<M>;
673
844
  /** Text search, AND-ed with the filters. */
674
845
  searchString?: string;
675
846
  /** Most groups to return. Grouped aggregates only. */
676
847
  limit?: number;
677
848
  /** Groups to skip, to page past `limit`. Grouped aggregates only. */
678
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>>[];
679
868
  }
869
+ /**
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>;
680
883
  /**
681
884
  * Where a grouped aggregate's page of groups sits: the `limit` it was cut at,
682
885
  * the `offset` it started from, and whether more groups follow.
@@ -704,11 +907,12 @@ export interface AggregateResult extends Array<AggregateRow> {
704
907
  readonly meta?: AggregateMeta;
705
908
  }
706
909
  /**
707
- * Which column an iteration seeks on, for keyset ("seek") pagination.
910
+ * Which column an iteration sorts by when it is asked to seek, for keyset
911
+ * ("seek") pagination.
708
912
  *
709
913
  * Either the column name on its own — sorted ascending — or the column plus an
710
- * explicit direction. The column must be **unique** and must be the column the
711
- * 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}.
712
916
  *
713
917
  * @group Data
714
918
  */
@@ -730,23 +934,23 @@ export interface PageWalkOptions<M extends Record<string, unknown> = Record<stri
730
934
  */
731
935
  pageSize?: number;
732
936
  /**
733
- * Paginate by **seeking on a column** instead of by offset.
937
+ * **Require** seeking: fail rather than fall back to offset paging.
734
938
  *
735
- * Offset paging — the default — re-counts rows on every request, so a row
736
- * inserted or deleted *while the iteration runs* shifts the window and the
737
- * walk silently skips or repeats rows. Seeking is immune to that: each page
738
- * asks for rows strictly after the last one seen, so concurrent writes
739
- * 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.
740
947
  *
741
- * Prefer this whenever the collection has a unique, sortable column
742
- * (typically its primary key). The column must be unique — a repeated value
743
- * at a page boundary either skips rows or stalls, and the iterator throws
744
- * rather than looping — and the query is ordered by it, so a `cursor` and a
745
- * conflicting `orderBy` is an error, not a silent override.
746
- *
747
- * Implemented with the parameters `find()` already takes (an `orderBy` plus
748
- * a `>` / `<` filter on the cursor column), so it works on every transport
749
- * 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.
750
954
  *
751
955
  * @example
752
956
  * for await (const job of client.data.jobs.iterate({ cursor: "id" })) { … }
@@ -764,6 +968,9 @@ export interface PageWalkOptions<M extends Record<string, unknown> = Record<stri
764
968
  * `find()` takes except the window itself (`limit`, `offset`, `page`), which
765
969
  * the iterator owns, plus the walk options.
766
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
+ *
767
974
  * @group Data
768
975
  */
769
976
  export type IterateParams<M extends Record<string, unknown> = Record<string, unknown>, Inc = IncludeSpec> = Omit<FindParams<M, Inc>, "limit" | "offset" | "page"> & PageWalkOptions<M>;
@@ -806,7 +1013,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
806
1013
  * errors on a typed client.
807
1014
  */
808
1015
  where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): this;
809
- where(logicalCondition: LogicalCondition): this;
1016
+ where(logicalCondition: LogicalConditionFor<M>): this;
810
1017
  /**
811
1018
  * Sort by a column, a relation or JSON path, `_score`, or an aggregate over
812
1019
  * a to-many relation — the same key set {@link FindParams.orderBy} takes.
@@ -826,7 +1033,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
826
1033
  *
827
1034
  * The query embedding is the caller's to produce.
828
1035
  */
829
- vectorSearch(property: string, vector: number[], options?: {
1036
+ vectorSearch(property: Extract<keyof M, string>, vector: number[], options?: {
830
1037
  distance?: "cosine" | "l2" | "inner_product";
831
1038
  threshold?: number;
832
1039
  }): this;
@@ -839,7 +1046,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
839
1046
  * Return only these columns. A real projection: the columns are what is
840
1047
  * read from the database, not what survives a trim of the response.
841
1048
  */
842
- fields(...columns: (FieldPath<M> | string)[]): this;
1049
+ fields(...columns: Extract<keyof M, string>[]): this;
843
1050
  /** `SELECT DISTINCT` over the projection — see {@link FindParams.distinct}. */
844
1051
  distinct(enabled?: boolean): this;
845
1052
  /** Continue after a previous page's `meta.nextCursor`. */
@@ -941,10 +1148,13 @@ export declare const BATCH_REF_KEY = "$ref";
941
1148
  * one, which is the case worth catching.
942
1149
  *
943
1150
  * Any `$`-prefixed key counts, because `{ $increment: 1 }` written to a number
944
- * column as a JSON document is the failure this exists to prevent. No collection
945
- * can declare a column whose value legitimately has a key beginning with `$`: a
946
- * `map` property's sub-keys are declared, and `$` is not valid in the
947
- * 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.
948
1158
  *
949
1159
  * The one exception is `{ $ref: … }`, the batch's backward reference. It stands
950
1160
  * where a *value* goes and is resolved to one before the row is written, so it
@@ -953,7 +1163,7 @@ export declare const BATCH_REF_KEY = "$ref";
953
1163
  *
954
1164
  * @group Data
955
1165
  */
956
- export declare function isFieldOperation(value: unknown): boolean;
1166
+ export declare function isFieldOperation(value: unknown, propertyType?: string): boolean;
957
1167
  /** True when any value in a write payload is (or is attempting to be) one. @group Data */
958
1168
  export declare function hasFieldOperation(values: Record<string, unknown> | undefined): boolean;
959
1169
  export type FieldOperation =
@@ -980,8 +1190,23 @@ export type FieldOperation =
980
1190
  * @group Data
981
1191
  */
982
1192
  export type UpdateValues<U> = {
983
- [K in keyof U]?: U[K] | FieldOperation;
1193
+ [K in keyof U]?: U[K] | FieldOperationFor<U[K]>;
984
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);
985
1210
  /**
986
1211
  * Where an upsert looks for the row it might be replacing.
987
1212
  *
@@ -1060,7 +1285,7 @@ export type BatchOperation<DB = Record<string, unknown>> = {
1060
1285
  collection: K;
1061
1286
  id: string | number | BatchRef;
1062
1287
  values: {
1063
- [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperation | BatchRef;
1288
+ [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperationFor<UpdateOf<DB[K]>[F]> | BatchRef;
1064
1289
  };
1065
1290
  ref?: string;
1066
1291
  } | {
@@ -1155,6 +1380,66 @@ export interface WriteOptions {
1155
1380
  */
1156
1381
  ifMatch?: string;
1157
1382
  }
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
+ }
1158
1443
  export interface SDKCollectionClient<M extends Record<string, unknown> = Record<string, unknown>, I = Partial<M>, U = Partial<M>,
1159
1444
  /** The include this collection takes; {@link IncludeOf} for a generated `Database`. */
1160
1445
  Inc = IncludeSpec> {
@@ -1201,14 +1486,15 @@ Inc = IncludeSpec> {
1201
1486
  *
1202
1487
  * ## Consistency
1203
1488
  *
1204
- * By default this pages by **offset**, which is only as stable as the table
1205
- * is still: a row inserted or deleted ahead of the cursor between two
1206
- * requests shifts every later window, so the walk can skip a row or hand
1207
- * back the same one twice. That is inherent to offset paging, not a bug
1208
- * here. On a collection with a unique sortable column, pass
1209
- * {@link PageWalkOptions.cursor} to seek on it instead — the walk then
1210
- * asks for rows strictly after the last one it saw, which concurrent writes
1211
- * 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.
1212
1498
  *
1213
1499
  * @example
1214
1500
  * for await (const job of client.data.jobs.iterate({
@@ -1230,7 +1516,7 @@ Inc = IncludeSpec> {
1230
1516
  * {@link FindAllParams.maxRows} when the data really is bigger, or switch to
1231
1517
  * `iterate()` and stream it.
1232
1518
  *
1233
- * The offset-drift caveat on {@link iterate} applies here too.
1519
+ * The consistency notes on {@link iterate} apply here too.
1234
1520
  *
1235
1521
  * @throws When more rows match than `maxRows` allows.
1236
1522
  *
@@ -1242,9 +1528,10 @@ Inc = IncludeSpec> {
1242
1528
  */
1243
1529
  findAll(params?: FindAllParams<M, Inc>): Promise<M[]>;
1244
1530
  /**
1245
- * 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.
1246
1533
  */
1247
- findById(id: string | number): Promise<M | undefined>;
1534
+ findById(id: string | number, options?: ByIdReadOptions<Inc>): Promise<M | undefined>;
1248
1535
  /**
1249
1536
  * Read one record by its ID, or throw if it is not there.
1250
1537
  *
@@ -1361,6 +1648,10 @@ Inc = IncludeSpec> {
1361
1648
  * `unique` index. Anything else is a 400 rather than a Postgres error
1362
1649
  * raised half-way through a transaction.
1363
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
+ *
1364
1655
  * The `on_create` timestamp of a row that already existed is left alone: a
1365
1656
  * conflict means the row's creation is a fact about the past, and a nightly
1366
1657
  * re-import that reset `createdAt` on everything it touched would take
@@ -1368,9 +1659,9 @@ Inc = IncludeSpec> {
1368
1659
  *
1369
1660
  * @example
1370
1661
  * ```ts
1371
- * await client.data.users.upsert(
1372
- * { email: "a@b.c", name: "Ada" },
1373
- * { onConflict: ["email"] }
1662
+ * await client.data.products.upsert(
1663
+ * { sku: "LAMP-01", name: "Desk lamp" },
1664
+ * { onConflict: ["sku"] }
1374
1665
  * );
1375
1666
  * ```
1376
1667
  */
@@ -1479,8 +1770,28 @@ Inc = IncludeSpec> {
1479
1770
  * other reason to reach for it instead.
1480
1771
  */
1481
1772
  listen(params: FindParams<M, Inc> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
1482
- /** {@link listen} for a single row. Prefer `observeById()`. */
1483
- listenById(id: string | number, onUpdate: (row: M | undefined) => 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>;
1484
1795
  /**
1485
1796
  * Count the number of records matching the given filter.
1486
1797
  *
@@ -1516,7 +1827,7 @@ Inc = IncludeSpec> {
1516
1827
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M, Inc>;
1517
1828
  /** A relation path (`author.name`) or a JSON path (`metadata->>tier`). */
1518
1829
  where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M, Inc>;
1519
- where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M, Inc>;
1830
+ where(logicalCondition: LogicalConditionFor<M>): SDKQueryBuilderInterface<M, Inc>;
1520
1831
  orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc", nulls?: NullsPlacement): SDKQueryBuilderInterface<M, Inc>;
1521
1832
  limit(count: number): SDKQueryBuilderInterface<M, Inc>;
1522
1833
  offset(count: number): SDKQueryBuilderInterface<M, Inc>;
@@ -1528,13 +1839,13 @@ Inc = IncludeSpec> {
1528
1839
  * Postgres only, over a `type: "vector"` property. See
1529
1840
  * {@link SDKQueryBuilderInterface.vectorSearch}.
1530
1841
  */
1531
- vectorSearch(property: string, vector: number[], options?: {
1842
+ vectorSearch(property: Extract<keyof M, string>, vector: number[], options?: {
1532
1843
  distance?: "cosine" | "l2" | "inner_product";
1533
1844
  threshold?: number;
1534
1845
  }): SDKQueryBuilderInterface<M, Inc>;
1535
1846
  include(...relations: IncludeArgOf<Inc>[]): SDKQueryBuilderInterface<M, Inc>;
1536
1847
  /** {@link SDKQueryBuilderInterface.fields} */
1537
- fields(...columns: (FieldPath<M> | string)[]): SDKQueryBuilderInterface<M, Inc>;
1848
+ fields(...columns: Extract<keyof M, string>[]): SDKQueryBuilderInterface<M, Inc>;
1538
1849
  /** {@link SDKQueryBuilderInterface.distinct} */
1539
1850
  distinct(enabled?: boolean): SDKQueryBuilderInterface<M, Inc>;
1540
1851
  /** {@link SDKQueryBuilderInterface.after} */
@@ -1635,6 +1946,31 @@ export type InsertOf<T> = T extends {
1635
1946
  export type UpdateOf<T> = T extends {
1636
1947
  Update: infer U extends Record<string, unknown>;
1637
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
+ };
1638
1974
  /**
1639
1975
  * Note on the untyped branch below: its index signature is
1640
1976
  * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
@@ -1655,7 +1991,7 @@ export type UpdateOf<T> = T extends {
1655
1991
  * compile for any of them. Do not restore the arm; use `collection(slug)` if a
1656
1992
  * caller genuinely needs the by-slug function.
1657
1993
  */
1658
- export type RebaseSdkData<DB = unknown> = {
1994
+ export type RebaseSdkData<DB = unknown> = (unknown extends DB ? unknown : TypedSlugCollection<DB>) & {
1659
1995
  /**
1660
1996
  * Get a flat collection accessor by slug.
1661
1997
  *
@@ -1664,9 +2000,7 @@ export type RebaseSdkData<DB = unknown> = {
1664
2000
  * await accessor.find({ limit: 10 });
1665
2001
  */
1666
2002
  collection<M extends Record<string, unknown> = Record<string, unknown>>(slug: string): SDKCollectionClient<M>;
1667
- } & (DB extends Record<string, unknown> ? {
1668
- [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>, IncludeOf<DB[K], DB>>;
1669
- } : {
2003
+ } & (unknown extends DB ? {
1670
2004
  /**
1671
2005
  * Dynamic flat collection accessor.
1672
2006
  * Access any collection by its slug as a property.
@@ -1675,5 +2009,7 @@ export type RebaseSdkData<DB = unknown> = {
1675
2009
  * data.products.find({ where: { status: ["==", "published"] } })
1676
2010
  */
1677
2011
  [collectionSlug: string]: SDKCollectionClient;
2012
+ } : {
2013
+ [K in keyof DB]: SDKCollectionClient<RowOf<DB[K]>, InsertOf<DB[K]>, UpdateOf<DB[K]>, IncludeOf<DB[K], DB>>;
1678
2014
  });
1679
2015
  export {};