@rebasepro/types 0.13.0 → 0.13.1-canary.g18cfeb7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import type { DataDriver } from "./controllers/data_driver";
1
2
  import type { StorageSource } from "./controllers/storage";
2
3
  import type { RebaseClient } from "./controllers/client";
3
4
  import type { RebaseSdkData } from "./controllers/data";
@@ -19,7 +20,14 @@ export type RebaseCallContext<USER extends User = User> = {
19
20
  * The Rebase client instance.
20
21
  * Available in all entity callbacks (beforeSave, afterSave, afterRead,
21
22
  * beforeDelete, afterDelete) and in CollectionActionsProps via context.
22
- * Use it to call backend functions, access data, storage, etc.
23
+ * Use it to call backend functions, access storage, send email, etc.
24
+ *
25
+ * ⚠️ **Not the same trust level as {@link data}.** Server-side this is the
26
+ * app singleton, so `client.dataAsAdmin` is **always** the RLS-bypassing
27
+ * plane — while {@link data}, one property over, follows whoever triggered
28
+ * the callback. On a user request, reaching for `context.client.dataAsAdmin`
29
+ * silently escalates a user-scoped operation to admin. For queries in a
30
+ * callback use {@link data}; come here for functions, storage and email.
23
31
  *
24
32
  * @example
25
33
  * // In a beforeSave callback:
@@ -35,11 +43,60 @@ export type RebaseCallContext<USER extends User = User> = {
35
43
  * Unified data access — `context.data.products.create(...)`.
36
44
  * Access any collection as a dynamic property.
37
45
  *
38
- * Returns flat rows (`{ id, ...columns }`), identical to the frontend SDK
39
- * client so `context.data` in a backend callback and `client.data` in the
40
- * frontend behave the same way (`row.title`, never `row.values.title`).
46
+ * **Inherits the privilege of whatever triggered the callback.** This is not
47
+ * a fixed trust level, and it is the one thing to know about this accessor:
48
+ *
49
+ * - Triggered by a **user request** (REST, realtime, an admin-panel edit):
50
+ * user-scoped. The callback runs on the RLS-bound transaction opened for
51
+ * that request, so policies apply to reads *and* writes — a callback
52
+ * cannot see a row its caller could not.
53
+ * - Triggered by **server-context work** (`rebase.dataAsAdmin`, a cron):
54
+ * unscoped, on the owner connection, bypassing RLS.
55
+ *
56
+ * So a callback that reads a sibling row will find it when an admin task
57
+ * saves and may find nothing when an end user saves — without an error,
58
+ * because RLS filters rather than raises. Write callbacks that tolerate
59
+ * that, or reach for {@link client}`.dataAsAdmin` deliberately when the
60
+ * callback genuinely has to see past its caller.
61
+ *
62
+ * Verified end-to-end against Postgres rather than asserted — see
63
+ * `"scopes context.data to the caller when a callback runs on a user
64
+ * request"` in `server-postgres`' `rls-enforcement` e2e suite. The
65
+ * documentation previously claimed the opposite (that callbacks always have
66
+ * full access), which is the unsafe direction to be wrong in.
67
+ *
68
+ * Returns flat rows (`{ id, ...columns }`), identical in *shape* to the
69
+ * frontend SDK client — so `context.data` in a backend callback and
70
+ * `client.data` in the frontend are accessed the same way (`row.title`,
71
+ * never `row.values.title`). Shape only: privilege differs as above.
41
72
  */
42
73
  data: RebaseSdkData;
74
+ /**
75
+ * The driver executing the operation this callback is attached to.
76
+ *
77
+ * Present server-side only. Declared here because it is already public in
78
+ * practice — the backend has always passed it, and the callbacks guide
79
+ * documented `context.driver.withAuth(user)` in all six locales. The
80
+ * contract simply did not name it, so `buildCallContext` was cast through
81
+ * `as unknown as RebaseCallContext` and nothing about the object was
82
+ * type-checked at all.
83
+ *
84
+ * The guide no longer recommends `withAuth` — {@link data} is already
85
+ * user-scoped on a user request, so the manual re-scoping it described was
86
+ * answering a problem that did not exist. The field stays declared rather
87
+ * than removed: it is on the runtime object, dropping it would break anyone
88
+ * who found it, and a named optional is better than a silent extra.
89
+ *
90
+ * `withAuth` is not on {@link DataDriver} because not every engine supports
91
+ * RLS scoping; it is narrowed here, and left optional so a driver without it
92
+ * is a compile-time absence rather than a runtime surprise.
93
+ */
94
+ driver?: DataDriver & {
95
+ withAuth?(user: {
96
+ uid: string;
97
+ roles?: string[];
98
+ }): Promise<DataDriver>;
99
+ };
43
100
  /**
44
101
  * Used storage implementation
45
102
  */
@@ -371,7 +371,22 @@ export interface RebaseClient<DB = unknown> {
371
371
  setOnUnauthorized?(handler: () => Promise<boolean>): void;
372
372
  /** Resolve the current auth token */
373
373
  resolveToken?(): Promise<string | null>;
374
- /** Make a raw HTTP call to the backend */
374
+ /**
375
+ * POST to an arbitrary path on the backend — the escape hatch, not the way
376
+ * to call a function.
377
+ *
378
+ * For a custom function use {@link functions}`.invoke(name, payload)`: it
379
+ * targets `/functions/<name>`, takes a method and sub-path, and returns the
380
+ * response body as sent. This posts wherever you point it and **unwraps**:
381
+ * it returns `res.data` when the response has a `data` property and the
382
+ * whole envelope otherwise — so an endpoint that legitimately answers
383
+ * `{ data: null }` hands back the envelope rather than `null`. Two ways to
384
+ * reach a function with two different response contracts is a trap; this is
385
+ * the one that exists for paths `invoke` cannot express.
386
+ *
387
+ * @internal Prefer `functions.invoke()`. Kept public because a backend can
388
+ * mount routes outside `/functions`, and nothing else reaches those.
389
+ */
375
390
  call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
376
391
  /**
377
392
  * Execute raw SQL against the database.
@@ -419,64 +434,6 @@ export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>,
419
434
  params?: unknown[];
420
435
  }): Promise<Record<string, unknown>[]>;
421
436
  }
422
- /**
423
- * The browser-side Rebase surface — the shape produced by
424
- * `createRebaseClient()` in `@rebasepro/client`.
425
- *
426
- * Its {@link data} accessor is **user-scoped**: every call carries the signed-in
427
- * user's token, so backend RLS policies apply. It deliberately omits the
428
- * server-only members — there is no `sql`, no `email`, and no
429
- * `dataAsAdmin`, so the RLS-bypassing accessor can never be reached from
430
- * browser code.
431
- */
432
- export interface RebaseBrowserClient<DB = unknown> {
433
- /** User-scoped data access layer (carries the signed-in user's token). */
434
- data: RebaseSdkData<DB>;
435
- /** Unified Authentication layer */
436
- auth: AuthClient;
437
- /** Unified Storage layer (default storage source, backward-compatible) */
438
- storage?: StorageSource;
439
- /** Registry of all named storage sources for multi-backend support */
440
- storageRegistry?: StorageSourceRegistry;
441
- /** Build a server-backed {@link StorageSource} for a named storage source. */
442
- createStorageSource?(storageId: string): StorageSource;
443
- /** Discover the storage sources declared on the backend. */
444
- fetchStorageSources?(): Promise<StorageSourceDefinition[]>;
445
- /** Admin API for user management */
446
- admin?: AdminAPI;
447
- /** Cron job management API */
448
- cron?: CronAPI;
449
- /** Database backup management API */
450
- backups?: BackupsAPI;
451
- /** Custom backend functions API */
452
- functions?: FunctionsAPI;
453
- /** Service API keys management API */
454
- apiKeys?: ApiKeysAPI;
455
- /** Base HTTP URL of the backend server */
456
- baseUrl?: string;
457
- /**
458
- * The path every API route is mounted under, appended to {@link baseUrl}.
459
- *
460
- * `"/api"` unless the backend was configured with a different `basePath`
461
- * and the client told to match. Exposed because code that builds a URL by
462
- * hand — rather than going through the client's own methods — otherwise has
463
- * to guess, and guessing `/api` is wrong for exactly the projects that set
464
- * the option.
465
- */
466
- apiPath?: string;
467
- /** WebSocket client for realtime subscriptions */
468
- ws?: RebaseWebSocket;
469
- /** Set the auth token for subsequent requests */
470
- setToken?(token: string | null): void;
471
- /** Set a function that lazily resolves the auth token */
472
- setAuthTokenGetter?(getter: () => Promise<string | null>): void;
473
- /** Set handler called when a request returns 401 */
474
- setOnUnauthorized?(handler: () => Promise<boolean>): void;
475
- /** Resolve the current auth token */
476
- resolveToken?(): Promise<string | null>;
477
- /** Make a raw HTTP call to the backend */
478
- call?<T = unknown>(endpoint: string, payload?: unknown): Promise<T>;
479
- }
480
437
  /**
481
438
  * Client-side registry for managing multiple storage sources.
482
439
  *
@@ -34,13 +34,19 @@ export interface FilterCondition {
34
34
  *
35
35
  * `limit`/`offset` and `page` describe the same window two ways. If **both
36
36
  * `offset` and `page` are provided, `page` wins** — the backend computes
37
- * `offset = (page - 1) * (limit ?? 20)` and ignores the explicit `offset`.
38
- * Pick one style per query.
37
+ * `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
38
+ * explicit `offset`. Pick one style per query.
39
39
  *
40
40
  * @group Data
41
41
  */
42
42
  export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
43
- /** Maximum number of items to return (default: 20). */
43
+ /**
44
+ * Maximum number of items to return.
45
+ *
46
+ * Defaults to {@link DEFAULT_LIST_LIMIT}, and is clamped to
47
+ * {@link MAX_LIST_LIMIT}. Both bounds are applied by the backend, so a
48
+ * read is never unbounded whether or not a limit was asked for.
49
+ */
44
50
  limit?: number;
45
51
  /**
46
52
  * Number of items to skip. Ignored when {@link FindParams.page} is also
@@ -49,7 +55,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
49
55
  offset?: number;
50
56
  /**
51
57
  * Page number (1-indexed), alternative to {@link FindParams.offset}.
52
- * When set, overrides `offset` as `(page - 1) * (limit ?? 20)`.
58
+ * When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
53
59
  */
54
60
  page?: number;
55
61
  /**
@@ -108,12 +114,12 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
108
114
  };
109
115
  }
110
116
  /**
111
- * Fluent query builder for the **admin admin** — resolves to `FindResponse<M>`
117
+ * Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`
112
118
  * (Snapshot-wrapped rows).
113
119
  *
114
120
  * @internal App developers should use {@link SDKQueryBuilderInterface}
115
121
  * (flat rows, returned by `client.data.*` / `context.data.*`). This
116
- * Snapshot-flavored variant backs the admin admin internals only.
122
+ * Snapshot-flavored variant backs the admin panel internals only.
117
123
  *
118
124
  * @group Data
119
125
  */
@@ -129,13 +135,13 @@ export interface QueryBuilderInterface<M extends Record<string, unknown> = Recor
129
135
  listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
130
136
  }
131
137
  /**
132
- * A single collection's CRUD accessor for the **admin admin** — every method
138
+ * A single collection's CRUD accessor for the **admin panel** — every method
133
139
  * resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
134
140
  *
135
141
  * @internal App developers do **not** use this. The public, symmetric surface
136
142
  * is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
137
143
  * in the SDK and `context.data.products` in framework callbacks. This
138
- * Snapshot-flavored accessor backs the admin admin view-model only.
144
+ * Snapshot-flavored accessor backs the admin panel view-model only.
139
145
  *
140
146
  * @group Data
141
147
  */
@@ -169,6 +175,21 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
169
175
  * @returns The updated entity
170
176
  */
171
177
  update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
178
+ /**
179
+ * Update many records in a single transaction.
180
+ *
181
+ * See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.
182
+ */
183
+ updateMany?(updates: {
184
+ id: string | number;
185
+ data: Partial<EntityValues<M>>;
186
+ }[]): Promise<Entity<M>[]>;
187
+ /**
188
+ * Delete many records in a single transaction.
189
+ *
190
+ * See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.
191
+ */
192
+ deleteMany?(ids: (string | number)[]): Promise<void>;
172
193
  /**
173
194
  * Delete a record by ID.
174
195
  */
@@ -185,6 +206,12 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
185
206
  listenById?(id: string | number, onUpdate: (entity: Entity<M> | undefined) => void, onError?: (error: Error) => void): () => void;
186
207
  /**
187
208
  * Count the number of records matching the given filter.
209
+ *
210
+ * Optional on this contract because a data source need not support it, and
211
+ * required on `CollectionClient` — the HTTP implementation always has it.
212
+ * So `client.data.posts.count()` compiles in the browser while the same
213
+ * call through a `context.data` accessor needs `count?.()`, which is the
214
+ * one place the two halves of this API are not interchangeable.
188
215
  */
189
216
  count?(params?: FindParams<M>): Promise<number>;
190
217
  where<K extends keyof M & string>(column: K, operator: WhereFilterOp, value: WhereValue<M[K]>): QueryBuilderInterface<M>;
@@ -461,6 +488,12 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
461
488
  * Batches are capped server-side (1000 rows by default) because one batch
462
489
  * holds its locks for the whole transaction — chunk larger jobs.
463
490
  *
491
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
492
+ * A client that never sees the response cannot know whether the batch
493
+ * committed, and without a key the server cannot tell the retry from a
494
+ * second genuine import — so it performs it again, duplicating every row in
495
+ * the batch rather than just one.
496
+ *
464
497
  * @returns The written rows, in the order given.
465
498
  *
466
499
  * @example
@@ -472,7 +505,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
472
505
  */
473
506
  createMany(data: I[], options?: {
474
507
  upsert?: boolean;
475
- }): Promise<M[]>;
508
+ } & WriteOptions): Promise<M[]>;
476
509
  /**
477
510
  * Update an existing record by ID.
478
511
  * @param data The fields to update (the collection's `Update` shape).
@@ -480,18 +513,94 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
480
513
  * @throws {RebaseApiError} with status 404 when the record does not exist.
481
514
  */
