@rebasepro/types 0.19.1 → 0.19.2-canary.g09316f6

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, OrderBySpec, RelationAggregateSort } from "../types/filter-operators.js";
4
+ import { WhereFilterOp, FieldPath, NonColumnFieldPath, FilterValues, NullsPlacement, OrderBySpec, 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
  *
@@ -54,8 +54,24 @@ export type WhereElementOf<T> = ElementOf<T> | IdOf<ElementOf<T>>;
54
54
  * permissive as it was.
55
55
  */
56
56
  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;
57
+ /**
58
+ * A group of conditions combined with `and`, `or`, or negated with `not`.
59
+ *
60
+ * ## `not`
61
+ *
62
+ * `not` negates the **conjunction** of its `conditions`: `not(a)` is `NOT a`,
63
+ * and `not(a, b)` is `NOT (a AND b)`. One rule, stated here and applied
64
+ * identically by the wire codec (`or(...)`/`and(...)`/`not(...)` in
65
+ * `@rebasepro/common`), the REST `?not=` parameter and every driver compiler,
66
+ * so a negation means the same thing whichever end writes it.
67
+ *
68
+ * Negation is not expressible by inverting the operators inside the group: SQL
69
+ * three-valued logic makes `NOT (a AND b)` and `(NOT a) OR (NOT b)` differ the
70
+ * moment a NULL is involved, and only one of them is what the caller wrote. It
71
+ * compiles to a real `NOT (...)`.
72
+ */
57
73
  export interface LogicalCondition {
58
- type: "and" | "or";
74
+ type: "and" | "or" | "not";
59
75
  conditions: (FilterCondition | LogicalCondition)[];
60
76
  }
61
77
  export interface FilterCondition {
@@ -63,6 +79,76 @@ export interface FilterCondition {
63
79
  operator: WhereFilterOp;
64
80
  value: unknown;
65
81
  }
82
+ /**
83
+ * How one relation is loaded by {@link FindParams.include}.
84
+ *
85
+ * `true` loads the relation whole. The object form narrows it — the same four
86
+ * knobs a top-level query has, applied to the rows *inside* one relation — and
87
+ * `include` nests, so a query can ask for "each post's five newest published
88
+ * comments, each with its author" in one request.
89
+ *
90
+ * ```ts
91
+ * include: {
92
+ * comments: {
93
+ * limit: 5,
94
+ * where: { published: ["==", true] },
95
+ * orderBy: ["created_at", "desc"],
96
+ * include: { author: true }
97
+ * }
98
+ * }
99
+ * ```
100
+ *
101
+ * Nesting is bounded at {@link MAX_INCLUDE_DEPTH} hops. Each hop is another
102
+ * batched query, and the bound is what stops one request from walking a
103
+ * self-referencing relation forever.
104
+ *
105
+ * @group Data
106
+ */
107
+ export interface IncludeOptions {
108
+ /** Rows to load per parent row. Applied per parent, not across the page. */
109
+ limit?: number;
110
+ /** Filter the related rows, in the same dialect as {@link FindParams.where}. */
111
+ where?: FilterValues<string>;
112
+ /** An `and`/`or`/`not` group over the related rows. */
113
+ logical?: LogicalCondition;
114
+ /**
115
+ * Sort the related rows — the tuple form, or the `field:direction[:nulls]`
116
+ * shorthand the REST `?orderBy=` parameter uses.
117
+ *
118
+ * The string is accepted because this whole object travels over a query
119
+ * string, where a tuple is three characters of JSON heavier for no gain.
120
+ */
121
+ orderBy?: OrderBySpec<string> | string;
122
+ /** Columns of the *related* row to return. `id` is always included. */
123
+ fields?: string[];
124
+ /** Relations of the related row to load in turn. */
125
+ include?: IncludeSpec;
126
+ }
127
+ /**
128
+ * The relations a read loads, as a list of (possibly dotted) names or as a
129
+ * tree.
130
+ *
131
+ * - `["author", "comments.author"]` — a dotted path is the same thing as the
132
+ * nested object form, spelled flat. It is what the REST `?include=` parameter
133
+ * carries, and the two forms compile to the same request.
134
+ * - `["*"]` — every relation, one hop deep. The admin panel's shape.
135
+ * - `{ comments: { limit: 5, include: { author: true } } }` — the parametrised
136
+ * form.
137
+ *
138
+ * A name that is not a relation of the collection is a **400
139
+ * `UNKNOWN_RELATION`**, not a silent omission: a read that quietly drops an
140
+ * `include` answers 200 with the field missing, which is indistinguishable from
141
+ * a row that genuinely has no related row.
142
+ *
143
+ * @group Data
144
+ */
145
+ export type IncludeSpec = string[] | Record<string, true | IncludeOptions>;
146
+ /**
147
+ * Hops an {@link IncludeSpec} may nest. `comments.author` is two.
148
+ *
149
+ * @group Data
150
+ */
151
+ export declare const MAX_INCLUDE_DEPTH = 3;
66
152
  /**
67
153
  * Parameters for querying a collection.
68
154
  *
@@ -145,13 +231,59 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
145
231
  */
146
232
  orderBy?: OrderBySpec<FieldPath<M> | ComputedSortField>;
147
233
  /**
148
- * Relations to include in the response.
234
+ * Relations to load into the response — see {@link IncludeSpec}.
235
+ *
236
+ * Not checked against `M` here: a relation name comes from the collection's
237
+ * `relations`, not from its columns, so nothing in a *hand-written* row type
238
+ * can validate one. A **generated** `Database` narrows this to the
239
+ * collection's actual relation keys, recursively — see `rebase codegen`.
240
+ *
241
+ * An unknown name is a 400 `UNKNOWN_RELATION`. It used to be ignored.
242
+ */
243
+ include?: IncludeSpec;
244
+ /**
245
+ * Columns to return, instead of all of them.
246
+ *
247
+ * A real column projection: only these columns are read from the database,
248
+ * so a query that needs two fields of a wide row does not pay for the rest.
249
+ * `excludeFromApi` still applies — naming such a column here does not
250
+ * un-hide it — and the primary key is always returned, because a row that
251
+ * cannot be addressed cannot be updated, deleted or paged past.
252
+ *
253
+ * A relation named in {@link FindParams.include} is loaded regardless of
254
+ * whether it appears here; use {@link IncludeOptions.fields} to narrow the
255
+ * columns *within* an included relation.
256
+ */
257
+ fields?: string[];
258
+ /**
259
+ * Collapse rows that are identical over the columns being returned.
260
+ *
261
+ * `SELECT DISTINCT` over the projection — so it is only meaningful
262
+ * alongside {@link FindParams.fields}, and with the primary key in the
263
+ * projection (which it always is) every row is already distinct. Pair it
264
+ * with `fields` naming the columns you actually want the distinct values of.
149
265
  *
150
- * Deliberately `string[]` and not checked against `M`: a relation name
151
- * comes from the collection's `relations`, not from its columns, so nothing
152
- * in a generated row type can validate one.
266
+ * `meta.total` counts distinct rows too, so a distinct listing's `hasMore`
267
+ * describes the set it is paging.
153
268
  */
154
- include?: string[];
269
+ distinct?: boolean;
270
+ /**
271
+ * Continue from where a previous page ended — keyset ("seek") pagination.
272
+ *
273
+ * The value is the opaque `meta.nextCursor` of the previous response. It
274
+ * encodes the sort keys the query was ordered by and the last row's values
275
+ * for them, so a page picks up strictly after the last row served rather
276
+ * than at a row *count* that concurrent writes have already moved.
277
+ *
278
+ * It has to describe the same query: `after` alongside a different
279
+ * `orderBy` is a 400 `CURSOR_ORDER_MISMATCH` rather than a page of rows
280
+ * seeked in an order nobody asked for. Mutually exclusive with `offset` and
281
+ * `page` for the same reason.
282
+ *
283
+ * Multi-key sorts and nullable keys both work — the comparison is built
284
+ * over every key, in order, with the NULL placement the sort declared.
285
+ */
286
+ after?: string;
155
287
  /**
156
288
  * Text search string, AND-ed with `where`/`logical`. This is the value
157
289
  * behind the query builder's `.search()` method.
@@ -279,6 +411,7 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
279
411
  */
280
412
  createMany?(data: Partial<EntityValues<M>>[], options?: {
281
413
  upsert?: boolean;
414
+ onConflict?: readonly string[];
282
415
  }): Promise<Entity<M>[]>;
283
416
  /**
284
417
  * Update an existing record by ID.
@@ -324,6 +457,13 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
324
457
  * one place the two halves of this API are not interchangeable.
325
458
  */
326
459
  count?(params?: FindParams<M>): Promise<number>;
460
+ /**
461
+ * {@link SDKCollectionClient.aggregate}. Optional here for the same reason
462
+ * `count` is: not every data source can compute one, and the SDK wraps an
463
+ * absent implementation in a stub that says so rather than returning a
464
+ * number nothing counted.
465
+ */
466
+ aggregate?(params: AggregateParams<M>): Promise<AggregateRow[]>;
327
467
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): QueryBuilderInterface<M>;
328
468
  where(logicalCondition: LogicalCondition): QueryBuilderInterface<M>;
329
469
  orderBy(column: (keyof M & string) | ComputedSortField, direction?: "asc" | "desc"): QueryBuilderInterface<M>;
@@ -356,6 +496,20 @@ export interface PaginationMeta {
356
496
  limit: number;
357
497
  offset: number;
358
498
  hasMore: boolean;
499
+ /**
500
+ * The opaque cursor that continues this listing — pass it back as
501
+ * {@link FindParams.after}.
502
+ *
503
+ * Present whenever there is a next page to describe (`hasMore` is true and
504
+ * the page returned at least one row). Absent on the last page, and absent
505
+ * on a query no cursor can describe (relevance ordering, whose scores are
506
+ * computed per query and are not comparable between two of them).
507
+ *
508
+ * Opaque on purpose: it encodes the sort keys *and* the last row's values
509
+ * for them, and a client that parsed it would be depending on an encoding
510
+ * that exists to be changed.
511
+ */
512
+ nextCursor?: string;
359
513
  }
360
514
  /**
361
515
  * Paginated response from a collection query (SDK-facing).
@@ -416,6 +570,66 @@ export type QueryComputedFields = {
416
570
  */
417
571
  _distance?: number;
418
572
  };
