@rebasepro/types 0.13.0 → 0.13.1-canary.g249daa1
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 +298 -30
- package/dist/controllers/data_driver.d.ts +48 -0
- package/dist/errors.d.ts +30 -4
- package/dist/index.es.js +113 -5
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +1 -1
- package/dist/types/backend.d.ts +2 -0
- package/dist/types/collections.d.ts +36 -3
- package/dist/types/cron.d.ts +50 -9
- package/dist/types/entities.d.ts +11 -0
- package/dist/types/entity_callbacks.d.ts +2 -1
- package/dist/types/index.d.ts +2 -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/dist/types/search.d.ts +231 -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 +298 -30
- package/src/controllers/data_driver.ts +49 -0
- package/src/errors.ts +43 -4
- package/src/types/admin_block.ts +2 -0
- package/src/types/backend.ts +2 -0
- package/src/types/collections.ts +37 -3
- package/src/types/cron.ts +51 -9
- package/src/types/entities.ts +12 -0
- package/src/types/entity_callbacks.ts +2 -1
- package/src/types/index.ts +2 -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/types/search.ts +247 -0
package/src/types/backend.ts
CHANGED
package/src/types/collections.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Relation } from "./relations";
|
|
|
7
7
|
import type { SecurityRule } from "./security_rules";
|
|
8
8
|
import { getDataSourceCapabilities } from "./data_source";
|
|
9
9
|
import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operators";
|
|
10
|
+
import type { SearchConfig } from "./search";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Base interface containing all driver-agnostic collection properties.
|
|
@@ -19,9 +20,27 @@ import type { WhereFilterOp, FilterValues, FilterPreset } from "./filter-operato
|
|
|
19
20
|
export interface BaseCollectionConfig<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> {
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* The collection's identity. Required, and the value nearly everything else
|
|
24
|
+
* keys on:
|
|
25
|
+
*
|
|
26
|
+
* - the REST path — `/api/data/<slug>`
|
|
27
|
+
* - the SDK accessor — `client.data.<slug>` / `client.data.collection("<slug>")`
|
|
28
|
+
* - the admin panel's URL
|
|
29
|
+
* - the target of a `reference` or `relation` property
|
|
30
|
+
*
|
|
31
|
+
* Conventionally kebab-case and plural (`blog-posts`). It is independent of
|
|
32
|
+
* {@link table}: the slug is what callers say, the table is where the rows
|
|
33
|
+
* live, and renaming one does not rename the other.
|
|
34
|
+
*
|
|
35
|
+
* Treat it as frozen once anything has shipped against it — changing a slug
|
|
36
|
+
* changes every URL and every generated accessor at once.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* defineCollection({
|
|
40
|
+
* slug: "blog-posts", // /api/data/blog-posts, client.data.blogPosts
|
|
41
|
+
* table: "posts",
|
|
42
|
+
* properties: { … }
|
|
43
|
+
* })
|
|
25
44
|
*/
|
|
26
45
|
slug: string;
|
|
27
46
|
|
|
@@ -283,6 +302,21 @@ export interface PostgresCollectionConfig<M extends Record<string, unknown> = Re
|
|
|
283
302
|
* @default false
|
|
284
303
|
*/
|
|
285
304
|
disableDefaultPolicies?: boolean;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Opt in to Postgres full-text search for this collection.
|
|
308
|
+
*
|
|
309
|
+
* Omit it and `.search()` keeps its existing behaviour exactly — an
|
|
310
|
+
* `ILIKE '%term%'` across top-level string properties. Declare it and the
|
|
311
|
+
* collection gains one generated `tsvector` column and a GIN index, and
|
|
312
|
+
* `.search()` compiles to a ranked `@@ websearch_to_tsquery` against them.
|
|
313
|
+
*
|
|
314
|
+
* Postgres-only, like {@link VectorProperty}: the block is rejected at boot
|
|
315
|
+
* on other engines rather than silently ignored.
|
|
316
|
+
*
|
|
317
|
+
* @see SearchConfig
|
|
318
|
+
*/
|
|
319
|
+
search?: SearchConfig;
|
|
286
320
|
}
|
|
287
321
|
|
|
288
322
|
/**
|
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
|
// =============================================================================
|
package/src/types/entities.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SearchMatch } from "./search";
|
|
1
2
|
/**
|
|
2
3
|
* New or existing status
|
|
3
4
|
* @group Models
|
|
@@ -26,6 +27,17 @@ export interface Entity<M extends Record<string, unknown> = Record<string, unkno
|
|
|
26
27
|
*/
|
|
27
28
|
values: EntityValues<M>;
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Why this entity is in a search result: which declared fields matched, and
|
|
32
|
+
* the text around each hit.
|
|
33
|
+
*
|
|
34
|
+
* Present only on rows returned by a search that asked for it. A sibling of
|
|
35
|
+
* `values` rather than a key inside it, because it describes the *query*,
|
|
36
|
+
* not the record — nothing in the collection declares it, no form edits it,
|
|
37
|
+
* and a record fetched by id never has one.
|
|
38
|
+
*/
|
|
39
|
+
searchMatches?: SearchMatch[];
|
|
40
|
+
|
|
29
41
|
/**
|
|
30
42
|
* Which driver this entity belongs to (e.g., 'postgres', 'firestore').
|
|
31
43
|
* If not specified, the default driver is assumed.
|
|
@@ -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
|
@@ -5,8 +5,10 @@ export * from "./chips";
|
|
|
5
5
|
export * from "./properties";
|
|
6
6
|
export * from "./admin_block";
|
|
7
7
|
export * from "./collections";
|
|
8
|
+
export * from "./search";
|
|
8
9
|
export * from "./relations";
|
|
9
10
|
export * from "./policy";
|
|
11
|
+
export * from "./rls-functions";
|
|
10
12
|
export * from "./security_rules";
|
|
11
13
|
|
|
12
14
|
export * from "./entity_callbacks";
|
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 {
|
package/src/types/properties.ts
CHANGED
|
@@ -225,7 +225,7 @@ export interface BaseProperty<CustomProps = unknown> {
|
|
|
225
225
|
* written and queryable server-side; it is stripped from every row the API
|
|
226
226
|
* serves, for every caller, including admins and service keys.
|
|
227
227
|
*
|
|
228
|
-
* This is a server-side guarantee, unlike `
|
|
228
|
+
* This is a server-side guarantee, unlike `admin.hideFromCollection`, which
|
|
229
229
|
* only stops the admin panel from *rendering* a field and leaves it in the
|
|
230
230
|
* JSON payload.
|
|
231
231
|
*/
|
|
@@ -1035,6 +1035,18 @@ export interface ImageResize {
|
|
|
1035
1035
|
*/
|
|
1036
1036
|
export type JsonLogicRule = Record<string, any>;
|
|
1037
1037
|
|
|
1038
|
+
/**
|
|
1039
|
+
* A condition that is either a JSON Logic rule or a literal answer.
|
|
1040
|
+
*
|
|
1041
|
+
* The unconditional case is the common one — "this field is never editable",
|
|
1042
|
+
* "this field is never shown" — and with only a rule accepted it had to be
|
|
1043
|
+
* spelled `{ "==": [1, 1] }`, which reads as a puzzle at the call site. A plain
|
|
1044
|
+
* `true` says the same thing.
|
|
1045
|
+
*
|
|
1046
|
+
* @group Entity properties
|
|
1047
|
+
*/
|
|
1048
|
+
export type ConditionRule = JsonLogicRule | boolean;
|
|
1049
|
+
|
|
1038
1050
|
/**
|
|
1039
1051
|
* Conditions for individual enum values within a property.
|
|
1040
1052
|
* @group Entity properties
|
|
@@ -1084,8 +1096,10 @@ export interface PropertyConditions {
|
|
|
1084
1096
|
* \`\`\`json
|
|
1085
1097
|
* { "==": [{ "var": "values.status" }, "archived"] }
|
|
1086
1098
|
* \`\`\`
|
|
1099
|
+
*
|
|
1100
|
+
* A literal `true` disables it unconditionally.
|
|
1087
1101
|
*/
|
|
1088
|
-
disabled?:
|
|
1102
|
+
disabled?: ConditionRule;
|
|
1089
1103
|
|
|
1090
1104
|
/**
|
|
1091
1105
|
* Message to display when the field is disabled by a condition.
|
|
@@ -1101,14 +1115,19 @@ export interface PropertyConditions {
|
|
|
1101
1115
|
/**
|
|
1102
1116
|
* Hide the field completely when this condition evaluates to true.
|
|
1103
1117
|
* The field is removed from the form (not just visually hidden).
|
|
1118
|
+
*
|
|
1119
|
+
* A literal `true` hides it unconditionally. This is the way to keep a
|
|
1120
|
+
* property out of the form without keeping it out of the collection.
|
|
1104
1121
|
*/
|
|
1105
|
-
hidden?:
|
|
1122
|
+
hidden?: ConditionRule;
|
|
1106
1123
|
|
|
1107
1124
|
/**
|
|
1108
1125
|
* Make the field read-only when this condition evaluates to true.
|
|
1109
1126
|
* Renders as a preview instead of an input.
|
|
1127
|
+
*
|
|
1128
|
+
* A literal `true` makes it read-only unconditionally.
|
|
1110
1129
|
*/
|
|
1111
|
-
readOnly?:
|
|
1130
|
+
readOnly?: ConditionRule;
|
|
1112
1131
|
|
|
1113
1132
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1114
1133
|
// VALIDATION CONDITIONS
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQL helper functions RLS policies call, and the schema they live in.
|
|
3
|
+
*
|
|
4
|
+
* ## One schema, and it is ours
|
|
5
|
+
*
|
|
6
|
+
* Rebase creates exactly one schema in a project's database: `rebase`. These
|
|
7
|
+
* three functions live in it alongside the framework's own tables, and that is
|
|
8
|
+
* the whole contract — a reader can look at a database and know precisely which
|
|
9
|
+
* namespace belongs to the framework and that nothing else was touched.
|
|
10
|
+
*
|
|
11
|
+
* It used to be two. `uid()`, `jwt()` and `roles()` sat in a schema called
|
|
12
|
+
* `auth`, which is Supabase's name, chosen so that a developer who had written
|
|
13
|
+
* Supabase RLS would recognise `auth.uid()`. The familiarity was real but the
|
|
14
|
+
* name was not Rebase's to take, and taking it had a concrete cost: pointing
|
|
15
|
+
* Rebase at a database that already had a Supabase `auth` schema meant
|
|
16
|
+
* `CREATE OR REPLACE FUNCTION auth.uid() RETURNS text` against Supabase's
|
|
17
|
+
* `RETURNS uuid`, which Postgres rejects outright —
|
|
18
|
+
*
|
|
19
|
+
* ERROR: cannot change return type of existing function
|
|
20
|
+
* HINT: Use DROP FUNCTION auth.uid() first.
|
|
21
|
+
*
|
|
22
|
+
* — and the failure landed inside a catch-all that logged a warning and carried
|
|
23
|
+
* on, leaving a database with auth tables, no helper functions, and policies
|
|
24
|
+
* calling functions that did not exist. Under `rebase db migrate` the same
|
|
25
|
+
* statements aborted the migration instead.
|
|
26
|
+
*
|
|
27
|
+
* `rebase.uid()` collides with nobody. A Supabase database keeps its `auth`
|
|
28
|
+
* schema untouched and gains a `rebase` one, which is what a gradual migration
|
|
29
|
+
* needs.
|
|
30
|
+
*
|
|
31
|
+
* ## Why functions at all, rather than inlining `current_setting`
|
|
32
|
+
*
|
|
33
|
+
* Because the indirection has already been spent once. `uid()` resolves
|
|
34
|
+
* `app.uid` and falls back to the pre-rename `app.user_id`, so that during a
|
|
35
|
+
* rolling deploy — old and new pods serving one database — both eras resolve
|
|
36
|
+
* the principal. That was a single `CREATE OR REPLACE`. Inlined into policy
|
|
37
|
+
* bodies it would have been a rewrite of every policy on every table.
|
|
38
|
+
*
|
|
39
|
+
* ## Why the name is not configurable
|
|
40
|
+
*
|
|
41
|
+
* A policy body is stored SQL: Postgres parses `USING (…)` once and keeps it, so
|
|
42
|
+
* these strings are written into every policy in every database Rebase has
|
|
43
|
+
* provisioned. Everything that reads policies back — the SQL-to-policy parser
|
|
44
|
+
* behind the admin UI, the drift checker, `rls-check` — would have to know the
|
|
45
|
+
* configured value to recognise its own output. One frozen name is the feature.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The schema Rebase owns. The only schema Rebase creates. */
|
|
49
|
+
export const REBASE_SCHEMA = "rebase";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The principal of the current request, as text, or NULL in the server context.
|
|
53
|
+
*
|
|
54
|
+
* Never NULL for a user request — an anonymous one carries
|
|
55
|
+
* {@link ANONYMOUS_USER_ID} — which is what makes `IS NULL` a reliable test for
|
|
56
|
+
* the trusted server plane and `IS NOT NULL` a tautology.
|
|
57
|
+
*/
|
|
58
|
+
export const RLS_UID_SQL = `${REBASE_SCHEMA}.uid()`;
|
|
59
|
+
|
|
60
|
+
/** The request's roles as a comma-separated string, for `string_to_array`. */
|
|
61
|
+
export const RLS_ROLES_SQL = `${REBASE_SCHEMA}.roles()`;
|
|
62
|
+
|
|
63
|
+
/** The request's JWT claims as `jsonb`, or `{}`. */
|
|
64
|
+
export const RLS_JWT_SQL = `${REBASE_SCHEMA}.jwt()`;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The pre-1.0 spellings, for recognising policies and hand-written SQL that
|
|
68
|
+
* predate the move.
|
|
69
|
+
*
|
|
70
|
+
* Kept because policies outlive the server that wrote them: a database migrated
|
|
71
|
+
* by an older release still holds `auth.uid()` in its policy bodies until the
|
|
72
|
+
* next push or boot recompiles them, and anything that reads policies back has
|
|
73
|
+
* to recognise both eras or report the framework's own output as foreign drift.
|
|
74
|
+
* Also used to give a project whose `securityRules` contain raw `auth.uid()` a
|
|
75
|
+
* message naming the replacement, instead of a parse failure.
|
|
76
|
+
*/
|
|
77
|
+
export const LEGACY_RLS_SCHEMA = "auth";
|
|
78
|
+
export const LEGACY_RLS_UID_SQL = `${LEGACY_RLS_SCHEMA}.uid()`;
|
|
79
|
+
export const LEGACY_RLS_ROLES_SQL = `${LEGACY_RLS_SCHEMA}.roles()`;
|
|
80
|
+
export const LEGACY_RLS_JWT_SQL = `${LEGACY_RLS_SCHEMA}.jwt()`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Rewrites the pre-1.0 function calls in a fragment of policy SQL.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately anchored on a word boundary and the schema qualifier, so a column
|
|
86
|
+
* called `auth_uid` or a table named `auth` is left alone.
|
|
87
|
+
*/
|
|
88
|
+
export function rewriteLegacyRlsFunctions(sql: string): string {
|
|
89
|
+
return sql.replace(
|
|
90
|
+
/\bauth\.(uid|jwt|roles)\s*\(\s*\)/gi,
|
|
91
|
+
(_match, fn: string) => `${REBASE_SCHEMA}.${fn.toLowerCase()}()`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a fragment of SQL still calls the pre-1.0 functions. */
|
|
96
|
+
export function usesLegacyRlsFunctions(sql: string): boolean {
|
|
97
|
+
return /\bauth\.(uid|jwt|roles)\s*\(\s*\)/i.test(sql);
|
|
98
|
+
}
|