482
515
  update(id: string | number, data: U): Promise<M>;
516
+ /**
517
+ * Update many records in a single request and a single transaction.
518
+ *
519
+ * The counterpart to {@link createMany}, and the reason it exists is the
520
+ * same: one call per row means one HTTP round trip and one transaction per
521
+ * row. Every record still runs the normal pipeline — callbacks, relations,
522
+ * row-level security — and the batch is all-or-nothing, so a rejected
523
+ * record leaves none of them written and the error names the offending
524
+ * index.
525
+ *
526
+ * Each entry is `{ id, data }` rather than a flat row carrying its own key.
527
+ * That is deliberate: on a table keyed on something other than `id` — a
528
+ * `sku`, a composite key — a flat row cannot say whether a column is the
529
+ * address or a value to write. Naming the address separately mirrors
530
+ * single-row `update(id, data)` exactly and leaves nothing to infer.
531
+ *
532
+ * An id that matches no row fails the batch with a 404 rather than being
533
+ * skipped, for the same reason `update()` does: silently updating four of
534
+ * five rows is worse than updating none.
535
+ *
536
+ * Batches share `createMany`'s server-side cap (1000 rows by default),
537
+ * because one batch holds its locks for the whole transaction.
538
+ *
539
+ * Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
540
+ * An update replayed in full is naturally idempotent, but one interleaved
541
+ * with another writer's is not — the key is what stops a lost ACK from
542
+ * re-applying a stale batch over newer data.
543
+ *
544
+ * @returns The updated rows, in the order given.
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * await client.data.orders.updateMany([
549
+ * { id: "o-1", data: { status: "shipped" } },
550
+ * { id: "o-2", data: { status: "shipped" } }
551
+ * ]);
552
+ * ```
553
+ */
554
+ updateMany(updates: {
555
+ id: string | number;
556
+ data: U;
557
+ }[], options?: WriteOptions): Promise<M[]>;
483
558
  /**
484
559
  * Delete a record by ID.
485
560
  * @throws {RebaseApiError} with status 404 when the record does not exist.
486
561
  */