573
+ /**
574
+ * One aggregate a query asks for.
575
+ *
576
+ * `count` alone counts rows; every other function names a column, and `count`
577
+ * with a column counts its non-NULL values.
578
+ *
579
+ * The result key is derived rather than chosen: `sum(total)` comes back as
580
+ * `sum_total` and a bare `count()` as `count`. Letting a caller name it would
581
+ * mean checking their name is not also a `groupBy` field — a rule nobody would
582
+ * guess, and a silently overwritten value if it went unchecked.
583
+ *
584
+ * @group Data
585
+ */
586
+ export type AggregateSelect<M extends Record<string, unknown> = Record<string, unknown>> = {
587
+ fn: "count";
588
+ field?: Extract<keyof M, string>;
589
+ } | {
590
+ fn: "sum" | "avg" | "min" | "max";
591
+ field: Extract<keyof M, string>;
592
+ };
593
+ /**
594
+ * One row of an aggregate result: the `groupBy` columns, plus one key per
595
+ * {@link AggregateSelect} under its derived alias.
596
+ *
597
+ * `count`, `sum` and `avg` arrive as numbers — Postgres returns bigint and
598
+ * numeric as strings, and they are parsed once at the driver rather than by
599
+ * every caller. `min`/`max` keep the column's own type.
600
+ *
601
+ * @group Data
602
+ */
603
+ export type AggregateRow = Record<string, unknown>;
604
+ /**
605
+ * What {@link SDKCollectionClient.aggregate} takes: the same narrowing a
606
+ * `find()` takes, minus the parts of it that describe a *page* of rows.
607
+ *
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.
612
+ *
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.
616
+ *
617
+ * @group Data
618
+ */
619
+ export interface AggregateParams<M extends Record<string, unknown> = Record<string, unknown>> {
620
+ /** The aggregates to compute. At least one. */
621
+ select: AggregateSelect<M>[];
622
+ /** Columns to group by. Omit for a single row over everything that matches. */
623
+ groupBy?: Extract<keyof M, string>[];
624
+ /** Filter conditions, as {@link FindParams.where}. */
625
+ where?: FilterValues<FieldPath<M>>;
626
+ /** An `and`/`or`/`not` group, AND-ed with `where`. */
627
+ logical?: LogicalCondition;
628
+ /** Text search, AND-ed with the filters. */
629
+ searchString?: string;
630
+ /** Most groups to return. Ignored without `groupBy`. */
631
+ limit?: number;
632
+ }
419
633
  /**
420
634
  * Which column an iteration seeks on, for keyset ("seek") pagination.
421
635
  *
@@ -524,7 +738,7 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
524
738
  * Sort by a column, a relation or JSON path, `_score`, or an aggregate over
525
739
  * a to-many relation — the same key set {@link FindParams.orderBy} takes.
526
740
  */
