@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.
@@ -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 ?? 20)` and ignores the explicit `offset`.
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
- /** Maximum number of items to return (default: 20). */
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 ?? 20)`.
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 admin** — resolves to `FindResponse<M>`
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 admin internals only.
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 admin** — every method
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 admin view-model only.
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
- * Subscribe to a collection for real-time updates.
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
- listen?(params: FindParams<M> | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
625
+ deleteMany(ids: (string | number)[], options?: WriteOptions): Promise<void>;
533
626
 
534
627
  /**
535
- * Subscribe to a single record for real-time updates.
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 admin** (Entity-shaped).
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 admin only.
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 | ((slug: 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 | ((slug: 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 (e.g. `"NOT_FOUND"`, `"BAD_REQUEST"`). */
14
- code?: string;
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?: string;
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
 
@@ -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",
@@ -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
- * You can set an alias that will be used internally instead of the collection name.
23
- * The `slug` value will be used to determine the URL of the collection.
24
- * Note that you can use this value in reference properties too.
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 { RebaseClient } from "../controllers/client";
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 {@link RebaseClient}. This is the **same singleton**
97
- * exposed as `rebase` (imported from `@rebasepro/server`) and as
98
- * `context` in collection callbacks — it is only named `client` here.
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 (`client.data`) runs with **admin privileges and bypasses
101
- * RLS** (`{ uid: "service", roles: ["admin"] }`). There is no per-request
102
- * user in a cron, so treat every query as fully trusted and scope your own
103
- * filters explicitly.
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: RebaseClient;
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 `rebase.data`.
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**.
@@ -7,6 +7,7 @@ export * from "./admin_block";
7
7
  export * from "./collections";
8
8
  export * from "./relations";
9
9
  export * from "./policy";
10
+ export * from "./rls-functions";
10
11
  export * from "./security_rules";
11
12
 
12
13
  export * from "./entity_callbacks";
@@ -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 `auth.uid()`.
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 **`auth.uid() IS NOT NULL` is a
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
- * `auth.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
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 = auth.uid()`.
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(auth.roles(), ',') && 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(auth.roles(), ',') @> 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
- * `auth.uid() IS NOT NULL AND auth.uid() <> 'anonymous'`.
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 `auth.uid()`. Checking only `IS NOT NULL` grants to
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
- * `auth.uid()` is `NULL` for them and only for them. Compiles to
172
- * `auth.uid() IS NULL`.
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 = auth.uid())
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 `auth.uid()`. @group Models */
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(auth.roles(), ',')`.
282
+ * `string_to_array(rebase.roles(), ',')`.
283
283
  * @group Models
284
284
  */
285
285
  export interface AuthRolesPolicyOperand {