@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/src/controllers/data.ts
CHANGED
|
@@ -38,13 +38,19 @@ export interface FilterCondition {
|
|
|
38
38
|
*
|
|
39
39
|
* `limit`/`offset` and `page` describe the same window two ways. If **both
|
|
40
40
|
* `offset` and `page` are provided, `page` wins** — the backend computes
|
|
41
|
-
* `offset = (page - 1) * (limit ??
|
|
42
|
-
* Pick one style per query.
|
|
41
|
+
* `offset = (page - 1) * (limit ?? DEFAULT_LIST_LIMIT)` and ignores the
|
|
42
|
+
* explicit `offset`. Pick one style per query.
|
|
43
43
|
*
|
|
44
44
|
* @group Data
|
|
45
45
|
*/
|
|
46
46
|
export interface FindParams<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Maximum number of items to return.
|
|
49
|
+
*
|
|
50
|
+
* Defaults to {@link DEFAULT_LIST_LIMIT}, and is clamped to
|
|
51
|
+
* {@link MAX_LIST_LIMIT}. Both bounds are applied by the backend, so a
|
|
52
|
+
* read is never unbounded whether or not a limit was asked for.
|
|
53
|
+
*/
|
|
48
54
|
limit?: number;
|
|
49
55
|
/**
|
|
50
56
|
* Number of items to skip. Ignored when {@link FindParams.page} is also
|
|
@@ -53,7 +59,7 @@ export interface FindParams<M extends Record<string, unknown> = Record<string, u
|
|
|
53
59
|
offset?: number;
|
|
54
60
|
/**
|
|
55
61
|
* Page number (1-indexed), alternative to {@link FindParams.offset}.
|
|
56
|
-
* When set, overrides `offset` as `(page - 1) * (limit ??
|
|
62
|
+
* When set, overrides `offset` as `(page - 1) * (limit ?? DEFAULT_LIST_LIMIT)`.
|
|
57
63
|
*/
|
|
58
64
|
page?: number;
|
|
59
65
|
/**
|
|
@@ -116,12 +122,12 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
|
|
|
116
122
|
|
|
117
123
|
|
|
118
124
|
/**
|
|
119
|
-
* Fluent query builder for the **admin
|
|
125
|
+
* Fluent query builder for the **admin panel** — resolves to `FindResponse<M>`
|
|
120
126
|
* (Snapshot-wrapped rows).
|
|
121
127
|
*
|
|
122
128
|
* @internal App developers should use {@link SDKQueryBuilderInterface}
|
|
123
129
|
* (flat rows, returned by `client.data.*` / `context.data.*`). This
|
|
124
|
-
* Snapshot-flavored variant backs the admin
|
|
130
|
+
* Snapshot-flavored variant backs the admin panel internals only.
|
|
125
131
|
*
|
|
126
132
|
* @group Data
|
|
127
133
|
*/
|
|
@@ -138,13 +144,13 @@ export interface QueryBuilderInterface<M extends Record<string, unknown> = Recor
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
/**
|
|
141
|
-
* A single collection's CRUD accessor for the **admin
|
|
147
|
+
* A single collection's CRUD accessor for the **admin panel** — every method
|
|
142
148
|
* resolves to `Snapshot`-wrapped rows (`FindResponse<M>` / `Snapshot<M>`).
|
|
143
149
|
*
|
|
144
150
|
* @internal App developers do **not** use this. The public, symmetric surface
|
|
145
151
|
* is {@link SDKCollectionClient} (flat rows), exposed as `client.data.products`
|
|
146
152
|
* in the SDK and `context.data.products` in framework callbacks. This
|
|
147
|
-
* Snapshot-flavored accessor backs the admin
|
|
153
|
+
* Snapshot-flavored accessor backs the admin panel view-model only.
|
|
148
154
|
*
|
|
149
155
|
* @group Data
|
|
150
156
|
*/
|
|
@@ -181,6 +187,20 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
181
187
|
*/
|
|
182
188
|
update(id: string | number, data: Partial<EntityValues<M>>): Promise<Entity<M>>;
|
|
183
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Update many records in a single transaction.
|
|
192
|
+
*
|
|
193
|
+
* See {@link SDKCollectionClient.updateMany}. Optional, as `createMany` is.
|
|
194
|
+
*/
|
|
195
|
+
updateMany?(updates: { id: string | number; data: Partial<EntityValues<M>> }[]): Promise<Entity<M>[]>;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Delete many records in a single transaction.
|
|
199
|
+
*
|
|
200
|
+
* See {@link SDKCollectionClient.deleteMany}. Optional, as `createMany` is.
|
|
201
|
+
*/
|
|
202
|
+
deleteMany?(ids: (string | number)[]): Promise<void>;
|
|
203
|
+
|
|
184
204
|
/**
|
|
185
205
|
* Delete a record by ID.
|
|
186
206
|
*/
|
|
@@ -200,6 +220,12 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
|
|
|
200
220
|
|
|
201
221
|
/**
|
|
202
222
|
* Count the number of records matching the given filter.
|
|
223
|
+
*
|
|
224
|
+
* Optional on this contract because a data source need not support it, and
|
|
225
|
+
* required on `CollectionClient` — the HTTP implementation always has it.
|
|
226
|
+
* So `client.data.posts.count()` compiles in the browser while the same
|
|
227
|
+
* call through a `context.data` accessor needs `count?.()`, which is the
|
|
228
|
+
* one place the two halves of this API are not interchangeable.
|
|
203
229
|
*/
|
|
204
230
|
count?(params?: FindParams<M>): Promise<number>;
|
|
205
231
|
|
|
@@ -501,6 +527,12 @@ export interface SDKCollectionClient<
|
|
|
501
527
|
* Batches are capped server-side (1000 rows by default) because one batch
|
|
502
528
|
* holds its locks for the whole transaction — chunk larger jobs.
|
|
503
529
|
*
|
|
530
|
+
* Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
|
|
531
|
+
* A client that never sees the response cannot know whether the batch
|
|
532
|
+
* committed, and without a key the server cannot tell the retry from a
|
|
533
|
+
* second genuine import — so it performs it again, duplicating every row in
|
|
534
|
+
* the batch rather than just one.
|
|
535
|
+
*
|
|
504
536
|
* @returns The written rows, in the order given.
|
|
505
537
|
*
|
|
506
538
|
* @example
|
|
@@ -510,7 +542,7 @@ export interface SDKCollectionClient<
|
|
|
510
542
|
* }
|
|
511
543
|
* ```
|
|
512
544
|
*/
|
|
513
|
-
createMany(data: I[], options?: { upsert?: boolean }): Promise<M[]>;
|
|
545
|
+
createMany(data: I[], options?: { upsert?: boolean } & WriteOptions): Promise<M[]>;
|
|
514
546
|
|
|
515
547
|
/**
|
|
516
548
|
* Update an existing record by ID.
|
|
@@ -520,6 +552,46 @@ export interface SDKCollectionClient<
|
|
|
520
552
|
*/
|
|
521
553
|
update(id: string | number, data: U): Promise<M>;
|
|
522
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Update many records in a single request and a single transaction.
|
|
557
|
+
*
|
|
558
|
+
* The counterpart to {@link createMany}, and the reason it exists is the
|
|
559
|
+
* same: one call per row means one HTTP round trip and one transaction per
|
|
560
|
+
* row. Every record still runs the normal pipeline — callbacks, relations,
|
|
561
|
+
* row-level security — and the batch is all-or-nothing, so a rejected
|
|
562
|
+
* record leaves none of them written and the error names the offending
|
|
563
|
+
* index.
|
|
564
|
+
*
|
|
565
|
+
* Each entry is `{ id, data }` rather than a flat row carrying its own key.
|
|
566
|
+
* That is deliberate: on a table keyed on something other than `id` — a
|
|
567
|
+
* `sku`, a composite key — a flat row cannot say whether a column is the
|
|
568
|
+
* address or a value to write. Naming the address separately mirrors
|
|
569
|
+
* single-row `update(id, data)` exactly and leaves nothing to infer.
|
|
570
|
+
*
|
|
571
|
+
* An id that matches no row fails the batch with a 404 rather than being
|
|
572
|
+
* skipped, for the same reason `update()` does: silently updating four of
|
|
573
|
+
* five rows is worse than updating none.
|
|
574
|
+
*
|
|
575
|
+
* Batches share `createMany`'s server-side cap (1000 rows by default),
|
|
576
|
+
* because one batch holds its locks for the whole transaction.
|
|
577
|
+
*
|
|
578
|
+
* Pass {@link WriteOptions.idempotencyKey} on anything that may be retried.
|
|
579
|
+
* An update replayed in full is naturally idempotent, but one interleaved
|
|
580
|
+
* with another writer's is not — the key is what stops a lost ACK from
|
|
581
|
+
* re-applying a stale batch over newer data.
|
|
582
|
+
*
|
|
583
|
+
* @returns The updated rows, in the order given.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```ts
|
|
587
|
+
* await client.data.orders.updateMany([
|
|
588
|
+
* { id: "o-1", data: { status: "shipped" } },
|
|
589
|
+
* { id: "o-2", data: { status: "shipped" } }
|
|
590
|
+
* ]);
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
updateMany(updates: { id: string | number; data: U }[], options?: WriteOptions): Promise<M[]>;
|
|
594
|
+
|
|
523
595
|
/**
|
|
524
596
|
* Delete a record by ID.
|
|
525
597
|
* @throws {RebaseApiError} with status 404 when the record does not exist.
|
|
@@ -527,13 +599,48 @@ export interface SDKCollectionClient<
|
|
|
527
599
|
delete(id: string | number): Promise<void>;
|
|
528
600
|
|
|
529
601
|
/**
|
|
530
|
-
*
|
|
602
|
+
* Delete many records in a single request and a single transaction.
|
|
603
|
+
*
|
|
604
|
+
* Takes ids, not a filter. A filter-shaped bulk delete is a different and
|
|
605
|
+
* far more dangerous operation — the failure mode is an omitted or
|
|
606
|
+
* mistyped condition emptying a table, and it cannot be reviewed at the
|
|
607
|
+
* call site the way an explicit list can. Read first, then pass the ids you
|
|
608
|
+
* meant.
|
|
609
|
+
*
|
|
610
|
+
* `beforeDelete` and `afterDelete` fire per row, exactly as they do for
|
|
611
|
+
* single deletes, and returning `false` from `beforeDelete` fails the batch
|
|
612
|
+
* rather than quietly dropping one row from it. All-or-nothing, so an id
|
|
613
|
+
* that matches no row 404s the whole call.
|
|
614
|
+
*
|
|
615
|
+
* Shares `createMany`'s row cap.
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```ts
|
|
619
|
+
* const stale = await client.data.sessions.findAll({
|
|
620
|
+
* where: { expires_at: ["<", cutoff] }
|
|
621
|
+
* });
|
|
622
|
+
* await client.data.sessions.deleteMany(stale.map(s => s.id as string));
|
|
623
|
+
* ```
|
|
531
624
|
*/
|
|
532
|
-
|
|
625
|
+
deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
|
|
533
626
|
|
|
534
627
|
/**
|
|
535
|
-
*
|
|
628
|
+
* The low-level realtime subscription: raw server pushes, nothing else.
|
|
629
|
+
*
|
|
630
|
+
* **Prefer `observe()`** on a client from `@rebasepro/client`, which wraps
|
|
631
|
+
* this one and is what a UI actually wants — it emits from the local
|
|
632
|
+
* database first when offline is enabled, re-emits on local writes and
|
|
633
|
+
* rollbacks, and de-duplicates emissions so a refresh that changes nothing
|
|
634
|
+
* does not call back. `listen` does none of that; it forwards what the
|
|
635
|
+
* socket sends.
|
|
636
|
+
*
|
|
637
|
+
* Optional because it is only present when realtime is enabled. `observe()`
|
|
638
|
+
* is not — it degrades to a single fetch — which is the other reason to
|
|
639
|
+
* reach for it instead.
|
|
536
640
|
*/
|
|
641
|
+
listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
|
|
642
|
+
|
|
643
|
+
/** {@link listen} for a single row. Prefer `observeById()`. */
|
|
537
644
|
listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
|
|
538
645
|
|
|
539
646
|
/**
|
|
@@ -552,7 +659,7 @@ export interface SDKCollectionClient<
|
|
|
552
659
|
}
|
|
553
660
|
|
|
554
661
|
/**
|
|
555
|
-
* The unified data access object for the **admin
|
|
662
|
+
* The unified data access object for the **admin panel** (Entity-shaped).
|
|
556
663
|
*
|
|
557
664
|
* Access collections as dynamic properties: `data.products.find(...)`. Each
|
|
558
665
|
* accessor returns `Entity`-wrapped records (`{ id, path, values }`) — the
|
|
@@ -561,7 +668,7 @@ export interface SDKCollectionClient<
|
|
|
561
668
|
*
|
|
562
669
|
* @internal App developers do **not** use this — they use
|
|
563
670
|
* {@link RebaseSdkData} (flat rows), which is what the SDK client and backend
|
|
564
|
-
* `context.data` expose. This Entity-shaped map backs the admin
|
|
671
|
+
* `context.data` expose. This Entity-shaped map backs the admin panel only.
|
|
565
672
|
*
|
|
566
673
|
* @group Data
|
|
567
674
|
*/
|
|
@@ -584,10 +691,15 @@ export type RebaseData<DB = unknown> = {
|
|
|
584
691
|
* Dynamic collection accessor.
|
|
585
692
|
* Access any collection by its slug as a property.
|
|
586
693
|
*
|
|
694
|
+
* The index signature is `CollectionAccessor` alone, for the reason
|
|
695
|
+
* spelled out on {@link RebaseSdkData}: unioning in the `collection`
|
|
696
|
+
* method's own signature is unnecessary across an intersection, and it
|
|
697
|
+
* costs `data.products.find()` — the access this `@example` documents.
|
|
698
|
+
*
|
|
587
699
|
* @example
|
|
588
700
|
* data.products.find({ where: { status: ["==", "published"] } })
|
|
589
701
|
*/
|
|
590
|
-
[collectionSlug: string]: CollectionAccessor
|
|
702
|
+
[collectionSlug: string]: CollectionAccessor;
|
|
591
703
|
}
|
|
592
704
|
);
|
|
593
705
|
|
|
@@ -639,6 +751,26 @@ export type InsertOf<T> = T extends { Insert: infer I extends Record<string, unk
|
|
|
639
751
|
*/
|
|
640
752
|
export type UpdateOf<T> = T extends { Update: infer U extends Record<string, unknown> } ? U : Partial<RowOf<T>>;
|
|
641
753
|
|
|
754
|
+
/**
|
|
755
|
+
* Note on the untyped branch below: its index signature is
|
|
756
|
+
* `SDKCollectionClient`, NOT `SDKCollectionClient | ((slug: string) => …)`.
|
|
757
|
+
*
|
|
758
|
+
* The union looks like it is needed so `collection` — a method on this same
|
|
759
|
+
* object — satisfies the index signature. It is not, because `collection` is
|
|
760
|
+
* declared in a *separate* member of the intersection, and TypeScript only
|
|
761
|
+
* requires named properties to be assignable to an index signature declared
|
|
762
|
+
* alongside them. Including the function arm cost the documented accessor:
|
|
763
|
+
*
|
|
764
|
+
* rebase.dataAsAdmin.projects.find()
|
|
765
|
+
* // ^ Property 'find' does not exist on type
|
|
766
|
+
* // 'SDKCollectionClient | ((slug: string) => …)'
|
|
767
|
+
*
|
|
768
|
+
* Every project without a generated `Database` type lands on this branch, so
|
|
769
|
+
* property-style access — the form used by the `@example` below, by the
|
|
770
|
+
* scaffolded function template, and by the 0.13 migration note — did not
|
|
771
|
+
* compile for any of them. Do not restore the arm; use `collection(slug)` if a
|
|
772
|
+
* caller genuinely needs the by-slug function.
|
|
773
|
+
*/
|
|
642
774
|
export type RebaseSdkData<DB = unknown> = {
|
|
643
775
|
/**
|
|
644
776
|
* Get a flat collection accessor by slug.
|
|
@@ -659,6 +791,6 @@ export type RebaseSdkData<DB = unknown> = {
|
|
|
659
791
|
* @example
|
|
660
792
|
* data.products.find({ where: { status: ["==", "published"] } })
|
|
661
793
|
*/
|
|
662
|
-
[collectionSlug: string]: SDKCollectionClient
|
|
794
|
+
[collectionSlug: string]: SDKCollectionClient;
|
|
663
795
|
}
|
|
664
796
|
);
|
|
@@ -169,6 +169,23 @@ export interface SaveManyProps<M extends Record<string, unknown> = Record<string
|
|
|
169
169
|
upsert?: boolean;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
export interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
176
|
+
path: string;
|
|
177
|
+
/**
|
|
178
|
+
* The rows to update, each named by its address.
|
|
179
|
+
*
|
|
180
|
+
* Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the
|
|
181
|
+
* values and is insert-shaped — `saveMany` passes `status: "new"` and no
|
|
182
|
+
* `id`, so it cannot express "update exactly this row". This can, and it is
|
|
183
|
+
* why bulk update is a separate driver method rather than a flag on that one.
|
|
184
|
+
*/
|
|
185
|
+
updates: { id: string | number; values: Partial<EntityValues<M>> }[];
|
|
186
|
+
collection?: CollectionConfig<M>;
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
/**
|
|
173
190
|
* @internal
|
|
174
191
|
*/
|
|
@@ -177,6 +194,15 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
|
|
|
177
194
|
collection?: CollectionConfig<M>;
|
|
178
195
|
}
|
|
179
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
export interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {
|
|
201
|
+
path: string;
|
|
202
|
+
ids: (string | number)[];
|
|
203
|
+
collection?: CollectionConfig<M>;
|
|
204
|
+
}
|
|
205
|
+
|
|
180
206
|
export type FilterCombinationValidProps = {
|
|
181
207
|
path: string;
|
|
182
208
|
databaseId?: string;
|
|
@@ -258,6 +284,17 @@ export interface DataDriver {
|
|
|
258
284
|
*/
|
|
259
285
|
saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
260
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Update many rows in one transaction, each addressed by id.
|
|
289
|
+
*
|
|
290
|
+
* Optional for the same reason `saveMany` is: a driver that cannot make the
|
|
291
|
+
* batch atomic should not pretend to. The REST layer reports
|
|
292
|
+
* `BULK_UNSUPPORTED` rather than silently falling back to a loop of single
|
|
293
|
+
* writes, which would be neither atomic nor one round trip — the two things
|
|
294
|
+
* a caller reaches for a batch to get.
|
|
295
|
+
*/
|
|
296
|
+
updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;
|
|
297
|
+
|
|
261
298
|
/**
|
|
262
299
|
* Delete a entity
|
|
263
300
|
* @param props
|
|
@@ -271,6 +308,14 @@ export interface DataDriver {
|
|
|
271
308
|
*/
|
|
272
309
|
deleteAll?(path: string): Promise<void>;
|
|
273
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Delete many rows in one transaction, addressed by id.
|
|
313
|
+
*
|
|
314
|
+
* Ids rather than a filter, deliberately — see
|
|
315
|
+
* {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.
|
|
316
|
+
*/
|
|
317
|
+
deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;
|
|
318
|
+
|
|
274
319
|
/**
|
|
275
320
|
* Check if the given property is unique in the given collection
|
|
276
321
|
* @param path Collection path
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
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 =
|
|
27
|
+
| "BAD_REQUEST"
|
|
28
|
+
| "UNAUTHORIZED"
|
|
29
|
+
| "FORBIDDEN"
|
|
30
|
+
| "NOT_FOUND"
|
|
31
|
+
| "CONFLICT"
|
|
32
|
+
| "INTERNAL_ERROR"
|
|
33
|
+
| "SERVICE_UNAVAILABLE"
|
|
34
|
+
| "DB_PERMISSION_DENIED"
|
|
35
|
+
| "SCHEMA_DRIFT"
|
|
36
|
+
// `string & {}` keeps the union open while preserving completion on the
|
|
37
|
+
// literals above — a bare `| string` would collapse them and offer nothing.
|
|
38
|
+
| (string & {});
|
|
39
|
+
|
|
1
40
|
/**
|
|
2
41
|
* Structured initializer for {@link RebaseApiError}.
|
|
3
42
|
*
|
|
@@ -10,8 +49,8 @@ export interface RebaseErrorInit {
|
|
|
10
49
|
* logic errors that have no HTTP status.
|
|
11
50
|
*/
|
|
12
51
|
status?: number;
|
|
13
|
-
/** Stable, machine-readable error code
|
|
14
|
-
code?:
|
|
52
|
+
/** Stable, machine-readable error code. See {@link RebaseErrorCode}. */
|
|
53
|
+
code?: RebaseErrorCode;
|
|
15
54
|
/** Structured error payload returned by the server, when present. */
|
|
16
55
|
details?: unknown;
|
|
17
56
|
/** The underlying error this one wraps, if any. */
|
|
@@ -45,8 +84,8 @@ export interface RebaseErrorInit {
|
|
|
45
84
|
export class RebaseApiError extends Error {
|
|
46
85
|
/** HTTP status code, or `undefined` for non-HTTP errors. */
|
|
47
86
|
readonly status?: number;
|
|
48
|
-
/** Stable machine-readable error code, when the server supplied one. */
|
|
49
|
-
readonly code?:
|
|
87
|
+
/** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
|
|
88
|
+
readonly code?: RebaseErrorCode;
|
|
50
89
|
/** Structured error payload from the server, when present. */
|
|
51
90
|
readonly details?: unknown;
|
|
52
91
|
|
package/src/types/admin_block.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const ADMIN_COLLECTION_KEYS = [
|
|
|
41
41
|
"defaultSize",
|
|
42
42
|
"defaultViewMode",
|
|
43
43
|
"disableDefaultActions",
|
|
44
|
+
"display",
|
|
44
45
|
"enabledViews",
|
|
45
46
|
"entityActions",
|
|
46
47
|
"entityViews",
|
|
@@ -51,6 +52,7 @@ export const ADMIN_COLLECTION_KEYS = [
|
|
|
51
52
|
"formAutoSave",
|
|
52
53
|
"formView",
|
|
53
54
|
"group",
|
|
55
|
+
"hideFromEntityViews",
|
|
54
56
|
"hideFromNavigation",
|
|
55
57
|
"hideIdFromCollection",
|
|
56
58
|
"hideIdFromForm",
|
package/src/types/collections.ts
CHANGED
|
@@ -19,9 +19,27 @@ import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operato
|
|
|
19
19
|
export interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The collection's identity. Required, and the value nearly everything else
|
|
23
|
+
* keys on:
|
|
24
|
+
*
|
|
25
|
+
* - the REST path — `/api/data/<slug>`
|
|
26
|
+
* - the SDK accessor — `client.data.<slug>` / `client.data.collection("<slug>")`
|
|
27
|
+
* - the admin panel's URL
|
|
28
|
+
* - the target of a `reference` or `relation` property
|
|
29
|
+
*
|
|
30
|
+
* Conventionally kebab-case and plural (`blog-posts`). It is independent of
|
|
31
|
+
* {@link table}: the slug is what callers say, the table is where the rows
|
|
32
|
+
* live, and renaming one does not rename the other.
|
|
33
|
+
*
|
|
34
|
+
* Treat it as frozen once anything has shipped against it — changing a slug
|
|
35
|
+
* changes every URL and every generated accessor at once.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* defineCollection({
|
|
39
|
+
* slug: "blog-posts", // /api/data/blog-posts, client.data.blogPosts
|
|
40
|
+
* table: "posts",
|
|
41
|
+
* properties: { … }
|
|
42
|
+
* })
|
|
25
43
|
*/
|
|
26
44
|
slug: string;
|
|
27
45
|
|
package/src/types/cron.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RebaseServerClient } from "../controllers/client";
|
|
2
|
+
import type { RebaseSdkData } from "../controllers/data";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Cron Job type definitions for Rebase.
|
|
@@ -93,16 +94,57 @@ export interface CronJobContext {
|
|
|
93
94
|
log: (...args: unknown[]) => void;
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
|
-
* The server-side
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
* The server-side Rebase singleton — the **same object** `import { rebase }
|
|
98
|
+
* from "@rebasepro/server"` returns, and the same one `defineFunction`
|
|
99
|
+
* hands its callback. Spelled the same way here so that one thing has one
|
|
100
|
+
* name across every server-side authoring surface.
|
|
99
101
|
*
|
|
100
|
-
* Its data plane
|
|
101
|
-
* RLS** (`{ uid: "service", roles:
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* Its data plane is {@link RebaseServerClient.dataAsAdmin}, which runs with
|
|
103
|
+
* **admin privileges and bypasses RLS** (`{ uid: "service", roles:
|
|
104
|
+
* ["admin"] }`). A cron has no per-request user, so there is no user-scoped
|
|
105
|
+
* alternative here and no policy to fall back on: scope every query's
|
|
106
|
+
* filters yourself.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* export default defineCron({
|
|
110
|
+
* name: "Nightly cleanup",
|
|
111
|
+
* schedule: "0 3 * * *",
|
|
112
|
+
* async handler({ rebase, log }) {
|
|
113
|
+
* const expired = await rebase.dataAsAdmin.sessions.findAll({
|
|
114
|
+
* where: { expired: ["==", true] }
|
|
115
|
+
* });
|
|
116
|
+
* for (const session of expired) {
|
|
117
|
+
* await rebase.dataAsAdmin.sessions.delete(session.id as string);
|
|
118
|
+
* }
|
|
119
|
+
* log(`Deleted ${expired.length} expired sessions`);
|
|
120
|
+
* }
|
|
121
|
+
* });
|
|
122
|
+
*/
|
|
123
|
+
rebase: RebaseServerClient;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The same object as {@link rebase}, under the name this context used
|
|
127
|
+
* before.
|
|
128
|
+
*
|
|
129
|
+
* @deprecated Use `rebase` instead. Two things made the old name a problem,
|
|
130
|
+
* and neither was cosmetic. It contradicted every other server surface,
|
|
131
|
+
* where the singleton is `rebase` — the previous docstring had to end with
|
|
132
|
+
* *"it is only named `client` here"*. And typing it as `RebaseClient`
|
|
133
|
+
* re-exposed `client.data`, the alias that {@link RebaseServerClient}
|
|
134
|
+
* deliberately `Omit`s so the RLS-bypassing plane has exactly one name and
|
|
135
|
+
* the privilege is visible at the call site. A reader who learned
|
|
136
|
+
* `client.data` here carried it to a collection callback, where
|
|
137
|
+
* `context.data` is the *user-scoped* plane — same spelling, opposite
|
|
138
|
+
* privilege.
|
|
139
|
+
*
|
|
140
|
+
* Still the full server client at runtime, and `data` still resolves, so
|
|
141
|
+
* existing cron files keep working and keep compiling. It will be removed
|
|
142
|
+
* in the next major.
|
|
104
143
|
*/
|
|
105
|
-
client:
|
|
144
|
+
client: RebaseServerClient & {
|
|
145
|
+
/** @deprecated Use `rebase.dataAsAdmin` — the name states the privilege. */
|
|
146
|
+
data: RebaseSdkData;
|
|
147
|
+
};
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
// =============================================================================
|
|
@@ -8,7 +8,8 @@ import type { RebaseCallContext } from "../call_context";
|
|
|
8
8
|
*
|
|
9
9
|
* Register per-collection on the collection's `callbacks` field, or globally
|
|
10
10
|
* via `initializeRebaseBackend({ callbacks })`. Fires on **every** data path — REST API,
|
|
11
|
-
* WebSocket / realtime subscriptions, and server-side
|
|
11
|
+
* WebSocket / realtime subscriptions, and server-side writes through
|
|
12
|
+
* `rebase.dataAsAdmin`.
|
|
12
13
|
*
|
|
13
14
|
* When both global and per-collection callbacks are registered, execution
|
|
14
15
|
* order is: **global → collection → property callbacks**.
|
package/src/types/index.ts
CHANGED
package/src/types/policy.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type PolicyExpression =
|
|
|
32
32
|
| RawPolicyExpression;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* The id a request without a logged-in user reports as `
|
|
35
|
+
* The id a request without a logged-in user reports as `rebase.uid()`.
|
|
36
36
|
*
|
|
37
37
|
* A user-context request always sets `app.uid`: blank would read back as
|
|
38
38
|
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
@@ -40,7 +40,7 @@ export type PolicyExpression =
|
|
|
40
40
|
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
41
41
|
* is set.
|
|
42
42
|
*
|
|
43
|
-
* The consequence for policy authors is that **`
|
|
43
|
+
* The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
|
|
44
44
|
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
45
45
|
* {@link policy.authenticated} to mean "signed in", and
|
|
46
46
|
* {@link policy.serverContext} to mean "the trusted server context". Do not
|
|
@@ -58,7 +58,7 @@ export const ANONYMOUS_USER_ID = "anonymous";
|
|
|
58
58
|
* JavaScript evaluator and the linter were all built on
|
|
59
59
|
* {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
|
|
60
60
|
* callers as `'anon'` — so `policy.authenticated()`, which compiled to
|
|
61
|
-
* `
|
|
61
|
+
* `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
|
|
62
62
|
* sanctioned way to write "signed in" granted to everyone, and the linter
|
|
63
63
|
* flagged the spelling that actually worked as a foreign convention.
|
|
64
64
|
*
|
|
@@ -117,7 +117,7 @@ export interface NotPolicyExpression {
|
|
|
117
117
|
export type PolicyCompareOperator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Compares two operands, e.g. `owner_id =
|
|
120
|
+
* Compares two operands, e.g. `owner_id = rebase.uid()`.
|
|
121
121
|
* @group Models
|
|
122
122
|
*/
|
|
123
123
|
export interface ComparePolicyExpression {
|
|
@@ -129,7 +129,7 @@ export interface ComparePolicyExpression {
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* True when the user holds *at least one* of the given application roles.
|
|
132
|
-
* Compiles to `string_to_array(
|
|
132
|
+
* Compiles to `string_to_array(rebase.roles(), ',') && ARRAY[...]`.
|
|
133
133
|
* @group Models
|
|
134
134
|
*/
|
|
135
135
|
export interface RolesOverlapPolicyExpression {
|
|
@@ -139,7 +139,7 @@ export interface RolesOverlapPolicyExpression {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* True when the user holds *all* of the given application roles.
|
|
142
|
-
* Compiles to `string_to_array(
|
|
142
|
+
* Compiles to `string_to_array(rebase.roles(), ',') @> ARRAY[...]`.
|
|
143
143
|
* @group Models
|
|
144
144
|
*/
|
|
145
145
|
export interface RolesContainPolicyExpression {
|
|
@@ -149,11 +149,11 @@ export interface RolesContainPolicyExpression {
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* True when a signed-in user is making the request. Compiles to
|
|
152
|
-
* `
|
|
152
|
+
* `rebase.uid() IS NOT NULL AND rebase.uid() <> 'anonymous'`.
|
|
153
153
|
*
|
|
154
154
|
* Both halves are load-bearing. `IS NOT NULL` excludes the server context;
|
|
155
155
|
* the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who
|
|
156
|
-
* *do* carry a non-null `
|
|
156
|
+
* *do* carry a non-null `rebase.uid()`. Checking only `IS NOT NULL` grants to
|
|
157
157
|
* everyone — see {@link ANONYMOUS_USER_ID}.
|
|
158
158
|
*
|
|
159
159
|
* `policy.not(policy.authenticated())` therefore means "anonymous visitor or
|
|
@@ -168,8 +168,8 @@ export interface AuthenticatedPolicyExpression {
|
|
|
168
168
|
/**
|
|
169
169
|
* True only in the trusted **server context** — the built-in flows that run
|
|
170
170
|
* without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so
|
|
171
|
-
* `
|
|
172
|
-
* `
|
|
171
|
+
* `rebase.uid()` is `NULL` for them and only for them. Compiles to
|
|
172
|
+
* `rebase.uid() IS NULL`.
|
|
173
173
|
*
|
|
174
174
|
* This is what lets the owner connection satisfy a policy even under FORCE RLS.
|
|
175
175
|
* It is deliberately a primitive rather than `not(authenticated())`: the two
|
|
@@ -207,7 +207,7 @@ export interface ServerContextPolicyExpression {
|
|
|
207
207
|
* ),
|
|
208
208
|
* })
|
|
209
209
|
* // → EXISTS (SELECT 1 FROM team_members _ex0
|
|
210
|
-
* // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id =
|
|
210
|
+
* // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = rebase.uid())
|
|
211
211
|
* ```
|
|
212
212
|
*
|
|
213
213
|
* Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript
|
|
@@ -272,14 +272,14 @@ export interface LiteralPolicyOperand {
|
|
|
272
272
|
value: string | number | boolean | null;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
/** The current user's id — compiles to `
|
|
275
|
+
/** The current user's id — compiles to `rebase.uid()`. @group Models */
|
|
276
276
|
export interface AuthUidPolicyOperand {
|
|
277
277
|
kind: "authUid";
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/**
|
|
281
281
|
* The current user's roles as an array — compiles to
|
|
282
|
-
* `string_to_array(
|
|
282
|
+
* `string_to_array(rebase.roles(), ',')`.
|
|
283
283
|
* @group Models
|
|
284
284
|
*/
|
|
285
285
|
export interface AuthRolesPolicyOperand {
|