@rebasepro/types 0.9.1-canary.1d2d8b5 → 0.9.1-canary.58368ce

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.
@@ -149,6 +149,15 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
149
149
  * @returns The created entity
150
150
  */
151
151
  create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>>;
152
+ /**
153
+ * Create many records in a single transaction.
154
+ *
155
+ * See {@link SDKCollectionClient.createMany}. Optional: not every driver can
156
+ * write in bulk, and callers should fall back to `create` per record.
157
+ */
158
+ createMany?(data: Partial<EntityValues<M>>[], options?: {
159
+ upsert?: boolean;
160
+ }): Promise<Entity<M>[]>;
152
161
  /**
153
162
  * Update an existing record by ID.
154
163
  * @returns The updated entity
@@ -262,10 +271,43 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
262
271
  /**
263
272
  * Create a new record.
264
273
  * @param data The record data to create (the collection's `Insert` shape).
265
- * @param id Optional specific ID to use for the new record.
274
+ * @param id Optional specific id, sent as an `id` column. This is for tables
275
+ * whose key *is* `id`: the value goes in as that column. For a table keyed
276
+ * on anything else (a `sku`, a composite key), there is no `id` column to
277
+ * receive it — put the key in `data` instead, where it belongs among the
278
+ * columns.
266
279
  * @returns The created row
267
280
  */
268
281
  create(data: I, id?: string | number): Promise<M>;
282
+ /**
283
+ * Write many records in a single request and a single transaction.
284
+ *
285
+ * Built for imports and ETL, where one call per row means one HTTP round
286
+ * trip and one transaction per row. Every record still runs the normal
287
+ * pipeline — callbacks, relations, row-level security — and the batch is
288
+ * all-or-nothing: if any record is rejected, none of them land and the
289
+ * error names the offending index.
290
+ *
291
+ * A record carrying its primary key updates that row; one without inserts.
292
+ * With `{ upsert: true }` each record is written as INSERT ... ON CONFLICT
293
+ * DO UPDATE on the primary key instead, which is what makes a re-runnable
294
+ * import idempotent.
295
+ *
296
+ * Batches are capped server-side (1000 rows by default) because one batch
297
+ * holds its locks for the whole transaction — chunk larger jobs.
298
+ *
299
+ * @returns The written rows, in the order given.
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * for (const chunk of chunks(rows, 1000)) {
304
+ * await client.data.products.createMany(chunk, { upsert: true });
305
+ * }
306
+ * ```
307
+ */
308
+ createMany(data: I[], options?: {
309
+ upsert?: boolean;
310
+ }): Promise<M[]>;
269
311
  /**
270
312
  * Update an existing record by ID.
271
313
  * @param data The fields to update (the collection's `Update` shape).
@@ -345,7 +387,7 @@ export type RebaseData<DB = unknown> = {
345
387
  * - The frontend SDK client (`client.data.products.find()`)
346
388
  * - Backend framework callbacks & scripts (`context.data.products.find()`)
347
389
  *
348
- * Every accessor returns flat rows (`{ id, ...columns }`) via
390
+ * Every accessor returns flat rows (the table's columns) via
349
391
  * {@link SDKCollectionClient} — access fields directly (`row.title`), never
350
392
  * `row.values.title`. The admin CMS uses {@link RebaseData} (Entity) instead.
351
393
  *
@@ -65,6 +65,30 @@ export interface SaveProps<M extends Record<string, unknown> = Record<string, un
65
65
  previousValues?: Partial<EntityValues<M>>;
66
66
  collection?: CollectionConfig<M>;
67
67
  status: EntityStatus;
68
+ /**
69
+ * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key
70
+ * instead of choosing between insert and update up front.
71
+ *
72
+ * One statement, so it does not lose the race a read-then-write can, and it
73
+ * succeeds whether or not the row is already there — what a re-runnable
74
+ * import needs. Requires every primary key column to be present; without
75
+ * them there is no conflict target and the row is inserted normally.
76
+ */
77
+ upsert?: boolean;
78
+ }
79
+ /**
80
+ * @internal
81
+ */
82
+ export interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
83
+ path: string;
84
+ /**
85
+ * The rows to write. A row carrying its primary key updates (or, with
86
+ * `upsert`, inserts-or-updates) that row; one without inserts.
87
+ */
88
+ rows: Partial<EntityValues<M>>[];
89
+ collection?: CollectionConfig<M>;
90
+ /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */
91
+ upsert?: boolean;
68
92
  }
69
93
  /**
70
94
  * @internal
@@ -135,6 +159,19 @@ export interface DataDriver {
135
159
  * @param props
136
160
  */
137
161
  save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