527
- orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc"): this;
741
+ orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc", nulls?: NullsPlacement): this;
528
742
  limit(count: number): this;
529
743
  offset(count: number): this;
530
744
  search(searchString: string, options?: {
@@ -543,8 +757,29 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
543
757
  distance?: "cosine" | "l2" | "inner_product";
544
758
  threshold?: number;
545
759
  }): this;
546
- include(...relations: string[]): this;
760
+ /**
761
+ * Load relations — names, dotted paths (`"comments.author"`), or the
762
+ * parametrised tree. Repeated calls merge rather than replace.
763
+ */
764
+ include(...relations: (string | IncludeSpec)[]): this;
765
+ /**
766
+ * Return only these columns. A real projection: the columns are what is
767
+ * read from the database, not what survives a trim of the response.
768
+ */
769
+ fields(...columns: (FieldPath<M> | string)[]): this;
770
+ /** `SELECT DISTINCT` over the projection — see {@link FindParams.distinct}. */
771
+ distinct(enabled?: boolean): this;
772
+ /** Continue after a previous page's `meta.nextCursor`. */
773
+ after(cursor: string): this;
547
774
  find(): Promise<FindResult<M>>;
775
+ /**
776
+ * Aggregate the rows this query matches instead of returning them.
777
+ *
778
+ * The builder's `where`/`logical`/`search` narrow which rows are
779
+ * aggregated; its `orderBy`, `include` and window do not apply and are
780
+ * ignored, exactly as they are on the REST route.
781
+ */
782
+ aggregate(params: Omit<AggregateParams<M>, "where" | "logical" | "searchString">): Promise<AggregateRow[]>;
548
783
  /**
549
784
  * Page through everything this query matches, one row at a time.
550
785
  *
@@ -591,6 +826,173 @@ export interface SDKQueryBuilderInterface<M extends Record<string, unknown> = Re
591
826
  *
592
827
  * @group Data
593
828
  */
