@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
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type { CollectionConfig } from "./collections";
|
|
2
|
-
import { serializeCollections } from "./collection_contract";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The schema version stamp.
|
|
6
|
-
*
|
|
7
|
-
* One function, used in three places that must agree or the whole drift-detection
|
|
8
|
-
* story is noise: `rebase build` writes it into a bundle manifest, the runtime
|
|
9
|
-
* serves it from the contract endpoint, and a generated SDK records the value it
|
|
10
|
-
* was built from. If any two of those computed it differently, every client would
|
|
11
|
-
* look permanently out of date.
|
|
12
|
-
*
|
|
13
|
-
* It covers **collections only** — the client's contract is the shape of the
|
|
14
|
-
* data, so editing a hook or a server function must not invalidate every SDK in
|
|
15
|
-
* every repository. That is a deliberate narrowing, not an oversight.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */
|
|
19
|
-
function canonicalize(value: unknown): string {
|
|
20
|
-
if (value === null || typeof value !== "object") {
|
|
21
|
-
return JSON.stringify(value) ?? "null";
|
|
22
|
-
}
|
|
23
|
-
if (Array.isArray(value)) {
|
|
24
|
-
return `[${value.map(canonicalize).join(",")}]`;
|
|
25
|
-
}
|
|
26
|
-
const entries = Object.entries(value as Record<string, unknown>)
|
|
27
|
-
.filter(([, v]) => v !== undefined)
|
|
28
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
29
|
-
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(",")}}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Reduce a collection to the parts a generated client is actually built from.
|
|
34
|
-
*
|
|
35
|
-
* The version answers one question — "is this SDK stale?" — so it must change
|
|
36
|
-
* exactly when the generated types could change, and never otherwise. Hashing a
|
|
37
|
-
* whole collection fails both halves of that:
|
|
38
|
-
*
|
|
39
|
-
* - Security rules, callbacks, icons, groups and UI settings do not appear in a
|
|
40
|
-
* generated client, so including them reports perfectly current SDKs as stale.
|
|
41
|
-
* - Worse, they are not stable *inputs*. The runtime applies default security
|
|
42
|
-
* rules when it loads collections, so the same source hashed before and after
|
|
43
|
-
* loading produced two different answers — a build-time stamp that could never
|
|
44
|
-
* match the server that served it.
|
|
45
|
-
*
|
|
46
|
-
* Codegen reads the slug (for the `Database` key and type names), the properties,
|
|
47
|
-
* and the relations. That is the projection.
|
|
48
|
-
*/
|
|
49
|
-
function projectForCodegen(collection: CollectionConfig): Record<string, unknown> {
|
|
50
|
-
const source = collection as CollectionConfig & {
|
|
51
|
-
relations?: unknown;
|
|
52
|
-
subcollections?: CollectionConfig[];
|
|
53
|
-
path?: string;
|
|
54
|
-
engine?: unknown;
|
|
55
|
-
dataSource?: unknown;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
slug: collection.slug ?? source.path,
|
|
60
|
-
properties: collection.properties,
|
|
61
|
-
relations: source.relations,
|
|
62
|
-
// The engine decides whether relations are resolved at all: codegen asks
|
|
63
|
-
// `getDataSourceCapabilities(collection.engine).supportsRelations`, and an
|
|
64
|
-
// engine that answers no drops every foreign-key column from the
|
|
65
|
-
// generated Row/Insert/Update types. Moving a collection to such an
|
|
66
|
-
// engine is a real change to the generated types, so it has to move the
|
|
67
|
-
// version. `dataSource` is what resolves to `engine`, so it counts too.
|
|
68
|
-
engine: source.engine,
|
|
69
|
-
dataSource: source.dataSource,
|
|
70
|
-
subcollections: source.subcollections?.map(projectForCodegen)
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Compute the canonical string a schema version hashes.
|
|
76
|
-
*
|
|
77
|
-
* Exposed separately so the hashing itself can differ by environment: Node has
|
|
78
|
-
* `crypto`, and callers without it can still compare canonical forms directly.
|
|
79
|
-
*/
|
|
80
|
-
export function canonicalSchemaPayload(collections: CollectionConfig[]): string {
|
|
81
|
-
const projected = serializeCollections(collections)
|
|
82
|
-
.map(collection => projectForCodegen(collection as CollectionConfig));
|
|
83
|
-
return canonicalize(projected);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* A short, non-cryptographic digest of the canonical payload.
|
|
88
|
-
*
|
|
89
|
-
* FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a
|
|
90
|
-
* security boundary: nothing trusts a schema version to prove anything, it only
|
|
91
|
-
* answers "is this the same schema as before". A hand-rolled hash keeps this
|
|
92
|
-
* module free of `node:crypto`, so the identical function runs in the browser,
|
|
93
|
-
* in the CLI, and in the runtime — which is the property that actually matters.
|
|
94
|
-
*/
|
|
95
|
-
export function computeSchemaVersion(collections: CollectionConfig[]): string {
|
|
96
|
-
const payload = canonicalSchemaPayload(collections);
|
|
97
|
-
|
|
98
|
-
let h1 = 0x811c9dc5;
|
|
99
|
-
let h2 = 0x01000193;
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < payload.length; i++) {
|
|
102
|
-
const code = payload.charCodeAt(i);
|
|
103
|
-
h1 ^= code;
|
|
104
|
-
// Multiply by the FNV prime using shifts to stay in 32-bit integer math.
|
|
105
|
-
h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;
|
|
106
|
-
h2 ^= code + i;
|
|
107
|
-
h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const hex = (n: number): string => n.toString(16).padStart(8, "0");
|
|
111
|
-
return `v1:${hex(h1)}${hex(h2)}`;
|
|
112
|
-
}
|
package/src/types/search.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Opt-in full-text search configuration.
|
|
3
|
-
*
|
|
4
|
-
* ## Why this is opt-in
|
|
5
|
-
*
|
|
6
|
-
* Without a `search` block, `.search()` behaves exactly as it always has: an
|
|
7
|
-
* `ILIKE '%term%'` OR-ed across the collection's top-level, non-enum `string`
|
|
8
|
-
* properties. That default is unchanged and will stay unchanged — declaring
|
|
9
|
-
* this block is the only way to get anything else.
|
|
10
|
-
*
|
|
11
|
-
* The default has three limits that no amount of tuning inside it can fix:
|
|
12
|
-
* it cannot reach inside `map` (JSONB) or `array` properties, it has no notion
|
|
13
|
-
* of relevance, and a leading `%` means it can never use an index. Collections
|
|
14
|
-
* that outgrow those limits declare what they want searched; collections that
|
|
15
|
-
* have not are left completely alone.
|
|
16
|
-
*
|
|
17
|
-
* ## What declaring it does
|
|
18
|
-
*
|
|
19
|
-
* One `tsvector` column, `GENERATED ALWAYS AS … STORED`, plus one GIN index on
|
|
20
|
-
* it. Postgres recomputes the column on every write of a source field, so it
|
|
21
|
-
* cannot drift from the row, and refuses any attempt to write it directly.
|
|
22
|
-
* `.search()` then compiles to `@@ websearch_to_tsquery(…)` against that
|
|
23
|
-
* column, which stems, drops stopwords, AND-es the terms, and ranks.
|
|
24
|
-
*
|
|
25
|
-
* These are stated consequences, not hidden ones: the column and the index
|
|
26
|
-
* appear in generated DDL, in `schema.generated.ts`, and in `rebase db push`
|
|
27
|
-
* output like any other declared object.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```ts
|
|
31
|
-
* const talents: PostgresCollectionConfig = {
|
|
32
|
-
* slug: "talents",
|
|
33
|
-
* table: "talents",
|
|
34
|
-
* properties: { … },
|
|
35
|
-
* search: {
|
|
36
|
-
* language: "spanish",
|
|
37
|
-
* unaccent: true,
|
|
38
|
-
* fields: [
|
|
39
|
-
* { path: "full_name", weight: "A" },
|
|
40
|
-
* "location",
|
|
41
|
-
* "questionnaire.certifications" // into the JSONB
|
|
42
|
-
* ]
|
|
43
|
-
* }
|
|
44
|
-
* };
|
|
45
|
-
* ```
|
|
46
|
-
*
|
|
47
|
-
* @group Search
|
|
48
|
-
*/
|
|
49
|
-
export interface SearchConfig {
|
|
50
|
-
/**
|
|
51
|
-
* The fields to index, in the author's own words. Nothing is inferred: a
|
|
52
|
-
* field is searched if and only if it is named here.
|
|
53
|
-
*
|
|
54
|
-
* A bare string is shorthand for `{ path, weight: "B" }`.
|
|
55
|
-
*
|
|
56
|
-
* A path may address:
|
|
57
|
-
* - a top-level `string` property — `"full_name"`
|
|
58
|
-
* - a `string[]` property — `"tags"` (every element is indexed)
|
|
59
|
-
* - a path into a `map` property — `"questionnaire.certifications"`,
|
|
60
|
-
* which indexes every string found at or below that point, including
|
|
61
|
-
* nested objects and arrays of strings. JSON *keys* are never indexed,
|
|
62
|
-
* only values.
|
|
63
|
-
*
|
|
64
|
-
* A path that does not resolve to one of those is a boot-time error, not
|
|
65
|
-
* a silent omission — a search field you believe is live and is not is the
|
|
66
|
-
* failure this whole block exists to prevent.
|
|
67
|
-
*/
|
|
68
|
-
fields: readonly (string | SearchField)[];
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The Postgres text search configuration, which decides stemming and
|
|
72
|
-
* stopwords. `"spanish"` stems `auditores` to `auditor` and drops `de`;
|
|
73
|
-
* `"simple"` does neither.
|
|
74
|
-
*
|
|
75
|
-
* Defaults to `"simple"`, which is the only choice that is never wrong:
|
|
76
|
-
* a stemmer applied to the wrong language silently mangles lexemes. Set it
|
|
77
|
-
* to your content's language to get stemming.
|
|
78
|
-
*
|
|
79
|
-
* @default "simple"
|
|
80
|
-
*/
|
|
81
|
-
language?: string;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Fold accents before indexing, so `auditoria` matches `auditoría`.
|
|
85
|
-
*
|
|
86
|
-
* This is not cosmetic in accented languages. Postgres stems the two
|
|
87
|
-
* spellings to *different* lexemes — `to_tsvector('spanish', 'auditoría')`
|
|
88
|
-
* yields `auditor` while `'auditoria'` yields `auditori` — so without this
|
|
89
|
-
* a query typed without accents misses the rows that carry them, which is
|
|
90
|
-
* most queries most users type.
|
|
91
|
-
*
|
|
92
|
-
* Requires the `unaccent` extension. Boot fails with an explicit message if
|
|
93
|
-
* it is not installed and cannot be created, rather than quietly indexing
|
|
94
|
-
* accented text as-is.
|
|
95
|
-
*
|
|
96
|
-
* @default false
|
|
97
|
-
*/
|
|
98
|
-
unaccent?: boolean;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Name of the generated column holding the `tsvector`.
|
|
102
|
-
*
|
|
103
|
-
* Only change this if `search_vector` collides with a column you already
|
|
104
|
-
* have. It is part of your schema once created: renaming it later is a
|
|
105
|
-
* column drop and recreate, which rewrites the table.
|
|
106
|
-
*
|
|
107
|
-
* @default "search_vector"
|
|
108
|
-
*/
|
|
109
|
-
column?: string;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Also match on trigram similarity, so near-misses and typos still rank —
|
|
113
|
-
* `iso14000` reaching `ISO 14001`, which no amount of stemming will do
|
|
114
|
-
* because they are simply different lexemes.
|
|
115
|
-
*
|
|
116
|
-
* Adds a second generated `text` column and a GIN trigram index alongside
|
|
117
|
-
* the `tsvector`, and requires the `pg_trgm` extension. Costs write time
|
|
118
|
-
* and disk; buys the single most common class of failed search.
|
|
119
|
-
*
|
|
120
|
-
* Also changes what `_score` means: the trigram similarity is added to
|
|
121
|
-
* `ts_rank`. It has to be. A typo matches nothing on the exact path, so
|
|
122
|
-
* every row this finds has a `ts_rank` of zero — ranking by that alone
|
|
123
|
-
* would order the results arbitrarily, which is the failure `fuzzy` exists
|
|
124
|
-
* to fix.
|
|
125
|
-
*
|
|
126
|
-
* @default false
|
|
127
|
-
*/
|
|
128
|
-
fuzzy?: boolean;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Similarity floor for {@link SearchConfig.fuzzy}, between 0 and 1. A row
|
|
132
|
-
* whose trigram similarity to the query falls below this never matches on
|
|
133
|
-
* the fuzzy path (it can still match on the exact one).
|
|
134
|
-
*
|
|
135
|
-
* Lower admits more typos and more noise. Ignored unless `fuzzy` is set.
|
|
136
|
-
*
|
|
137
|
-
* @default 0.3
|
|
138
|
-
*/
|
|
139
|
-
fuzzyThreshold?: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* One indexed field, with the weight it carries in the ranking.
|
|
144
|
-
*
|
|
145
|
-
* @group Search
|
|
146
|
-
*/
|
|
147
|
-
export interface SearchField {
|
|
148
|
-
/**
|
|
149
|
-
* Property name, or dotted path into a `map` property.
|
|
150
|
-
* @see SearchConfig.fields
|
|
151
|
-
*/
|
|
152
|
-
path: string;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Postgres weight class. `ts_rank` scores an `A` hit far above a `D` hit,
|
|
156
|
-
* which is how a name outranks a passing mention in a long description.
|
|
157
|
-
*
|
|
158
|
-
* The four classes are Postgres's own and there are exactly four.
|
|
159
|
-
*
|
|
160
|
-
* @default "B"
|
|
161
|
-
*/
|
|
162
|
-
weight?: SearchWeight;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Postgres tsvector weight classes, strongest to weakest.
|
|
167
|
-
*
|
|
168
|
-
* @group Search
|
|
169
|
-
*/
|
|
170
|
-
export type SearchWeight = "A" | "B" | "C" | "D";
|
|
171
|
-
|
|
172
|
-
/** The column name used when {@link SearchConfig.column} is not given. */
|
|
173
|
-
export const DEFAULT_SEARCH_COLUMN = "search_vector";
|
|
174
|
-
|
|
175
|
-
/** The text search configuration used when {@link SearchConfig.language} is not given. */
|
|
176
|
-
export const DEFAULT_SEARCH_LANGUAGE = "simple";
|
|
177
|
-
|
|
178
|
-
/** The weight a field carries when it does not name one. */
|
|
179
|
-
export const DEFAULT_SEARCH_WEIGHT: SearchWeight = "B";
|
|
180
|
-
|
|
181
|
-
/** The similarity floor used when {@link SearchConfig.fuzzyThreshold} is not given. */
|
|
182
|
-
export const DEFAULT_FUZZY_THRESHOLD = 0.3;
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Sort keys a query computes rather than reads from a column.
|
|
186
|
-
*
|
|
187
|
-
* `orderBy` is otherwise typed against the row — `keyof M` — which is exactly
|
|
188
|
-
* right for a column and exactly wrong for relevance: `_score` is produced by
|
|
189
|
-
* the query, so it appears in no generated row type and a project with a
|
|
190
|
-
* generated SDK could not name it. The runtime accepted it, the docs told
|
|
191
|
-
* people to use it, and the types rejected it.
|
|
192
|
-
*
|
|
193
|
-
* Kept as a named union rather than a loose `string` so the other half of the
|
|
194
|
-
* guarantee survives: a typo'd column is still a compile error, and remains a
|
|
195
|
-
* 400 at runtime rather than a silently unsorted list.
|
|
196
|
-
*
|
|
197
|
-
* `_distance` is deliberately not here. A vector search orders by distance on
|
|
198
|
-
* its own and overrides `orderBy` outright, so naming it would imply a choice
|
|
199
|
-
* the caller does not have.
|
|
200
|
-
*
|
|
201
|
-
* @group Search
|
|
202
|
-
*/
|
|
203
|
-
export type ComputedSortField = typeof RELEVANCE_SORT_FIELD;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* The relevance sort key. Valid only on a collection that declares a
|
|
207
|
-
* {@link SearchConfig} *and* on a query that carries a search string; anywhere
|
|
208
|
-
* else it is an unknown field and the request is refused.
|
|
209
|
-
*/
|
|
210
|
-
export const RELEVANCE_SORT_FIELD = "_score";
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* One field that matched, and the text around the hit.
|
|
214
|
-
*
|
|
215
|
-
* Returned per row as `_matches` when a query asks for it — see the `explain`
|
|
216
|
-
* option on `.search()`. Answers the question a ranked list otherwise leaves
|
|
217
|
-
* open: *why is this row here?* A candidate surfacing for "iso 14001" because
|
|
218
|
-
* of a certification is a different result from one surfacing because the
|
|
219
|
-
* string appears in a paragraph about something else, and the score alone
|
|
220
|
-
* cannot tell them apart.
|
|
221
|
-
*
|
|
222
|
-
* @group Search
|
|
223
|
-
*/
|
|
224
|
-
export interface SearchMatch {
|
|
225
|
-
/**
|
|
226
|
-
* The declared field path that matched, exactly as written in
|
|
227
|
-
* {@link SearchConfig.fields} — e.g. `"questionnaire.certifications"`.
|
|
228
|
-
* Map it to a label for display; the path is stable, a label is yours.
|
|
229
|
-
*/
|
|
230
|
-
field: string;
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* The matching text, with each hit wrapped in `<mark>…</mark>`.
|
|
234
|
-
*
|
|
235
|
-
* Built by Postgres's `ts_headline` over the same normalized text that was
|
|
236
|
-
* indexed. With {@link SearchConfig.unaccent} on that means the snippet
|
|
237
|
-
* reads with accents folded — `Auditoria` rather than `Auditoría`. That is
|
|
238
|
-
* deliberate: `ts_headline` over the *original* text cannot find a hit the
|
|
239
|
-
* unaccented query produced, so it returns the text with nothing marked at
|
|
240
|
-
* all. A readable snippet that highlights beats a prettier one that
|
|
241
|
-
* silently does not.
|
|
242
|
-
*
|
|
243
|
-
* Contains markup by construction. Render it as HTML or strip the tags —
|
|
244
|
-
* do not display it raw, and do not trust it as plain text.
|
|
245
|
-
*/
|
|
246
|
-
snippet: string;
|
|
247
|
-
}
|
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Row-level security rules for a collection.
|
|
3
|
-
*
|
|
4
|
-
* 325 lines of RLS policy contract that used to sit in the middle of
|
|
5
|
-
* `collections.ts`, between the admin panel's table view-models and the auth
|
|
6
|
-
* collection config. It has no relationship to the shape of a collection — it is
|
|
7
|
-
* the most-read part of the BaaS surface, and it now reads on its own.
|
|
8
|
-
*
|
|
9
|
-
* A rule is compiled to Postgres `CREATE POLICY` DDL and, for the structured
|
|
10
|
-
* flavour, independently evaluated in JavaScript, so the admin UI's idea of what
|
|
11
|
-
* a user may do derives from the same expression the database enforces.
|
|
12
|
-
*
|
|
13
|
-
* Note that rule *names* are not what you write: an unnamed rule compiles to
|
|
14
|
-
* `<table>_<op>_<sha1[0:7]>`, plus an injected `default_admin` baseline. Derive
|
|
15
|
-
* them with `getPolicyNamesForRule`/`getEffectiveSecurityRules` rather than
|
|
16
|
-
* matching on `rule.name`.
|
|
17
|
-
*/
|
|
18
|
-
import type { PolicyExpression } from "./policy";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* SQL operation that a policy applies to.
|
|
22
|
-
* @group Models
|
|
23
|
-
*/
|
|
24
|
-
export type SecurityOperation = "select" | "insert" | "update" | "delete" | "all";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Flexible Row Level Security rule for a collection.
|
|
28
|
-
*
|
|
29
|
-
* Built on PostgreSQL Row Level Security. Rules can range from
|
|
30
|
-
* simple convenience shortcuts to fully custom SQL expressions, giving you the
|
|
31
|
-
* full power of PostgreSQL Row Level Security.
|
|
32
|
-
*
|
|
33
|
-
* The authenticated user's identity is available in raw SQL via:
|
|
34
|
-
* - `rebase.uid()` — the user's ID
|
|
35
|
-
* - `rebase.roles()` — comma-separated app role IDs
|
|
36
|
-
* - `rebase.jwt()` — full JWT claims as JSONB
|
|
37
|
-
*
|
|
38
|
-
* These are set automatically per-transaction by the backend.
|
|
39
|
-
*
|
|
40
|
-
* **How rules combine:** PostgreSQL evaluates all matching policies for an
|
|
41
|
-
* operation. Permissive rules are OR'd together (any one passing is enough).
|
|
42
|
-
* Restrictive rules are AND'd (all must pass). This is standard PostgreSQL RLS behavior.
|
|
43
|
-
*
|
|
44
|
-
* **Mutual exclusivity:** `ownerField`, `access`, structured `condition`, and
|
|
45
|
-
* raw SQL (`using`/`withCheck`) cannot be combined. The type system enforces
|
|
46
|
-
* this — attempting to set conflicting fields will produce a compile-time
|
|
47
|
-
* error.
|
|
48
|
-
*
|
|
49
|
-
* **Which form to reach for:** prefer the structured {@link StructuredSecurityRule}
|
|
50
|
-
* (`condition`/`check`) or the shortcuts (`ownerField`, `access`, `roles`). These
|
|
51
|
-
* are engine-agnostic and evaluated identically by the database and the admin UI,
|
|
52
|
-
* so the UI never shows an action the database will reject. Raw SQL
|
|
53
|
-
* ({@link RawSQLSecurityRule}) keeps full PostgreSQL power but is Postgres-only
|
|
54
|
-
* and server-authoritative (the UI cannot evaluate arbitrary SQL locally).
|
|
55
|
-
*
|
|
56
|
-
* @group Models
|
|
57
|
-
*/
|
|
58
|
-
export type SecurityRule =
|
|
59
|
-
| OwnerSecurityRule
|
|
60
|
-
| PublicSecurityRule
|
|
61
|
-
| StructuredSecurityRule
|
|
62
|
-
| RawSQLSecurityRule
|
|
63
|
-
| RolesOnlySecurityRule;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Shared fields for all SecurityRule variants.
|
|
67
|
-
* @group Models
|
|
68
|
-
*/
|
|
69
|
-
export interface SecurityRuleBase {
|
|
70
|
-
/**
|
|
71
|
-
* Optional human-readable name for the policy.
|
|
72
|
-
* If not provided, one will be auto-generated from the table name and operation.
|
|
73
|
-
* Must be unique per table.
|
|
74
|
-
*
|
|
75
|
-
* When using `operations` (array), each generated policy will have the
|
|
76
|
-
* operation name appended, e.g. `"owner_access_select"`, `"owner_access_update"`.
|
|
77
|
-
*/
|
|
78
|
-
name?: string;
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Which SQL operation this policy applies to.
|
|
82
|
-
* Use this when the policy targets a single operation or all operations.
|
|
83
|
-
*
|
|
84
|
-
* For multiple specific operations, use `operations` (array) instead.
|
|
85
|
-
* If neither is specified, defaults to `"all"`.
|
|
86
|
-
*
|
|
87
|
-
* @default "all"
|
|
88
|
-
*/
|
|
89
|
-
operation?: SecurityOperation;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Array of SQL operations this policy applies to.
|
|
93
|
-
* The compiler will generate one PostgreSQL policy per operation, sharing
|
|
94
|
-
* the same configuration.
|
|
95
|
-
*
|
|
96
|
-
* This reduces boilerplate when the same rule applies to multiple (but not all)
|
|
97
|
-
* operations.
|
|
98
|
-
*
|
|
99
|
-
* Takes precedence over `operation` (singular) if both are specified.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* // Same rule for select and update
|
|
103
|
-
* { operations: ["select", "update"], ownerField: "user_id" }
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* // Equivalent to operation: "all"
|
|
107
|
-
* { operations: ["all"], ownerField: "user_id" }
|
|
108
|
-
*/
|
|
109
|
-
operations?: readonly SecurityOperation[];
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Whether this policy is `"permissive"` (default) or `"restrictive"`.
|
|
113
|
-
*
|
|
114
|
-
* - **permissive**: Multiple permissive policies for the same operation are
|
|
115
|
-
* OR'd together — if *any* passes, access is granted.
|
|
116
|
-
* - **restrictive**: Restrictive policies are AND'd with all permissive
|
|
117
|
-
* policies — they act as additional gates that *must* also pass.
|
|
118
|
-
*
|
|
119
|
-
* This is the standard PostgreSQL RLS model.
|
|
120
|
-
*
|
|
121
|
-
* @default "permissive"
|
|
122
|
-
*/
|
|
123
|
-
mode?: "permissive" | "restrictive";
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* **Shortcut.** Restrict this rule to users that have one of these
|
|
127
|
-
* application-level roles.
|
|
128
|
-
*
|
|
129
|
-
* **Important:** These are NOT native PostgreSQL database roles — names
|
|
130
|
-
* like `public`, `anon` or `authenticated` belong to {@link pgRoles} and
|
|
131
|
-
* produce a condition no user can satisfy if used here. These are
|
|
132
|
-
* application roles managed by Rebase, stored as an inline `roles TEXT[]`
|
|
133
|
-
* column on the users table, and injected into each transaction as
|
|
134
|
-
* `app.user_roles` — which `rebase.roles()` reads.
|
|
135
|
-
*
|
|
136
|
-
* There is no roles registry: a role exists once it is assigned to a user.
|
|
137
|
-
*
|
|
138
|
-
* Generates a safe array-overlap condition — the user passes if they hold
|
|
139
|
-
* *any* of the listed roles:
|
|
140
|
-
* `string_to_array(rebase.roles(), ',') && ARRAY['<role1>', '<role2>']`
|
|
141
|
-
*
|
|
142
|
-
* (Note: this is a true set intersection, NOT a regex/substring match, so
|
|
143
|
-
* a role named `admin` never matches `nonadmin` or `superadmin`.)
|
|
144
|
-
*
|
|
145
|
-
* Can be combined with `ownerField`, `access`, `condition`, or raw
|
|
146
|
-
* `using`/`withCheck`. When combined, the role check is AND'd with the
|
|
147
|
-
* other condition.
|
|
148
|
-
*
|
|
149
|
-
* @example
|
|
150
|
-
* // Only admins can delete
|
|
151
|
-
* { operation: "delete", roles: ["admin"] }
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* // Admins have unfiltered read access to all rows
|
|
155
|
-
* { operation: "select", roles: ["admin"], using: "true" }
|
|
156
|
-
*/
|
|
157
|
-
roles?: readonly string[];
|
|
158
|
-
|
|
159
|
-
// ── Advanced: native PostgreSQL role targeting ───────────────────────
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* **Advanced.** Native PostgreSQL database roles the policy applies to.
|
|
163
|
-
*
|
|
164
|
-
* By default, all generated policies target the `public` role (i.e.
|
|
165
|
-
* every database connection). This is correct for most setups where
|
|
166
|
-
* a single database role is used for all connections.
|
|
167
|
-
*
|
|
168
|
-
* **Important:** These are NOT the same as the application-level
|
|
169
|
-
* {@link roles} (admin, editor, viewer, etc.) — those are enforced in the
|
|
170
|
-
* USING/WITH CHECK clauses via `rebase.roles()`. This field controls the
|
|
171
|
-
* PostgreSQL `TO` clause in `CREATE POLICY ... TO role_name`.
|
|
172
|
-
*
|
|
173
|
-
* Use this if you have dedicated PostgreSQL roles (e.g. `app_read`,
|
|
174
|
-
* `app_write`) and want policies to target specific ones.
|
|
175
|
-
*
|
|
176
|
-
* @default ["public"]
|
|
177
|
-
*
|
|
178
|
-
* @example
|
|
179
|
-
* // Only apply this policy when connected as `app_role`
|
|
180
|
-
* { operation: "select", access: "public", pgRoles: ["app_role"] }
|
|
181
|
-
*/
|
|
182
|
-
pgRoles?: readonly string[];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Security rule that grants access based on row ownership.
|
|
187
|
-
* Generates a USING/WITH CHECK clause like: `<column> = rebase.uid()`
|
|
188
|
-
*
|
|
189
|
-
* Cannot be combined with `using`, `withCheck`, or `access`.
|
|
190
|
-
*
|
|
191
|
-
* @example
|
|
192
|
-
* { operation: "all", ownerField: "user_id" }
|
|
193
|
-
*
|
|
194
|
-
* @group Models
|
|
195
|
-
*/
|
|
196
|
-
export interface OwnerSecurityRule extends SecurityRuleBase {
|
|
197
|
-
/** The property (column) that stores the owner's user ID. */
|
|
198
|
-
ownerField: string;
|
|
199
|
-
access?: never;
|
|
200
|
-
using?: never;
|
|
201
|
-
withCheck?: never;
|
|
202
|
-
condition?: never;
|
|
203
|
-
check?: never;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Security rule that grants unrestricted row access (no row filtering).
|
|
208
|
-
* Generates `USING (true)`.
|
|
209
|
-
*
|
|
210
|
-
* This means "no row-level filter", NOT "anonymous/unauthenticated access".
|
|
211
|
-
* Authentication is still enforced at the API layer — this only controls which
|
|
212
|
-
* *rows* authenticated users can see.
|
|
213
|
-
*
|
|
214
|
-
* Cannot be combined with `using`, `withCheck`, or `ownerField`.
|
|
215
|
-
*
|
|
216
|
-
* @example
|
|
217
|
-
* // Public read (any authenticated user sees all rows)
|
|
218
|
-
* { operation: "select", access: "public" }
|
|
219
|
-
*
|
|
220
|
-
* @group Models
|
|
221
|
-
*/
|
|
222
|
-
export interface PublicSecurityRule extends SecurityRuleBase {
|
|
223
|
-
/** Grant unrestricted row access for this operation. */
|
|
224
|
-
access: "public";
|
|
225
|
-
ownerField?: never;
|
|
226
|
-
using?: never;
|
|
227
|
-
withCheck?: never;
|
|
228
|
-
condition?: never;
|
|
229
|
-
check?: never;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Security rule expressed as a structured, engine-agnostic
|
|
234
|
-
* {@link PolicyExpression}. This is the **recommended** way to write a
|
|
235
|
-
* non-trivial condition: it compiles to PostgreSQL `USING`/`WITH CHECK` SQL
|
|
236
|
-
* *and* is evaluated identically by the admin UI, so the UI can never show an
|
|
237
|
-
* action the database will reject.
|
|
238
|
-
*
|
|
239
|
-
* Cannot be combined with `ownerField`, `access`, or raw `using`/`withCheck`.
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* // Owner, or any user holding the `moderator` role
|
|
243
|
-
* {
|
|
244
|
-
* operation: "update",
|
|
245
|
-
* condition: policy.or(
|
|
246
|
-
* policy.compare(policy.field("user_id"), "eq", policy.authUid()),
|
|
247
|
-
* policy.rolesOverlap(["moderator"])
|
|
248
|
-
* )
|
|
249
|
-
* }
|
|
250
|
-
*
|
|
251
|
-
* @group Models
|
|
252
|
-
*/
|
|
253
|
-
export interface StructuredSecurityRule extends SecurityRuleBase {
|
|
254
|
-
/**
|
|
255
|
-
* Structured condition for the `USING` clause — which *existing* rows are
|
|
256
|
-
* visible / can be modified / deleted (SELECT, UPDATE, DELETE).
|
|
257
|
-
*/
|
|
258
|
-
condition: PolicyExpression;
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Structured condition for the `WITH CHECK` clause — which *new/updated*
|
|
262
|
-
* row values are allowed (INSERT, UPDATE). Defaults to `condition` when
|
|
263
|
-
* omitted, mirroring PostgreSQL's own behavior.
|
|
264
|
-
*/
|
|
265
|
-
check?: PolicyExpression;
|
|
266
|
-
|
|
267
|
-
ownerField?: never;
|
|
268
|
-
access?: never;
|
|
269
|
-
using?: never;
|
|
270
|
-
withCheck?: never;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Security rule using raw SQL expressions for full PostgreSQL RLS power.
|
|
275
|
-
*
|
|
276
|
-
* **Postgres-only and server-authoritative.** Arbitrary SQL cannot be
|
|
277
|
-
* evaluated by the admin UI, so a rule using this form is treated as *unknown*
|
|
278
|
-
* client-side (never silently allowed) and its effect on visible actions is
|
|
279
|
-
* reflected from the server. For conditions that should also drive the UI
|
|
280
|
-
* precisely, prefer the structured {@link StructuredSecurityRule}.
|
|
281
|
-
*
|
|
282
|
-
* Cannot be combined with `ownerField`, `access`, or structured `condition`.
|
|
283
|
-
*
|
|
284
|
-
* You can reference columns via `{column_name}` which will be resolved to
|
|
285
|
-
* `table.column_name` in the generated Drizzle code.
|
|
286
|
-
*
|
|
287
|
-
* @example
|
|
288
|
-
* // Rows published in the last 30 days are visible
|
|
289
|
-
* { operation: "select", using: "{published_at} > now() - interval '30 days'" }
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* // Only the owner, or users with 'moderator' role
|
|
293
|
-
* {
|
|
294
|
-
* operation: "select",
|
|
295
|
-
* using: "{user_id} = rebase.uid() OR rebase.roles() ~ 'moderator'"
|
|
296
|
-
* }
|
|
297
|
-
*
|
|
298
|
-
* @group Models
|
|
299
|
-
*/
|
|
300
|
-
export interface RawSQLSecurityRule extends SecurityRuleBase {
|
|
301
|
-
/**
|
|
302
|
-
* Raw SQL expression for the `USING` clause.
|
|
303
|
-
* This controls which *existing* rows are visible / can be modified / deleted.
|
|
304
|
-
* Applied to SELECT, UPDATE, and DELETE.
|
|
305
|
-
*/
|
|
306
|
-
using: string;
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Raw SQL expression for the `WITH CHECK` clause.
|
|
310
|
-
* This controls which *new/updated* row values are allowed.
|
|
311
|
-
* Applied to INSERT and UPDATE.
|
|
312
|
-
*
|
|
313
|
-
* If not provided on INSERT/UPDATE policies, falls back to `using`
|
|
314
|
-
* (which matches PostgreSQL's own default behavior).
|
|
315
|
-
*/
|
|
316
|
-
withCheck?: string;
|
|
317
|
-
|
|
318
|
-
ownerField?: never;
|
|
319
|
-
access?: never;
|
|
320
|
-
condition?: never;
|
|
321
|
-
check?: never;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Security rule that only filters by application roles, without any
|
|
326
|
-
* row-level condition (USING/WITH CHECK).
|
|
327
|
-
*
|
|
328
|
-
* Useful for simple "only admins can access this table" rules where
|
|
329
|
-
* no per-row filtering is needed.
|
|
330
|
-
*
|
|
331
|
-
* @example
|
|
332
|
-
* // Only admins can delete
|
|
333
|
-
* { operation: "delete", roles: ["admin"] }
|
|
334
|
-
*
|
|
335
|
-
* @group Models
|
|
336
|
-
*/
|
|
337
|
-
export interface RolesOnlySecurityRule extends SecurityRuleBase {
|
|
338
|
-
ownerField?: never;
|
|
339
|
-
access?: never;
|
|
340
|
-
using?: never;
|
|
341
|
-
withCheck?: never;
|
|
342
|
-
condition?: never;
|
|
343
|
-
check?: never;
|
|
344
|
-
}
|