@rebasepro/types 0.17.3 → 0.18.1
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/README.md +4 -0
- package/dist/call_context.d.ts +20 -0
- package/dist/controllers/client.d.ts +36 -4
- package/dist/controllers/data.d.ts +120 -10
- package/dist/errors.d.ts +83 -4
- package/dist/index.es.js +522 -160
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +2 -2
- package/dist/types/auth_adapter.d.ts +41 -6
- package/dist/types/backend.d.ts +48 -0
- package/dist/types/collections.d.ts +25 -1
- package/dist/types/cron.d.ts +34 -0
- package/dist/types/database_adapter.d.ts +39 -0
- package/dist/types/entity_callbacks.d.ts +14 -1
- package/dist/types/filter-operators.d.ts +24 -1
- package/dist/types/policy.d.ts +29 -1
- package/dist/types/properties.d.ts +216 -3
- package/dist/types/relations.d.ts +65 -7
- package/dist/types/resource_kinds.d.ts +173 -17
- package/dist/types/resources.d.ts +108 -7
- package/dist/types/rls-functions.d.ts +11 -0
- package/dist/types/storage_source.d.ts +12 -23
- package/package.json +24 -23
- package/src/call_context.ts +0 -120
- package/src/controllers/auth_state.ts +0 -24
- package/src/controllers/client.ts +0 -494
- package/src/controllers/collection_registry.ts +0 -62
- package/src/controllers/data.ts +0 -1012
- package/src/controllers/data_driver.ts +0 -576
- package/src/controllers/effective_role.ts +0 -4
- package/src/controllers/email.ts +0 -91
- package/src/controllers/index.ts +0 -11
- package/src/controllers/storage.ts +0 -252
- package/src/errors.ts +0 -119
- package/src/index.ts +0 -5
- package/src/types/admin_block.ts +0 -209
- package/src/types/api_keys.ts +0 -108
- package/src/types/auth_adapter.ts +0 -580
- package/src/types/backend.ts +0 -987
- package/src/types/backup.ts +0 -26
- package/src/types/channel_bus.ts +0 -202
- package/src/types/chips.ts +0 -34
- package/src/types/collection_contract.ts +0 -278
- package/src/types/collections.ts +0 -763
- package/src/types/component_ref.ts +0 -92
- package/src/types/cron.ts +0 -213
- package/src/types/data_source.ts +0 -357
- package/src/types/database_adapter.ts +0 -267
- package/src/types/entities.ts +0 -226
- package/src/types/entity_callbacks.ts +0 -229
- package/src/types/filter-operators.ts +0 -444
- package/src/types/history.ts +0 -66
- package/src/types/index.ts +0 -36
- package/src/types/indexes.ts +0 -180
- package/src/types/policy.ts +0 -328
- package/src/types/postgres_introspection.ts +0 -101
- package/src/types/project_manifest.ts +0 -598
- package/src/types/properties.ts +0 -1368
- package/src/types/relations.ts +0 -417
- package/src/types/resource_kinds.ts +0 -390
- package/src/types/resources.ts +0 -368
- package/src/types/rls-functions.ts +0 -98
- package/src/types/schema_editing.ts +0 -157
- package/src/types/schema_version.ts +0 -112
- package/src/types/search.ts +0 -247
- package/src/types/security_rules.ts +0 -344
- package/src/types/storage_authorize.ts +0 -77
- package/src/types/storage_source.ts +0 -248
- package/src/types/websockets.ts +0 -117
- package/src/users/index.ts +0 -2
- package/src/users/user.ts +0 -69
package/src/types/history.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity change history — the shape a history entry has on the wire.
|
|
3
|
-
*
|
|
4
|
-
* This was declared three times: once in `@rebasepro/server-postgres`, once in
|
|
5
|
-
* `@rebasepro/server-mongo`, and once again in the admin's `useHistory` hook.
|
|
6
|
-
* The two driver copies disagreed on the one field that matters for a consumer,
|
|
7
|
-
* `updated_at`, which was a `string` in Postgres and a `Date` in MongoDB — so
|
|
8
|
-
* nothing could read history without first choosing a driver.
|
|
9
|
-
*
|
|
10
|
-
* The contract is the wire shape, and on the wire it is an ISO-8601 string.
|
|
11
|
-
* A driver whose stored document differs (MongoDB keeps a `Date` and an
|
|
12
|
-
* `ObjectId`) declares that storage row for itself and maps to this on the way
|
|
13
|
-
* out; it is not the shared type.
|
|
14
|
-
*
|
|
15
|
-
* @group Backend
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* One recorded change to a row.
|
|
20
|
-
* @group Backend
|
|
21
|
-
*/
|
|
22
|
-
export interface EntityHistoryEntry {
|
|
23
|
-
id: string;
|
|
24
|
-
/** The table (Postgres) or collection (MongoDB) the row belongs to. */
|
|
25
|
-
table_name: string;
|
|
26
|
-
/** The row's id, as a string regardless of its native type. */
|
|
27
|
-
entity_id: string;
|
|
28
|
-
action: "create" | "update" | "delete";
|
|
29
|
-
/** Which fields changed. `null` for creates and deletes. */
|
|
30
|
-
changed_fields: string[] | null;
|
|
31
|
-
values: Record<string, unknown> | null;
|
|
32
|
-
previous_values: Record<string, unknown> | null;
|
|
33
|
-
updated_by: string | null;
|
|
34
|
-
/** ISO-8601. A driver storing a native date converts on read. */
|
|
35
|
-
updated_at: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Arguments to record one change.
|
|
40
|
-
* @group Backend
|
|
41
|
-
*/
|
|
42
|
-
export interface RecordHistoryParams {
|
|
43
|
-
tableName: string;
|
|
44
|
-
id: string;
|
|
45
|
-
action: "create" | "update" | "delete";
|
|
46
|
-
values?: Record<string, unknown> | null;
|
|
47
|
-
previousValues?: Record<string, unknown> | null;
|
|
48
|
-
updatedBy?: string | null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* How much history to keep. Pruning runs per row after each write.
|
|
53
|
-
* @group Backend
|
|
54
|
-
*/
|
|
55
|
-
export interface HistoryRetentionConfig {
|
|
56
|
-
/** Max entries per row. Oldest pruned first. Default 200. */
|
|
57
|
-
maxEntries: number;
|
|
58
|
-
/** Entries older than this many days are pruned. Default 90. */
|
|
59
|
-
ttlDays: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** @group Backend */
|
|
63
|
-
export interface FetchHistoryOptions {
|
|
64
|
-
limit?: number;
|
|
65
|
-
offset?: number;
|
|
66
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export * from "./entities";
|
|
2
|
-
export * from "./filter-operators";
|
|
3
|
-
export * from "./chips";
|
|
4
|
-
|
|
5
|
-
export * from "./properties";
|
|
6
|
-
export * from "./admin_block";
|
|
7
|
-
export * from "./collections";
|
|
8
|
-
export * from "./search";
|
|
9
|
-
export * from "./indexes";
|
|
10
|
-
export * from "./relations";
|
|
11
|
-
export * from "./policy";
|
|
12
|
-
export * from "./rls-functions";
|
|
13
|
-
export * from "./security_rules";
|
|
14
|
-
|
|
15
|
-
export * from "./entity_callbacks";
|
|
16
|
-
export * from "./websockets";
|
|
17
|
-
export * from "./backend";
|
|
18
|
-
export * from "./schema_editing";
|
|
19
|
-
export * from "./channel_bus";
|
|
20
|
-
export * from "./data_source";
|
|
21
|
-
export * from "./resources";
|
|
22
|
-
export * from "./resource_kinds";
|
|
23
|
-
export * from "./storage_source";
|
|
24
|
-
export * from "./cron";
|
|
25
|
-
export * from "./backup";
|
|
26
|
-
export * from "./component_ref";
|
|
27
|
-
export * from "./auth_adapter";
|
|
28
|
-
export * from "./database_adapter";
|
|
29
|
-
export * from "./api_keys";
|
|
30
|
-
export * from "./history";
|
|
31
|
-
export * from "./postgres_introspection";
|
|
32
|
-
export * from "./project_manifest";
|
|
33
|
-
export * from "./collection_contract";
|
|
34
|
-
export * from "./schema_version";
|
|
35
|
-
export * from "./storage_authorize";
|
|
36
|
-
|
package/src/types/indexes.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ordinary indexes, declared on a collection.
|
|
3
|
-
*
|
|
4
|
-
* Distinct from the two index-shaped things Rebase already builds. A `search`
|
|
5
|
-
* block builds a GIN index over a generated `tsvector`, and a `vector`
|
|
6
|
-
* property builds an ANN index over an embedding; both are structures the
|
|
7
|
-
* *feature* owns and neither is a query the developer wrote. This is the plain
|
|
8
|
-
* case — the btree behind a `where` clause — which had no declaration site at
|
|
9
|
-
* all, so the only way to have one was to write it by hand, where the next
|
|
10
|
-
* `rebase db push` planned it away.
|
|
11
|
-
*
|
|
12
|
-
* Every form here is core Postgres, deliberately. See {@link CollectionIndex}.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A key column of an index whose access method has no ordering.
|
|
17
|
-
*
|
|
18
|
-
* `gin` and `brin` reject `ASC`/`DESC`/`NULLS` outright — Postgres answers
|
|
19
|
-
* `access method "gin" does not support ASC/DESC options` — so those methods
|
|
20
|
-
* take this narrower shape and the combination is unrepresentable rather than
|
|
21
|
-
* refused at build time.
|
|
22
|
-
*/
|
|
23
|
-
export interface UnorderedIndexKey<Keys extends string = string> {
|
|
24
|
-
/**
|
|
25
|
-
* A property key on this collection — never a column name.
|
|
26
|
-
*
|
|
27
|
-
* Which column that resolves to depends on the property, and the two
|
|
28
|
-
* differ in exactly the case an index is most often wanted for: a
|
|
29
|
-
* `belongsTo` relation compiles to its resolved `localKey`
|
|
30
|
-
* (`primaryCategory` → `primary_category_id`), not to the snake-cased
|
|
31
|
-
* property key. Anything else resolves through `columnName`, or the
|
|
32
|
-
* snake-case default when it declares none.
|
|
33
|
-
*
|
|
34
|
-
* Writing the column name here would work for most properties and quietly
|
|
35
|
-
* index nothing for a foreign key, which is the one people reach for.
|
|
36
|
-
*/
|
|
37
|
-
prop: Keys | (string & {});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* A key column of an index, when its order matters.
|
|
42
|
-
*
|
|
43
|
-
* `direction` and `nulls` earn their place only when a query's `ORDER BY`
|
|
44
|
-
* mixes directions. A lone `DESC` index is redundant with its `ASC` twin —
|
|
45
|
-
* Postgres scans a btree backwards just as fast — and declaring both is
|
|
46
|
-
* refused.
|
|
47
|
-
*
|
|
48
|
-
* Writing the Postgres default down explicitly is free: the derived name
|
|
49
|
-
* hashes the *effective* order, so adding `direction: "asc"` to a column that
|
|
50
|
-
* was already ascending is not a redefinition and rebuilds nothing.
|
|
51
|
-
*/
|
|
52
|
-
export interface IndexKey<Keys extends string = string> extends UnorderedIndexKey<Keys> {
|
|
53
|
-
direction?: "asc" | "desc";
|
|
54
|
-
/** Postgres's own default: `last` under `asc`, `first` under `desc`. */
|
|
55
|
-
nulls?: "first" | "last";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The rows a partial index covers.
|
|
60
|
-
*
|
|
61
|
-
* Structure rather than a SQL string, and this is the most load-bearing choice
|
|
62
|
-
* in the type. A string would be replayed verbatim by Atlas in a scratch
|
|
63
|
-
* database, would be the one place a caller reaches for an extension operator
|
|
64
|
-
* class or a subquery, could not be checked against the collection's
|
|
65
|
-
* properties, and could not be fingerprinted — its own text would have to go
|
|
66
|
-
* into the derived name, so reformatting it would rename a live index.
|
|
67
|
-
*
|
|
68
|
-
* Structure keeps every reference resolvable at build time, keeps literals
|
|
69
|
-
* going through the same quoting as the rest of the DDL, and keeps the name
|
|
70
|
-
* stable under any rendering change.
|
|
71
|
-
*
|
|
72
|
-
* There is no `or`. An OR predicate almost always means the index should not
|
|
73
|
-
* be partial at all; a caller who genuinely needs one declares two indexes.
|
|
74
|
-
*/
|
|
75
|
-
export type IndexPredicate<Keys extends string = string> =
|
|
76
|
-
| { prop: Keys | (string & {}); op: "="; value: string | number | boolean }
|
|
77
|
-
| { prop: Keys | (string & {}); op: "!=" | "<" | "<=" | ">" | ">="; value: string | number }
|
|
78
|
-
| { prop: Keys | (string & {}); op: "is null" | "is not null" }
|
|
79
|
-
/**
|
|
80
|
-
* A non-empty list, enforced in the type. An empty `IN` is a predicate
|
|
81
|
-
* matching nothing: it builds an index over zero rows and reports success,
|
|
82
|
-
* which is the silent-empty-condition shape this codebase has been bitten
|
|
83
|
-
* by before.
|
|
84
|
-
*/
|
|
85
|
-
| { prop: Keys | (string & {}); op: "in"; value: readonly [string | number, ...(string | number)[]] }
|
|
86
|
-
| { and: readonly [IndexPredicate<Keys>, ...IndexPredicate<Keys>[]] };
|
|
87
|
-
|
|
88
|
-
interface BaseCollectionIndex<Keys extends string = string> {
|
|
89
|
-
/**
|
|
90
|
-
* The key columns, in order. This *is* the index's identity.
|
|
91
|
-
*
|
|
92
|
-
* Postgres can only use a leading subset, so `["ownerId", "createdAt"]`
|
|
93
|
-
* serves a query filtering on `ownerId`, and one filtering on both, and
|
|
94
|
-
* never one filtering on `createdAt` alone.
|
|
95
|
-
*
|
|
96
|
-
* Capped at five keys. Postgres allows thirty-two; past four the trailing
|
|
97
|
-
* columns are dead weight on every write, and the declaration is usually
|
|
98
|
-
* someone hoping a query gets faster by accretion. Payload columns that
|
|
99
|
-
* are not searched belong in `include`, which does not count against this.
|
|
100
|
-
*/
|
|
101
|
-
on: readonly [Keys | IndexKey<Keys>, ...(Keys | IndexKey<Keys>)[]];
|
|
102
|
-
|
|
103
|
-
where?: IndexPredicate<Keys>;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Why this index exists, in one line. Required, and the only required
|
|
107
|
-
* field carrying no SQL.
|
|
108
|
-
*
|
|
109
|
-
* An index is the only thing a Rebase config can declare that costs money
|
|
110
|
-
* forever and whose benefit is invisible from the config. `rebase doctor`
|
|
111
|
-
* prints this beside "0 scans in 34 days, 412 MB", which is the one moment
|
|
112
|
-
* anyone is in a position to decide whether to delete it. Without it
|
|
113
|
-
* nobody can decide, so nobody does, and the table accretes indexes for
|
|
114
|
-
* the life of the product.
|
|
115
|
-
*/
|
|
116
|
-
reason: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The default. Answers equality, range, `ORDER BY`, and uniqueness.
|
|
121
|
-
*/
|
|
122
|
-
export interface BtreeIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
|
|
123
|
-
using?: "btree";
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* A composite uniqueness guarantee.
|
|
127
|
-
*
|
|
128
|
-
* Single-column uniqueness is `validation.unique` on the property, and
|
|
129
|
-
* declaring it here is refused rather than accepted as a synonym.
|
|
130
|
-
* `validation.unique` compiles to an inline `UNIQUE` whose backing index
|
|
131
|
-
* Postgres — not Rebase — names `<table>_<column>_key`. That name is in
|
|
132
|
-
* every deployed database, appears in no contract file, and no release can
|
|
133
|
-
* reach in and rename it.
|
|
134
|
-
*/
|
|
135
|
-
unique?: boolean;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Payload columns carried in the leaf pages, for index-only scans. Not
|
|
139
|
-
* searchable and not ordered — they save a heap fetch at the cost of a
|
|
140
|
-
* fatter index. May not overlap `on`.
|
|
141
|
-
*/
|
|
142
|
-
include?: readonly (Keys | (string & {}))[];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Containment over an `array` property or a JSONB `map`, using core operator
|
|
147
|
-
* classes only. Trigram and full-text search are `search:`, not this.
|
|
148
|
-
*/
|
|
149
|
-
export interface GinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
|
|
150
|
-
using: "gin";
|
|
151
|
-
on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* A naturally-ordered column on an append-only table — tiny, and useless the
|
|
156
|
-
* moment rows arrive out of order.
|
|
157
|
-
*/
|
|
158
|
-
export interface BrinIndex<Keys extends string = string> extends BaseCollectionIndex<Keys> {
|
|
159
|
-
using: "brin";
|
|
160
|
-
on: readonly [Keys | UnorderedIndexKey<Keys>, ...(Keys | UnorderedIndexKey<Keys>)[]];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* An index on a collection's table.
|
|
165
|
-
*
|
|
166
|
-
* No `gist` and no `hash`: every interesting gist operator class ships in an
|
|
167
|
-
* extension, and hash indexes cannot be unique, composite, or ordered.
|
|
168
|
-
*
|
|
169
|
-
* The restriction to core Postgres is not conservatism, it is what keeps the
|
|
170
|
-
* whole model on the Atlas path. `rebase db push` materialises the desired
|
|
171
|
-
* state in a bare scratch database to plan against, `--exclude` does not
|
|
172
|
-
* suppress that replay, and `CREATE EXTENSION` cannot be put in the file — so
|
|
173
|
-
* an index needing `gin_trgm_ops` or `vector_cosine_ops` is refused at build
|
|
174
|
-
* time rather than emitted to fail later against a database the author has
|
|
175
|
-
* never heard of. Trigram search is `search:`; ANN is a `vector` property.
|
|
176
|
-
*/
|
|
177
|
-
export type CollectionIndex<Keys extends string = string> =
|
|
178
|
-
| BtreeIndex<Keys>
|
|
179
|
-
| GinIndex<Keys>
|
|
180
|
-
| BrinIndex<Keys>;
|
package/src/types/policy.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured, engine-agnostic policy expressions.
|
|
3
|
-
*
|
|
4
|
-
* A {@link PolicyExpression} is the single source of truth for a row-level
|
|
5
|
-
* security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL
|
|
6
|
-
* (authoritative enforcement) and independently evaluated in JavaScript (to
|
|
7
|
-
* drive the admin UI, and — in future — to enforce on engines without native
|
|
8
|
-
* RLS such as MongoDB). Because both the SQL and the JS decision derive from
|
|
9
|
-
* the *same* expression, the UI matches database enforcement by construction —
|
|
10
|
-
* no drift between two hand-written implementations.
|
|
11
|
-
*
|
|
12
|
-
* The only escape hatch that cannot be evaluated client-side is the
|
|
13
|
-
* {@link RawPolicyExpression} node (`{ kind: "raw" }`): it preserves full
|
|
14
|
-
* PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the
|
|
15
|
-
* JavaScript evaluator (never silently allowed) and reflected exactly in the UI
|
|
16
|
-
* via server-computed capability flags.
|
|
17
|
-
*
|
|
18
|
-
* @group Models
|
|
19
|
-
*/
|
|
20
|
-
export type PolicyExpression =
|
|
21
|
-
| TruePolicyExpression
|
|
22
|
-
| FalsePolicyExpression
|
|
23
|
-
| AndPolicyExpression
|
|
24
|
-
| OrPolicyExpression
|
|
25
|
-
| NotPolicyExpression
|
|
26
|
-
| ComparePolicyExpression
|
|
27
|
-
| RolesOverlapPolicyExpression
|
|
28
|
-
| RolesContainPolicyExpression
|
|
29
|
-
| AuthenticatedPolicyExpression
|
|
30
|
-
| ServerContextPolicyExpression
|
|
31
|
-
| ExistsInPolicyExpression
|
|
32
|
-
| RawPolicyExpression;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The id a request without a logged-in user reports as `rebase.uid()`.
|
|
36
|
-
*
|
|
37
|
-
* A user-context request always sets `app.uid`: blank would read back as
|
|
38
|
-
* `NULL`, and `NULL` is how the trusted server context is recognised, so an
|
|
39
|
-
* anonymous visitor would be promoted to server privileges. The driver
|
|
40
|
-
* therefore substitutes this sentinel at the single chokepoint where the GUC
|
|
41
|
-
* is set.
|
|
42
|
-
*
|
|
43
|
-
* The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
|
|
44
|
-
* tautology on the user path** — it is true for anonymous visitors too. Use
|
|
45
|
-
* {@link policy.authenticated} to mean "signed in", and
|
|
46
|
-
* {@link policy.serverContext} to mean "the trusted server context". Do not
|
|
47
|
-
* hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one
|
|
48
|
-
* literal is not enough.
|
|
49
|
-
*
|
|
50
|
-
* @group Models
|
|
51
|
-
*/
|
|
52
|
-
export const ANONYMOUS_USER_ID = "anonymous";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Every uid that has ever meant "nobody is signed in" — newest first.
|
|
56
|
-
*
|
|
57
|
-
* There are two because there were two. The types, the policy compiler, the
|
|
58
|
-
* JavaScript evaluator and the linter were all built on
|
|
59
|
-
* {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
|
|
60
|
-
* callers as `'anon'` — so `policy.authenticated()`, which compiled to
|
|
61
|
-
* `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
|
|
62
|
-
* sanctioned way to write "signed in" granted to everyone, and the linter
|
|
63
|
-
* flagged the spelling that actually worked as a foreign convention.
|
|
64
|
-
*
|
|
65
|
-
* The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here
|
|
66
|
-
* because policies outlive the server that generated them: a database still
|
|
67
|
-
* holding policies from before the fix, or a project whose server has not been
|
|
68
|
-
* upgraded yet, must not become a grant in either direction. Compile against
|
|
69
|
-
* this list, not against a single literal.
|
|
70
|
-
*
|
|
71
|
-
* No real user id is ever one of these, so a match is always "not signed in".
|
|
72
|
-
*
|
|
73
|
-
* @group Models
|
|
74
|
-
*/
|
|
75
|
-
export const ANONYMOUS_USER_IDS: readonly string[] = [ANONYMOUS_USER_ID, "anon"];
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Whether a uid stands for "no one is signed in", in any spelling rebase has
|
|
79
|
-
* used. `null`/`undefined` is the trusted server context, not an anonymous
|
|
80
|
-
* caller, and is therefore **not** anonymous — see {@link ANONYMOUS_USER_ID}.
|
|
81
|
-
*
|
|
82
|
-
* @group Models
|
|
83
|
-
*/
|
|
84
|
-
export function isAnonymousUid(uid: string | null | undefined): boolean {
|
|
85
|
-
return typeof uid === "string" && ANONYMOUS_USER_IDS.includes(uid);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Always allows. Compiles to `true`. @group Models */
|
|
89
|
-
export interface TruePolicyExpression {
|
|
90
|
-
kind: "true";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Always denies. Compiles to `false`. @group Models */
|
|
94
|
-
export interface FalsePolicyExpression {
|
|
95
|
-
kind: "false";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Logical AND — every operand must pass. @group Models */
|
|
99
|
-
export interface AndPolicyExpression {
|
|
100
|
-
kind: "and";
|
|
101
|
-
operands: readonly PolicyExpression[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Logical OR — at least one operand must pass. @group Models */
|
|
105
|
-
export interface OrPolicyExpression {
|
|
106
|
-
kind: "or";
|
|
107
|
-
operands: readonly PolicyExpression[];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Logical negation. @group Models */
|
|
111
|
-
export interface NotPolicyExpression {
|
|
112
|
-
kind: "not";
|
|
113
|
-
operand: PolicyExpression;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */
|
|
117
|
-
export type PolicyCompareOperator = "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Compares two operands, e.g. `owner_id = rebase.uid()`.
|
|
121
|
-
* @group Models
|
|
122
|
-
*/
|
|
123
|
-
export interface ComparePolicyExpression {
|
|
124
|
-
kind: "compare";
|
|
125
|
-
op: PolicyCompareOperator;
|
|
126
|
-
left: PolicyOperand;
|
|
127
|
-
right: PolicyOperand;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* True when the user holds *at least one* of the given application roles.
|
|
132
|
-
* Compiles to `string_to_array(rebase.roles(), ',') && ARRAY[...]`.
|
|
133
|
-
* @group Models
|
|
134
|
-
*/
|
|
135
|
-
export interface RolesOverlapPolicyExpression {
|
|
136
|
-
kind: "rolesOverlap";
|
|
137
|
-
roles: readonly string[];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* True when the user holds *all* of the given application roles.
|
|
142
|
-
* Compiles to `string_to_array(rebase.roles(), ',') @> ARRAY[...]`.
|
|
143
|
-
* @group Models
|
|
144
|
-
*/
|
|
145
|
-
export interface RolesContainPolicyExpression {
|
|
146
|
-
kind: "rolesContain";
|
|
147
|
-
roles: readonly string[];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* True when a signed-in user is making the request. Compiles to
|
|
152
|
-
* `rebase.uid() IS NOT NULL AND rebase.uid() <> 'anonymous'`.
|
|
153
|
-
*
|
|
154
|
-
* Both halves are load-bearing. `IS NOT NULL` excludes the server context;
|
|
155
|
-
* the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who
|
|
156
|
-
* *do* carry a non-null `rebase.uid()`. Checking only `IS NOT NULL` grants to
|
|
157
|
-
* everyone — see {@link ANONYMOUS_USER_ID}.
|
|
158
|
-
*
|
|
159
|
-
* `policy.not(policy.authenticated())` therefore means "anonymous visitor or
|
|
160
|
-
* the server context". To single out the server context, use
|
|
161
|
-
* {@link ServerContextPolicyExpression}.
|
|
162
|
-
* @group Models
|
|
163
|
-
*/
|
|
164
|
-
export interface AuthenticatedPolicyExpression {
|
|
165
|
-
kind: "authenticated";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* True only in the trusted **server context** — the built-in flows that run
|
|
170
|
-
* without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so
|
|
171
|
-
* `rebase.uid()` is `NULL` for them and only for them. Compiles to
|
|
172
|
-
* `rebase.uid() IS NULL`.
|
|
173
|
-
*
|
|
174
|
-
* This is what lets the owner connection satisfy a policy even under FORCE RLS.
|
|
175
|
-
* It is deliberately a primitive rather than `not(authenticated())`: the two
|
|
176
|
-
* meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},
|
|
177
|
-
* and conflating them is what turns a server-only grant into an anonymous one.
|
|
178
|
-
*
|
|
179
|
-
* The JavaScript evaluator always returns `false` for this node — a client is
|
|
180
|
-
* never the server context.
|
|
181
|
-
* @group Models
|
|
182
|
-
*/
|
|
183
|
-
export interface ServerContextPolicyExpression {
|
|
184
|
-
kind: "serverContext";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Membership / relational access: true when at least one row exists in another
|
|
189
|
-
* collection (a join/membership table) matching `where`. This is what lets you
|
|
190
|
-
* scope reads to "rows whose team the caller belongs to" without an N+1
|
|
191
|
-
* per-row lookup — it compiles to a single correlated `EXISTS` subquery.
|
|
192
|
-
*
|
|
193
|
-
* Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column
|
|
194
|
-
* of the joined collection, while {@link OuterFieldPolicyOperand}
|
|
195
|
-
* (`policy.outerField`) references a column of the row being checked (the outer
|
|
196
|
-
* table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to
|
|
197
|
-
* the caller.
|
|
198
|
-
*
|
|
199
|
-
* @example
|
|
200
|
-
* ```ts
|
|
201
|
-
* // documents visible only to members of the document's team:
|
|
202
|
-
* policy.existsIn({
|
|
203
|
-
* collection: "team_members",
|
|
204
|
-
* where: policy.and(
|
|
205
|
-
* policy.compare(policy.field("team_id"), "eq", policy.outerField("team_id")),
|
|
206
|
-
* policy.compare(policy.field("user_id"), "eq", policy.authUid()),
|
|
207
|
-
* ),
|
|
208
|
-
* })
|
|
209
|
-
* // → EXISTS (SELECT 1 FROM team_members _ex0
|
|
210
|
-
* // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = rebase.uid())
|
|
211
|
-
* ```
|
|
212
|
-
*
|
|
213
|
-
* Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript
|
|
214
|
-
* evaluator treats it as *unknown* (it cannot run a subquery client-side), so
|
|
215
|
-
* enforcement is always the database's.
|
|
216
|
-
* @group Models
|
|
217
|
-
*/
|
|
218
|
-
export interface ExistsInPolicyExpression {
|
|
219
|
-
kind: "existsIn";
|
|
220
|
-
/** Slug of the collection to search (the join / membership table). */
|
|
221
|
-
collection: string;
|
|
222
|
-
/** Condition evaluated against the joined collection's rows. */
|
|
223
|
-
where: PolicyExpression;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* A raw PostgreSQL boolean expression — the full-power escape hatch.
|
|
228
|
-
*
|
|
229
|
-
* Columns can be referenced as `{column_name}`. This is Postgres-only and
|
|
230
|
-
* **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary
|
|
231
|
-
* SQL, so it treats this node as *unknown* rather than guessing.
|
|
232
|
-
* @group Models
|
|
233
|
-
*/
|
|
234
|
-
export interface RawPolicyExpression {
|
|
235
|
-
kind: "raw";
|
|
236
|
-
sql: string;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* An operand referenced by a {@link ComparePolicyExpression}.
|
|
241
|
-
* @group Models
|
|
242
|
-
*/
|
|
243
|
-
export type PolicyOperand =
|
|
244
|
-
| FieldPolicyOperand
|
|
245
|
-
| OuterFieldPolicyOperand
|
|
246
|
-
| LiteralPolicyOperand
|
|
247
|
-
| AuthUidPolicyOperand
|
|
248
|
-
| AuthRolesPolicyOperand;
|
|
249
|
-
|
|
250
|
-
/** A column value on the row being evaluated. @group Models */
|
|
251
|
-
export interface FieldPolicyOperand {
|
|
252
|
-
kind: "field";
|
|
253
|
-
/** The property/column name (resolved to its DB column when compiled). */
|
|
254
|
-
name: string;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}
|
|
259
|
-
* — i.e. the row the RLS policy is being evaluated for, referenced from within the
|
|
260
|
-
* subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.
|
|
261
|
-
* @group Models
|
|
262
|
-
*/
|
|
263
|
-
export interface OuterFieldPolicyOperand {
|
|
264
|
-
kind: "outerField";
|
|
265
|
-
/** The property/column name on the outer collection. */
|
|
266
|
-
name: string;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** A constant value. @group Models */
|
|
270
|
-
export interface LiteralPolicyOperand {
|
|
271
|
-
kind: "literal";
|
|
272
|
-
value: string | number | boolean | null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** The current user's id — compiles to `rebase.uid()`. @group Models */
|
|
276
|
-
export interface AuthUidPolicyOperand {
|
|
277
|
-
kind: "authUid";
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* The current user's roles as an array — compiles to
|
|
282
|
-
* `string_to_array(rebase.roles(), ',')`.
|
|
283
|
-
* @group Models
|
|
284
|
-
*/
|
|
285
|
-
export interface AuthRolesPolicyOperand {
|
|
286
|
-
kind: "authRoles";
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ── Constructor helpers ──────────────────────────────────────────────
|
|
290
|
-
// Small, dependency-free builders so callers (and the desugaring in
|
|
291
|
-
// `@rebasepro/common`) can assemble expressions without object-literal noise.
|
|
292
|
-
|
|
293
|
-
/** @group Models */
|
|
294
|
-
export const policy = {
|
|
295
|
-
true: (): TruePolicyExpression => ({ kind: "true" }),
|
|
296
|
-
false: (): FalsePolicyExpression => ({ kind: "false" }),
|
|
297
|
-
and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: "and",
|
|
298
|
-
operands: operands as PolicyExpression[] }),
|
|
299
|
-
or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: "or",
|
|
300
|
-
operands: operands as PolicyExpression[] }),
|
|
301
|
-
not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: "not",
|
|
302
|
-
operand }),
|
|
303
|
-
compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>
|
|
304
|
-
({ kind: "compare",
|
|
305
|
-
op,
|
|
306
|
-
left,
|
|
307
|
-
right }),
|
|
308
|
-
rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: "rolesOverlap",
|
|
309
|
-
roles: roles as string[] }),
|
|
310
|
-
rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: "rolesContain",
|
|
311
|
-
roles: roles as string[] }),
|
|
312
|
-
authenticated: (): AuthenticatedPolicyExpression => ({ kind: "authenticated" }),
|
|
313
|
-
serverContext: (): ServerContextPolicyExpression => ({ kind: "serverContext" }),
|
|
314
|
-
existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>
|
|
315
|
-
({ kind: "existsIn",
|
|
316
|
-
collection: args.collection,
|
|
317
|
-
where: args.where }),
|
|
318
|
-
raw: (sql: string): RawPolicyExpression => ({ kind: "raw",
|
|
319
|
-
sql }),
|
|
320
|
-
field: (name: string): FieldPolicyOperand => ({ kind: "field",
|
|
321
|
-
name }),
|
|
322
|
-
outerField: (name: string): OuterFieldPolicyOperand => ({ kind: "outerField",
|
|
323
|
-
name }),
|
|
324
|
-
literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: "literal",
|
|
325
|
-
value }),
|
|
326
|
-
authUid: (): AuthUidPolicyOperand => ({ kind: "authUid" }),
|
|
327
|
-
authRoles: (): AuthRolesPolicyOperand => ({ kind: "authRoles" })
|
|
328
|
-
};
|