829
+ /**
830
+ * A change expressed as an operation on the column's current value, rather than
831
+ * as the value to store.
832
+ *
833
+ * `{ views: 5 }` says what the number becomes; `{ views: { $inc: 1 } }` says
834
+ * what happens to it. The difference is the read the caller no longer has to
835
+ * make — and the race that read opens. Two requests that each read `4`, add one
836
+ * and write `5` lose an increment between them; `SET views = views + 1` cannot,
837
+ * because the arithmetic happens inside the statement holding the row lock.
838
+ *
839
+ * Exactly one operator per field. `{ views: { $inc: 1, $push: "x" } }` is
840
+ * refused rather than applied in an order the caller cannot see.
841
+ *
842
+ * @group Data
843
+ */
844
+ /**
845
+ * The operator names, as a value.
846
+ *
847
+ * A runtime list beside the type because three layers have to *recognise* an
848
+ * operation, not just accept one: the REST validator, the driver that compiles
849
+ * it, and the offline queue that must refuse to apply one locally. Three copies
850
+ * of four strings is three chances for one of them to miss an operator added to
851
+ * the other two, and the failure is silent in the worst direction — an
852
+ * unrecognised marker is written to the column as a JSON document.
853
+ *
854
+ * @group Data
855
+ */
856
+ export declare const FIELD_OPERATORS: readonly ["$inc", "$push", "$pull", "$merge"];
857
+ /**
858
+ * The key of a {@link BatchRef}. Declared here, beside the field operators,
859
+ * because the two share one namespace: a `$`-prefixed key in a write payload is
860
+ * a marker, and every reader of that namespace has to know all of it.
861
+ *
862
+ * @group Data
863
+ */
864
+ export declare const BATCH_REF_KEY = "$ref";
865
+ /**
866
+ * Whether a value is *trying* to be a field operation — including a misspelled
867
+ * one, which is the case worth catching.
868
+ *
869
+ * 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.
874
+ *
875
+ * The one exception is `{ $ref: … }`, the batch's backward reference. It stands
876
+ * where a *value* goes and is resolved to one before the row is written, so it
877
+ * is not an operation on a column — reading it as a misspelled operator refused
878
+ * every `$ref` in a batch with "unknown field operator '$ref'".
879
+ *
880
+ * @group Data
881
+ */
882
+ export declare function isFieldOperation(value: unknown): boolean;
883
+ /** True when any value in a write payload is (or is attempting to be) one. @group Data */
884
+ export declare function hasFieldOperation(values: Record<string, unknown> | undefined): boolean;
885
+ export type FieldOperation =
886
+ /** Add to a `number` column; negative to subtract. `SET col = col + n`. */
887
+ {
888
+ $inc: number;
889
+ }
890
+ /** Append one value, or each of an array of values, to an `array` column. */
891
+ | {
892
+ $push: unknown;
893
+ }
894
+ /** Remove every occurrence of a value from an `array` column. */
895
+ | {
896
+ $pull: unknown;
897
+ }
898
+ /** Shallow-merge an object into a `map` column. `SET col = col || …::jsonb`. */
899
+ | {
900
+ $merge: Record<string, unknown>;
901
+ };
902
+ /**
903
+ * The payload {@link SDKCollectionClient.update} accepts: plain values, field
904
+ * operations, or both in one body.
905
+ *
906
+ * @group Data
907
+ */
908
+ export type UpdateValues<U> = {
909
+ [K in keyof U]?: U[K] | FieldOperation;
910
+ };
911
+ /**
912
+ * Where an upsert looks for the row it might be replacing.
913
+ *
914
+ * The columns must carry a uniqueness guarantee the database can use as an
915
+ * `ON CONFLICT` target — the primary key, a property with
916
+ * `validation.unique`, or the columns of a declared `unique` index. Anything
917
+ * else is refused with a 400 rather than sent to Postgres, which would answer
918
+ * `there is no unique or exclusion constraint matching the ON CONFLICT
919
+ * specification` from inside a transaction that has already done work.
920
+ *
921
+ * @group Data
922
+ */
923
+ export interface UpsertOptions extends WriteOptions {
924
+ /** Column names forming the conflict target. Defaults to the primary key. */
925
+ onConflict?: readonly string[];
926
+ }
927
+ /**
928
+ * A placeholder standing for a value only the server will know: the id of a row
929
+ * an earlier operation in the same batch creates.
930
+ *
931
+ * `{ "$ref": "order.id" }` reads the field `id` off the result of the operation
932
+ * that named itself `ref: "order"`. Without it a batch cannot express the one
933
+ * thing a cross-collection batch exists for — writing a parent and its children
934
+ * together — because the child's foreign key is not knowable until the parent
935
+ * has been inserted, and splitting the two into separate requests is exactly
936
+ * the non-atomic sequence the batch replaces.
937
+ *
938
+ * Only backward references resolve. `ref` names must be unique within a batch,
939
+ * and an operation may not reference itself or anything after it.
940
+ *
941
+ * @group Data
942
+ */
943
+ export interface BatchRef {
944
+ /** `<ref name>.<field>`, e.g. `order.id`. */
945
+ $ref: string;
946
+ }
947
+ /** One entry of a batch request. @group Data */
948
+ export type BatchOperation<DB = Record<string, unknown>> = {
949
+ [K in Extract<keyof DB, string>]: {
950
+ op: "create";
951
+ collection: K;
952
+ values: {
953
+ [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef;
954
+ } & Record<string, unknown>;
955
+ /** Name this row so a later operation can reference its columns. */
956
+ ref?: string;
957
+ } | {
958
+ op: "upsert";
959
+ collection: K;
960
+ values: {
961
+ [F in keyof InsertOf<DB[K]>]?: InsertOf<DB[K]>[F] | BatchRef;
962
+ } & Record<string, unknown>;
963
+ /** See {@link UpsertOptions.onConflict}. Defaults to the primary key. */
964
+ onConflict?: readonly string[];
965
+ ref?: string;
966
+ } | {
967
+ op: "update";
968
+ collection: K;
969
+ id: string | number | BatchRef;
970
+ values: {
971
+ [F in keyof UpdateOf<DB[K]>]?: UpdateOf<DB[K]>[F] | FieldOperation | BatchRef;
972
+ } & Record<string, unknown>;
973
+ ref?: string;
974
+ } | {
975
+ op: "delete";
976
+ collection: K;
977
+ id: string | number | BatchRef;
978
+ ref?: string;
979
+ };
980
+ }[Extract<keyof DB, string>];
981
+ /**
982
+ * What `POST /api/data/_batch` answers with.
983
+ *
984
+ * `data` is aligned to `operations`: the written row for a create, upsert or
985
+ * update, and `null` for a delete — so an index into one is an index into the
986
+ * other, whatever the batch mixed.
987
+ *
988
+ * @group Data
989
+ */
990
+ export interface BatchResult<R = Record<string, unknown>> {
991
+ data: (R | null)[];
992
+ meta: {
993
+ operations: number;
994
+ };
995
+ }
594
996
  /**
595
997
  * Per-request options for a write.
596
998
  * @group Data
@@ -621,6 +1023,37 @@ export interface WriteOptions {
621
1023
  * header rather than refusing the write.
622
1024
  */
623
1025
  idempotencyKey?: string;
1026
+ /**
1027
+ * Whether the server should send the written row back.
1028
+ *
1029
+ * `false` sends `Prefer: return=minimal`, and the write answers `204 No
1030
+ * Content` — `200` carrying the ids only, for a batch. The row is the
1031
+ * default because it carries what the server decided: a serial id, an
1032
+ * `autoValue` timestamp, whatever `beforeSave` rewrote. A caller that
1033
+ * needs none of that is paying for a full row serialisation and, on
1034
+ * Postgres, a read-back per written row.
1035
+ *
1036
+ * Reach for it on imports and fire-and-forget writes. The method resolves
1037
+ * to `undefined` (or `[]`) when it is set, so a caller cannot accidentally
1038
+ * use a row the server never sent.
1039
+ */
1040
+ returning?: boolean;
1041
+ /**
1042
+ * The version of the row this write was made against, so it is refused if
1043
+ * the row has moved on.
1044
+ *
1045
+ * The `ETag` from the read that produced the row — `etagOf(row)` on a row
1046
+ * from `findById`, or the `ETag` response header. A mismatch answers `412`
1047
+ * rather than writing, which is the difference between "update the row I
1048
+ * read" and "overwrite whatever is there now". Without it a read, an edit
1049
+ * and a write is last-writer-wins over everything the write did not send,
1050
+ * and the loser is told nothing.
1051
+ *
1052
+ * `"*"` asserts only that the row exists.
1053
+ *
1054
+ * Honoured on `update` and `delete`.
1055
+ */
1056
+ ifMatch?: string;
624
1057
  }