162
+ /**
163
+ * Save many rows as one unit of work.
164
+ *
165
+ * Every row runs the same pipeline as {@link save} — callbacks, relations
166
+ * and row-level security all still apply — but they share a single
167
+ * transaction, so the batch either lands whole or not at all. That, and the
168
+ * single round trip, is what makes importing tens of thousands of rows
169
+ * viable without dropping to raw SQL.
170
+ *
171
+ * Optional: drivers that cannot do this leave it undefined and callers fall
172
+ * back to `save` per row.
173
+ */
174
+ saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
138
175
  /**
139
176
  * Delete a entity
140
177
  * @param props
@@ -203,9 +240,14 @@ export interface DataDriver {
203
240
  * REST-optimised fetch service exposed by drivers that support
204
241
  * eager-loading of relations via `include`.
205
242
  *
206
- * The methods return flattened rows (`{ id, ...columns }`) with
207
- * included relations inlined as plain nested rowsthe shape served
208
- * to app developers through the REST API / SDK client.
243
+ * The methods return flattened rows exactly the table's columns, under their
244
+ * own names and with the types the database returned and included relations
245
+ * inlined as plain nested rows. This is the shape served to app developers
246
+ * through the REST API / SDK client.
247
+ *
248
+ * No synthesized `id`: identity is a primary key, which may be named anything
249
+ * and span several columns, so an address is derived by whoever needs one (see
250
+ * `buildCompositeId`) rather than written into the row on top of the data.
209
251
  *
210
252
  * @group DataDriver
211
253
  */
@@ -74,7 +74,7 @@ export interface AfterReadProps<M extends Record<string, unknown> = Record<strin
74
74
  */
75
75
  path: string;
76
76
  /**
77
- * Fetched row (flat — `{ id, ...columns }`)
77
+ * Fetched row (flat — the table's columns)
78
78
  */
79
79
  row: Record<string, unknown>;
80
80
  /**
@@ -149,7 +149,7 @@ export interface BeforeDeleteProps<M extends Record<string, unknown> = Record<st
149
149
  */
150
150
  id: string | number;
151
151
  /**
152
- * Deleted row (flat — `{ id, ...columns }`)
152
+ * Deleted row (flat — the table's columns)
153
153
  */
154
154
  row: Record<string, unknown>;
155
155
  /**
@@ -175,7 +175,7 @@ export interface AfterDeleteProps<M extends Record<string, unknown> = Record<str
175
175
  */
176
176
  id: string | number;
177
177
  /**
178
- * Deleted row (flat — `{ id, ...columns }`)
178
+ * Deleted row (flat — the table's columns)
179
179
  */
180
180
  row: Record<string, unknown>;
181
181
  /**
@@ -15,10 +15,34 @@ export interface WebSocketMessage {
15
15
  row?: Record<string, unknown> | null;
16
16
  error?: string;
17
17
  }
18
+ /**
19
+ * The key columns a collection's rows are addressed by.
20
+ *
21
+ * A row is exactly its columns and carries no address, so a subscriber that has
22
+ * to recognise one — to patch it, or to keep its reference across a refetch —
23
+ * derives the address from these. The SDK is usable with no collections
24
+ * declared at all, so the server is the only side that knows them.
25
+ *
26
+ * Undefined when the server cannot resolve them: a table with no primary key
27
+ * and no `id` column has no address, and rows of it cannot be recognised by
28
+ * anyone.
29
+ */
30
+ export type WirePrimaryKeys = {
31
+ fieldName: string;
32
+ type: "string" | "number";
33
+ isUUID?: boolean;
34
+ }[];
18
35
  export interface CollectionUpdateMessage extends WebSocketMessage {
19
36
  type: "collection_update";
20
37
  subscriptionId: string;
21
38
  rows: Record<string, unknown>[];
39
+ /**
40
+ * See {@link WirePrimaryKeys}. Sent with the rows themselves — and not only
41
+ * with a patch — because a CDC-originated change sends no patch at all: it
42
+ * invalidates and goes straight to a refetch, and the merge that preserves
43
+ * unchanged rows' references needs an address to match them by.
44
+ */
45
+ pks?: WirePrimaryKeys;
22
46
  }
