@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.
- package/dist/call_context.d.ts +61 -4
- package/dist/controllers/client.d.ts +16 -59
- package/dist/controllers/data.d.ts +150 -16
- package/dist/controllers/data_driver.d.ts +44 -0
- package/dist/errors.d.ts +30 -4
- package/dist/index.es.js +97 -5
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +1 -1
- package/dist/types/collections.d.ts +21 -3
- package/dist/types/cron.d.ts +50 -9
- package/dist/types/entity_callbacks.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/policy.d.ts +13 -13
- package/dist/types/properties.d.ts +22 -4
- package/dist/types/rls-functions.d.ts +84 -0
- package/package.json +2 -2
- package/src/call_context.ts +59 -4
- package/src/controllers/client.ts +16 -80
- package/src/controllers/data.ts +148 -16
- package/src/controllers/data_driver.ts +45 -0
- package/src/errors.ts +43 -4
- package/src/types/admin_block.ts +2 -0
- package/src/types/collections.ts +21 -3
- package/src/types/cron.ts +51 -9
- package/src/types/entity_callbacks.ts +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +13 -13
- package/src/types/properties.ts +23 -4
- package/src/types/rls-functions.ts +98 -0
package/dist/call_context.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
/**
|
|
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 ??
|
|
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
|
-
/**
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
587
|
+
deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
|
|
492
588
|
/**
|
|
493
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14
|
-
code?:
|
|
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?:
|
|
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);
|