625
1058
  export interface SDKCollectionClient<M extends Record<string, unknown> = Record<string, unknown>, I = Partial<M>, U = Partial<M>> {
626
1059
  /**
@@ -787,6 +1220,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
787
1220
  */
788
1221
  createMany(data: I[], options?: {
789
1222
  upsert?: boolean;
1223
+ onConflict?: readonly string[];
790
1224
  } & WriteOptions): Promise<M[]>;
791
1225
  /**
792
1226
  * Update an existing record by ID.
@@ -802,7 +1236,37 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
802
1236
  * that retry from a second deliberate edit — which on a `PATCH` that
803
1237
  * increments or appends is a second edit applied.
804
1238
  */
805
- update(id: string | number, data: U, options?: WriteOptions): Promise<M>;
1239
+ update(id: string | number, data: U | UpdateValues<U>, options?: WriteOptions): Promise<M>;
1240
+ /**
1241
+ * Insert the row, or replace the one already occupying its key.
1242
+ *
1243
+ * `INSERT ... ON CONFLICT DO UPDATE`, in one statement — so unlike a
1244
+ * `findById` followed by `create`-or-`update` it cannot lose the race
1245
+ * between the two, and unlike `create` it does not fail when the row is
1246
+ * already there. That is what makes a re-runnable import idempotent
1247
+ * without a key.
1248
+ *
1249
+ * The conflict target defaults to the primary key. Pass `onConflict` to
1250
+ * upsert on a natural key instead — `["email"]`, `["tenant_id", "slug"]` —
1251
+ * and the columns must carry a uniqueness guarantee the database can use:
1252
+ * a property with `validation.unique`, or the columns of a declared
1253
+ * `unique` index. Anything else is a 400 rather than a Postgres error
1254
+ * raised half-way through a transaction.
1255
+ *
1256
+ * The `on_create` timestamp of a row that already existed is left alone: a
1257
+ * conflict means the row's creation is a fact about the past, and a nightly
1258
+ * re-import that reset `createdAt` on everything it touched would take
1259
+ * every "new this week" query with it.
1260
+ *
1261
+ * @example
1262
+ * ```ts
1263
+ * await client.data.users.upsert(
1264
+ * { email: "a@b.c", name: "Ada" },
1265
+ * { onConflict: ["email"] }
1266
+ * );
1267
+ * ```
1268
+ */
1269
+ upsert(data: I, options?: UpsertOptions): Promise<M>;
806
1270
  /**
807
1271
  * Update many records in a single request and a single transaction.
808
1272
  *
@@ -843,13 +1307,19 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
843
1307
  */
844
1308
  updateMany(updates: {
845
1309
  id: string | number;
846
- data: U;
1310
+ data: U | UpdateValues<U>;
847
1311
  }[], options?: WriteOptions): Promise<M[]>;
848
1312
  /**
849
1313
  * Delete a record by ID.
850
1314
  * @throws {RebaseApiError} with status 404 when the record does not exist.
1315
+ *
1316
+ * Takes {@link WriteOptions} like every other write. It did not, so the one
1317
+ * mutation that cannot be made safe by repeating it — a delete replayed
1318
+ * after the row is gone answers 404, which an offline queue reads as a
1319
+ * permanent failure — was also the one that could not carry an
1320
+ * `idempotencyKey`.
851
1321
  */
852
- delete(id: string | number): Promise<void>;
1322
+ delete(id: string | number, options?: WriteOptions): Promise<void>;
853
1323
  /**
854
1324
  * Delete many records in a single request and a single transaction.
855
1325
  *
@@ -907,11 +1377,36 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
907
1377
  * it does instead.
908
1378
  */
909
1379
  count(params?: FindParams<M>): Promise<number>;
1380
+ /**
1381
+ * `count`/`sum`/`avg`/`min`/`max` over the matching rows, optionally
1382
+ * grouped — the SDK half of `GET /<collection>/aggregate`.
1383
+ *
1384
+ * The whole point is not to fetch rows in order to reduce them: "revenue by
1385
+ * status" over a million orders is one query and one row per status here,
1386
+ * and a `findAll()` plus a loop everywhere else — which is wrong under a
1387
+ * `limit` and unaffordable without one. It runs through the same
1388
+ * request-scoped handle as every other read, so RLS applies to the rows
1389
+ * being aggregated.
1390
+ *
1391
+ * ```ts
1392
+ * const rows = await rebase.data.orders.aggregate({
1393
+ * select: [{ fn: "sum", field: "total" }, { fn: "count" }],
1394
+ * groupBy: ["status"],
1395
+ * where: { created_at: [">=", startOfMonth] }
1396
+ * });
1397
+ * // [{ status: "paid", sum_total: 41822.5, count: 317 }, …]
1398
+ * ```
1399
+ *
1400
+ * Always present; a backend whose driver cannot aggregate answers 501
1401
+ * naming the capability rather than an empty result set, which would read
1402
+ * as "nothing matched".
1403
+ */
1404
+ aggregate(params: AggregateParams<M>): Promise<AggregateRow[]>;
910
1405
  where<K extends keyof M & string, Op extends WhereFilterOp>(column: K, operator: Op, value: WhereValueFor<Op, M[K]>): SDKQueryBuilderInterface<M>;
911
1406
  /** A relation path (`author.name`) or a JSON path (`metadata->>tier`). */
912
1407
  where(column: NonColumnFieldPath, operator: WhereFilterOp, value: unknown): SDKQueryBuilderInterface<M>;
913
1408
  where(logicalCondition: LogicalCondition): SDKQueryBuilderInterface<M>;
914
- orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc"): SDKQueryBuilderInterface<M>;
1409
+ orderBy(column: FieldPath<M> | ComputedSortField | RelationAggregateSort, direction?: "asc" | "desc", nulls?: NullsPlacement): SDKQueryBuilderInterface<M>;
915
1410
  limit(count: number): SDKQueryBuilderInterface<M>;
916
1411
  offset(count: number): SDKQueryBuilderInterface<M>;
917
1412
  search(searchString: string, options?: {
@@ -926,7 +1421,13 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
926
1421
  distance?: "cosine" | "l2" | "inner_product";
927
1422
  threshold?: number;
928
1423
  }): SDKQueryBuilderInterface<M>;
929
- include(...relations: string[]): SDKQueryBuilderInterface<M>;
1424
+ include(...relations: (string | IncludeSpec)[]): SDKQueryBuilderInterface<M>;
1425
+ /** {@link SDKQueryBuilderInterface.fields} */
1426
+ fields(...columns: (FieldPath<M> | string)[]): SDKQueryBuilderInterface<M>;
1427
+ /** {@link SDKQueryBuilderInterface.distinct} */
1428
+ distinct(enabled?: boolean): SDKQueryBuilderInterface<M>;
1429
+ /** {@link SDKQueryBuilderInterface.after} */
1430
+ after(cursor: string): SDKQueryBuilderInterface<M>;
930
1431
  }
931
1432
  /**
932
1433
  * The unified data access object for the **admin panel** (Entity-shaped).