487
562
  delete(id: string | number): Promise<void>;
488
563
  /**
489
- * Subscribe to a collection for real-time updates.
564
+ * Delete many records in a single request and a single transaction.
565
+ *
566
+ * Takes ids, not a filter. A filter-shaped bulk delete is a different and
567
+ * far more dangerous operation — the failure mode is an omitted or
568
+ * mistyped condition emptying a table, and it cannot be reviewed at the
569
+ * call site the way an explicit list can. Read first, then pass the ids you
570
+ * meant.
571
+ *
572
+ * `beforeDelete` and `afterDelete` fire per row, exactly as they do for
573
+ * single deletes, and returning `false` from `beforeDelete` fails the batch
574
+ * rather than quietly dropping one row from it. All-or-nothing, so an id
575
+ * that matches no row 404s the whole call.
576
+ *
577
+ * Shares `createMany`'s row cap.
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * const stale = await client.data.sessions.findAll({
582
+ * where: { expires_at: ["<", cutoff] }
583
+ * });
584
+ * await client.data.sessions.deleteMany(stale.map(s => s.id as string));
585
+ * ```
490
586
  */
491
- listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
587
+ deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
492
588
  /**
493
- * Subscribe to a single record for real-time updates.
589
+ * The low-level realtime subscription: raw server pushes, nothing else.
590
+ *
591
+ * **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps
592
+ * this one and is what a UI actually wants — it emits from the local
593
+ * database first when offline is enabled, re-emits on local writes and
594
+ * rollbacks, and de-duplicates emissions so a refresh that changes nothing
595
+ * does not call back. `listen` does none of that; it forwards what the
596
+ * socket sends.
597
+ *
598
+ * Optional because it is only present when realtime is enabled. `observe()`
599
+ * is not — it degrades to a single fetch — which is the other reason to
600
+ * reach for it instead.
494
601
  */