23
47
  export interface SingleUpdateMessage extends WebSocketMessage {
24
48
  type: "single_update";
@@ -34,9 +58,12 @@ export interface SingleUpdateMessage extends WebSocketMessage {
34
58
  export interface CollectionPatchMessage extends WebSocketMessage {
35
59
  type: "collection_patch";
36
60
  subscriptionId: string;
61
+ /** The address of the row this patch refers to — derived, never read off it. */
37
62
  id: string;
38
63
  /** The updated row, or null if deleted */
39
64
  row: Record<string, unknown> | null;
65
+ /** See {@link WirePrimaryKeys}: how the subscriber finds {@link id} in its cache. */
66
+ pks?: WirePrimaryKeys;
40
67
  }
41
68
  /**
42
69
  * Column metadata returned by table introspection.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.9.1-canary.1d2d8b5",
4
+ "version": "0.9.1-canary.58368ce",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -161,6 +161,14 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
161
161
  */
162
162
  create(data: Partial<EntityValues<M>>, id?: string | number): Promise<Entity<M>>;
163
163
 
164
+ /**
165
+ * Create many records in a single transaction.
166
+ *
167
+ * See {@link SDKCollectionClient.createMany}. Optional: not every driver can
168
+ * write in bulk, and callers should fall back to `create` per record.
169
+ */
170
+ createMany?(data: Partial<EntityValues<M>>[], options?: { upsert?: boolean }): Promise<Entity<M>[]>;
171
+
164
172
  /**
165
173
  * Update an existing record by ID.
166
174
  * @returns The updated entity
@@ -294,11 +302,43 @@ export interface SDKCollectionClient<
294
302
  /**
295
303
  * Create a new record.
296
304
  * @param data The record data to create (the collection's `Insert` shape).
297
- * @param id Optional specific ID to use for the new record.
305
+ * @param id Optional specific id, sent as an `id` column. This is for tables
306
+ * whose key *is* `id`: the value goes in as that column. For a table keyed
307
+ * on anything else (a `sku`, a composite key), there is no `id` column to
308
+ * receive it — put the key in `data` instead, where it belongs among the
309
+ * columns.
298
310
  * @returns The created row
299
311
  */
300
312
  create(data: I, id?: string | number): Promise<M>;
301
313
 
314
+ /**
315
+ * Write many records in a single request and a single transaction.
316
+ *
317
+ * Built for imports and ETL, where one call per row means one HTTP round
318
+ * trip and one transaction per row. Every record still runs the normal
319
+ * pipeline — callbacks, relations, row-level security — and the batch is
320
+ * all-or-nothing: if any record is rejected, none of them land and the
321
+ * error names the offending index.
322
+ *
323
+ * A record carrying its primary key updates that row; one without inserts.
324
+ * With `{ upsert: true }` each record is written as INSERT ... ON CONFLICT
325
+ * DO UPDATE on the primary key instead, which is what makes a re-runnable
326
+ * import idempotent.
327
+ *
328
+ * Batches are capped server-side (1000 rows by default) because one batch
329
+ * holds its locks for the whole transaction — chunk larger jobs.
330
+ *
331
+ * @returns The written rows, in the order given.
332
+ *
333
+ * @example
334
+ * ```ts
335
+ * for (const chunk of chunks(rows, 1000)) {
336
+ * await client.data.products.createMany(chunk, { upsert: true });
337
+ * }
338
+ * ```
339
+ */
340
+ createMany(data: I[], options?: { upsert?: boolean }): Promise<M[]>;
341
+
302
342
  /**
303
343
  * Update an existing record by ID.
304
344
  * @param data The fields to update (the collection's `Update` shape).
@@ -386,7 +426,7 @@ export type RebaseData<DB = unknown> = {
386
426
  * - The frontend SDK client (`client.data.products.find()`)
387
427
  * - Backend framework callbacks & scripts (`context.data.products.find()`)
388
428
  *
389
- * Every accessor returns flat rows (`{ id, ...columns }`) via
429
+ * Every accessor returns flat rows (the table's columns) via
390
430
  * {@link SDKCollectionClient} — access fields directly (`row.title`), never
391
431
  * `row.values.title`. The admin CMS uses {@link RebaseData} (Entity) instead.
392
432
  *
@@ -77,6 +77,31 @@ export interface SaveProps<M extends Record<string, unknown> = Record<string, un
77
77
  previousValues?: Partial<EntityValues<M>>;
78
78
  collection?: CollectionConfig<M>;
79
79
  status: EntityStatus;
80
+ /**
81
+ * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key
82
+ * instead of choosing between insert and update up front.
83
+ *
84
+ * One statement, so it does not lose the race a read-then-write can, and it
85
+ * succeeds whether or not the row is already there — what a re-runnable
86
+ * import needs. Requires every primary key column to be present; without
87
+ * them there is no conflict target and the row is inserted normally.
88
+ */
89
+ upsert?: boolean;
90
+ }
91
+
92
+ /**
93
+ * @internal
94
+ */
95
+ export interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
96
+ path: string;
97
+ /**
98
+ * The rows to write. A row carrying its primary key updates (or, with
99
+ * `upsert`, inserts-or-updates) that row; one without inserts.
100
+ */
101
+ rows: Partial<EntityValues<M>>[];
102
+ collection?: CollectionConfig<M>;
103
+ /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */
104
+ upsert?: boolean;
80
105
  }
81
106
 
82
107
  /**
@@ -154,6 +179,20 @@ export interface DataDriver {
154
179
  */
155
180
  save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;
156
181
 