602
+ listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
603
+ /** {@link listen} for a single row. Prefer `observeById()`. */
495
604
  listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
496
605
  /**
497
606
  * Count the number of records matching the given filter.
@@ -506,7 +615,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
506
615
  include(...relations: string[]): SDKQueryBuilderInterface<M>;
507
616
  }
508
617
  /**
509
- * The unified data access object for the **admin admin** (Entity-shaped).
618
+ * The unified data access object for the **admin panel** (Entity-shaped).
510
619
  *
511
620
  * Access collections as dynamic properties: `data.products.find(...)`. Each
512
621
  * accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
@@ -515,7 +624,7 @@ export interface SDKCollectionClient<M extends Record<string, unknown> = Record<
515
624
  *
516
625
  * @internal App developers do **not** use this — they use
517
626
  * {@link RebaseSdkData} (flat rows), which is what the SDK client and backend
518
- * `context.data` expose. This Entity-shaped map backs the admin admin only.
627
+ * `context.data` expose. This Entity-shaped map backs the admin panel only.
519
628
  *
520
629
  * @group Data
521
630
  */
@@ -539,10 +648,15 @@ export type RebaseData<DB = unknown> = {
539
648
  * Dynamic collection accessor.
540
649
  * Access any collection by its slug as a property.
541
650
  *
651
+ * The index signature is `CollectionAccessor` alone, for the reason
652
+ * spelled out on {@link RebaseSdkData}: unioning in the `collection`
653
+ * method's own signature is unnecessary across an intersection, and it
654
+ * costs `data.products.find()` — the access this `@example` documents.
655
+ *
542
656
  * @example
543
657
  * data.products.find({ where: { status: ["==", "published"] } })
544
658
  */
545
- [collectionSlug: string]: CollectionAccessor | ((slug: string) => CollectionAccessor);
659
+ [collectionSlug: string]: CollectionAccessor;
546
660
  });
547
661
  /**
548
662
  * The unified data access object for the **SDK** — flat rows, no Entity wrapper.
@@ -595,6 +709,26 @@ export type InsertOf<T> = T extends {
595
709
  export type UpdateOf<T> = T extends {
596
710
  Update: infer U extends Record<string, unknown>;
597
711
  } ? U : Partial<RowOf<T>>;
712
+ /**
713
+ * Note on the untyped branch below: its index signature is
714
+ * `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
715
+ *
716
+ * The union looks like it is needed so `collection` — a method on this same
717
+ * object — satisfies the index signature. It is not, because `collection` is
718
+ * declared in a *separate* member of the intersection, and TypeScript only
719
+ * requires named properties to be assignable to an index signature declared
720
+ * alongside them. Including the function arm cost the documented accessor:
721
+ *
722
+ * rebase.dataAsAdmin.projects.find()
723
+ * // ^ Property 'find' does not exist on type
724
+ * // 'SDKCollectionClient | ((slug: string) => …)'
725
+ *
726
+ * Every project without a generated `Database` type lands on this branch, so
727
+ * property-style access — the form used by the `@example` below, by the
728
+ * scaffolded function template, and by the 0.13 migration note — did not
729
+ * compile for any of them. Do not restore the arm; use `collection(slug)` if a
730
+ * caller genuinely needs the by-slug function.
731
+ */
598
732
  export type RebaseSdkData<DB = unknown> = {
599
733
  /**
600
734
  * Get a flat collection accessor by slug.
@@ -614,5 +748,5 @@ export type RebaseSdkData<DB = unknown> = {
614
748
  * @example
615
749
  * data.products.find({ where: { status: ["==", "published"] } })
616
750
  */
617
- [collectionSlug: string]: SDKCollectionClient | ((slug: string) => SDKCollectionClient);
751
+ [collectionSlug: string]: SDKCollectionClient;
618
752
  });