182
+ /**
183
+ * Save many rows as one unit of work.
184
+ *
185
+ * Every row runs the same pipeline as {@link save} — callbacks, relations
186
+ * and row-level security all still apply — but they share a single
187
+ * transaction, so the batch either lands whole or not at all. That, and the
188
+ * single round trip, is what makes importing tens of thousands of rows
189
+ * viable without dropping to raw SQL.
190
+ *
191
+ * Optional: drivers that cannot do this leave it undefined and callers fall
192
+ * back to `save` per row.
193
+ */
194
+ saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
195
+
157
196
  /**
158
197
  * Delete a entity
159
198
  * @param props
@@ -252,9 +291,14 @@ export interface DataDriver {
252
291
  * REST-optimised fetch service exposed by drivers that support
253
292
  * eager-loading of relations via `include`.
254
293
  *
255
- * The methods return flattened rows (`{ id, ...columns }`) with
256
- * included relations inlined as plain nested rowsthe shape served
257
- * to app developers through the REST API / SDK client.
294
+ * The methods return flattened rows exactly the table's columns, under their
295
+ * own names and with the types the database returned and included relations
296
+ * inlined as plain nested rows. This is the shape served to app developers
297
+ * through the REST API / SDK client.
298
+ *
299
+ * No synthesized `id`: identity is a primary key, which may be named anything
300
+ * and span several columns, so an address is derived by whoever needs one (see
301
+ * `buildCompositeId`) rather than written into the row on top of the data.
258
302
  *
259
303
  * @group DataDriver
260
304
  */
@@ -91,7 +91,7 @@ export interface AfterReadProps<M extends Record<string, unknown> = Record<strin
91
91
  path: string;
92
92
 
93
93
  /**
94
- * Fetched row (flat — `{ id, ...columns }`)
94
+ * Fetched row (flat — the table's columns)
95
95
  */
96
96
  row: Record<string, unknown>
97
97
 
@@ -185,7 +185,7 @@ export interface BeforeDeleteProps<M extends Record<string, unknown> = Record<st
185
185
  id: string | number;
186
186
 
187
187
  /**
188
- * Deleted row (flat — `{ id, ...columns }`)
188
+ * Deleted row (flat — the table's columns)
189
189
  */
190
190
  row: Record<string, unknown>;
191
191
 
@@ -217,7 +217,7 @@ export interface AfterDeleteProps<M extends Record<string, unknown> = Record<str
217
217
  id: string | number;
218
218
 
219
219
  /**
220
- * Deleted row (flat — `{ id, ...columns }`)
220
+ * Deleted row (flat — the table's columns)
221
221
  */
222
222
  row: Record<string, unknown>;
223
223
 
@@ -14,10 +14,31 @@ export interface WebSocketMessage {
14
14
  error?: string;
15
15
  }
16
16
 
17
+ /**
18
+ * The key columns a collection's rows are addressed by.
19
+ *
20
+ * A row is exactly its columns and carries no address, so a subscriber that has
21
+ * to recognise one — to patch it, or to keep its reference across a refetch —
22
+ * derives the address from these. The SDK is usable with no collections
23
+ * declared at all, so the server is the only side that knows them.
24
+ *
25
+ * Undefined when the server cannot resolve them: a table with no primary key
26
+ * and no `id` column has no address, and rows of it cannot be recognised by
27
+ * anyone.
28
+ */
29
+ export type WirePrimaryKeys = { fieldName: string; type: "string" | "number"; isUUID?: boolean }[];
30
+
17
31
  export interface CollectionUpdateMessage extends WebSocketMessage {
18
32
  type: "collection_update";
19
33
  subscriptionId: string;
20
34
  rows: Record<string, unknown>[];
35
+ /**
36
+ * See {@link WirePrimaryKeys}. Sent with the rows themselves — and not only
37
+ * with a patch — because a CDC-originated change sends no patch at all: it
38
+ * invalidates and goes straight to a refetch, and the merge that preserves
39
+ * unchanged rows' references needs an address to match them by.
40
+ */
41
+ pks?: WirePrimaryKeys;
21
42
  }
22
43
 
23
44
  export interface SingleUpdateMessage extends WebSocketMessage {
@@ -35,9 +56,12 @@ export interface SingleUpdateMessage extends WebSocketMessage {
35
56
  export interface CollectionPatchMessage extends WebSocketMessage {
36
57
  type: "collection_patch";
37
58
  subscriptionId: string;
59
+ /** The address of the row this patch refers to — derived, never read off it. */
38
60
  id: string;
39
61
  /** The updated row, or null if deleted */
40
62
  row: Record<string, unknown> | null;
63
+ /** See {@link WirePrimaryKeys}: how the subscriber finds {@link id} in its cache. */
64
+ pks?: WirePrimaryKeys;
41
65
  }
42
66
 
43
67
  /**