@@ -128,6 +128,25 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
128
128
  /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */
129
129
  upsert?: boolean;
130
130
  }
131
+ /**
132
+ * @internal
133
+ */
134
+ export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
135
+ path: string;
136
+ /**
137
+ * The rows to update, each named by its address.
138
+ *
139
+ * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
140
+ * values and is insert-shaped — `saveMany` passes `status: "new"` and no
141
+ * `id`, so it cannot express "update exactly this row". This can, and it is
142
+ * why bulk update is a separate driver method rather than a flag on that one.
143
+ */
144
+ updates: {
145
+ id: string | number;
146
+ values: Partial<EntityValues<M>>;
147
+ }[];
148
+ collection?: CollectionConfig<M>;
149
+ }
131
150
  /**
132
151
  * @internal
133
152
  */
@@ -139,6 +158,14 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
139
158
  };
140
159
  collection?: CollectionConfig<M>;
141
160
  }
161
+ /**
162
+ * @internal
163
+ */
164
+ export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
165
+ path: string;
166
+ ids: (string | number)[];
167
+ collection?: CollectionConfig<M>;
168
+ }
142
169
  export type FilterCombinationValidProps = {
143
170
  path: string;
144
171
  databaseId?: string;
@@ -210,6 +237,16 @@ export interface DataDriver {
210
237
  * back to `save` per row.
211
238
  */
212
239
  saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
240
+ /**
241
+ * Update many rows in one transaction, each addressed by id.
242
+ *
243
+ * Optional for the same reason `saveMany` is: a driver that cannot make the
244
+ * batch atomic should not pretend to. The REST layer reports
245
+ * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
246
+ * writes, which would be neither atomic nor one round trip — the two things
247
+ * a caller reaches for a batch to get.
248
+ */
249
+ updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
213
250
  /**
214
251
  * Delete a entity
215
252
  * @param props
@@ -221,6 +258,13 @@ export interface DataDriver {
221
258
  * @param path Collection path
222
259
  */
223
260
  deleteAll?(path: string): Promise<void>;
261
+ /**
262
+ * Delete many rows in one transaction, addressed by id.
263
+ *
264
+ * Ids rather than a filter, deliberately — see
265
+ * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
266
+ */
267
+ deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
224
268
  /**
225
269
  * Check if the given property is unique in the given collection
226
270
  * @param path Collection path
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,29 @@
1
+ /**
2
+ * The error codes every route can produce, as `RebaseApiError.code`.
3
+ *
4
+ * These are the defaults on `ApiError`'s static constructors server-side, so
5
+ * any endpoint can answer with one. They are **not** the complete set: routes
6
+ * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,
7
+ * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.
8
+ *
9
+ * Hence the union is deliberately open rather than closed. It exists to give
10
+ * autocomplete and to catch a typo in the common cases — `code` was a bare
11
+ * `string`, so `e.code === "NOT_FOUND"` and `e.code === "NOTFOUND"` were
12
+ * equally valid and only one of them worked. Closing it would be a lie that
13
+ * broke the moment a route added a code.
14
+ *
15
+ * @example
16
+ * if (e instanceof RebaseApiError) {
17
+ * switch (e.code) {
18
+ * case "NOT_FOUND": return null; // completed
19
+ * case "FORBIDDEN": return redirect();
20
+ * default: throw e; // routes' own codes land here
21
+ * }
22
+ * }
23
+ *
24
+ * @group Errors
25
+ */
26
+ export type RebaseErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "DB_PERMISSION_DENIED" | "SCHEMA_DRIFT" | (string & {});
1
27
  /**
2
28
  * Structured initializer for {@link RebaseApiError}.
3
29
  *
@@ -10,8 +36,8 @@ export interface RebaseErrorInit {
10
36
  * logic errors that have no HTTP status.
11
37
  */
12
38
  status?: number;
13
- /** Stable, machine-readable error code (e.g. `"NOT_FOUND"`, `"BAD_REQUEST"`). */
14
- code?: string;
39
+ /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
40
+ code?: RebaseErrorCode;
15
41
  /** Structured error payload returned by the server, when present. */
16
42
  details?: unknown;
17
43
  /** The underlying error this one wraps, if any. */
@@ -44,8 +70,8 @@ export interface RebaseErrorInit {
44
70
  export declare class RebaseApiError extends Error {
45
71
  /** HTTP status code, or `undefined` for non-HTTP errors. */
46
72
  readonly status?: number;
47
- /** Stable machine-readable error code, when the server supplied one. */
48
- readonly code?: string;
73
+ /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
74
+ readonly code?: RebaseErrorCode;
49
75
  /** Structured error payload from the server, when present. */
50
76
  readonly details?: unknown;
51
77
  constructor(message: string, init?: RebaseErrorInit);