@rebasepro/common 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/collections/CollectionRegistry.d.ts +1 -1
- package/dist/collections/default-collections.d.ts +15 -84
- package/dist/data/buildRebaseData.d.ts +1 -1
- package/dist/data/filter-dialect.d.ts +11 -0
- package/dist/data/sort-dialect.d.ts +15 -3
- package/dist/index.es.js +375 -63
- package/dist/index.es.js.map +1 -1
- package/dist/util/builders.d.ts +69 -24
- package/dist/util/callback-errors.d.ts +77 -0
- package/dist/util/callback-errors.test.d.ts +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/policy/evaluatePolicy.d.ts +6 -0
- package/dist/util/relations.d.ts +41 -0
- package/dist/util/table-name.test.d.ts +1 -0
- package/package.json +26 -22
- package/src/collections/CollectionRegistry.ts +0 -485
- package/src/collections/default-collections.ts +0 -109
- package/src/collections/index.ts +0 -2
- package/src/data/buildRebaseData.ts +0 -816
- package/src/data/buildRoutedRebaseData.ts +0 -103
- package/src/data/filter-conditions.ts +0 -46
- package/src/data/filter-dialect.ts +0 -737
- package/src/data/paginate.ts +0 -334
- package/src/data/query_builder.ts +0 -176
- package/src/data/resolveDataSource.ts +0 -135
- package/src/data/sort-dialect.ts +0 -237
- package/src/index.ts +0 -11
- package/src/table-classification.ts +0 -109
- package/src/types/json-logic-js.d.ts +0 -8
- package/src/util/auth-default-policies.ts +0 -215
- package/src/util/builders.ts +0 -82
- package/src/util/callbacks.ts +0 -122
- package/src/util/collections.ts +0 -117
- package/src/util/common.ts +0 -2
- package/src/util/conditions.ts +0 -168
- package/src/util/email.ts +0 -32
- package/src/util/entities.ts +0 -282
- package/src/util/enums.ts +0 -26
- package/src/util/identity.ts +0 -202
- package/src/util/index.ts +0 -21
- package/src/util/internal-tables.test.ts +0 -188
- package/src/util/internal-tables.ts +0 -197
- package/src/util/junction-policies.ts +0 -355
- package/src/util/paths.ts +0 -27
- package/src/util/permissions.test.ts +0 -866
- package/src/util/permissions.ts +0 -206
- package/src/util/pg-column-to-property.ts +0 -377
- package/src/util/policy/evaluatePolicy.ts +0 -194
- package/src/util/policy/index.ts +0 -4
- package/src/util/policy/policyToPostgres.ts +0 -263
- package/src/util/policy/securityRuleToConditions.ts +0 -67
- package/src/util/policy/sqlToPolicy.ts +0 -422
- package/src/util/relations.ts +0 -236
- package/src/util/resolutions.ts +0 -534
- package/src/util/resolve-relation.ts +0 -243
- package/src/util/storage.ts +0 -177
- package/src/util/string-column-length.ts +0 -31
package/src/data/sort-dialect.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import type { OrderBySortTuple, OrderBySpec, OrderByTuple } from "@rebasepro/types";
|
|
2
|
-
import { isRelationAggregateSort, sortKeyToString } from "@rebasepro/types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Sort-order wire codec.
|
|
6
|
-
*
|
|
7
|
-
* This is the ONLY module that knows about the colon-delimited wire format
|
|
8
|
-
* (`"field:direction"`) used in HTTP query parameters, and about the JSON-array
|
|
9
|
-
* form that carries a multi-column sort over the same parameter.
|
|
10
|
-
* Everything else speaks {@link OrderByTuple} exclusively.
|
|
11
|
-
*
|
|
12
|
-
* Mirrors the filter architecture in `filter-dialect.ts`.
|
|
13
|
-
*
|
|
14
|
-
* @module
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Collapse the one-key and many-key spellings of a sort into the list form.
|
|
19
|
-
*
|
|
20
|
-
* `["a", "desc"]` and `[["a", "desc"]]` mean the same thing and normalize to
|
|
21
|
-
* the same value; the two are told apart by whether the first element is
|
|
22
|
-
* itself an array, which no field name ever is.
|
|
23
|
-
*
|
|
24
|
-
* This is also where a {@link RelationAggregateSort} object stops being an
|
|
25
|
-
* object. Above this function a sort key may be either spelling; below it,
|
|
26
|
-
* every key is a string — which is what `OrderByTuple`, the REST parameter, the
|
|
27
|
-
* driver contract and the cursor all already were. Doing it here means the one
|
|
28
|
-
* place that already collapses the two *shapes* of a sort also collapses the
|
|
29
|
-
* two *spellings* of a key, rather than every consumer learning about both.
|
|
30
|
-
*
|
|
31
|
-
* @returns The keys in order of significance, or `undefined` for no sort. An
|
|
32
|
-
* empty list also returns `undefined` — "sort by nothing" is no sort, and
|
|
33
|
-
* letting `[]` through would have every layer below re-deciding what it meant.
|
|
34
|
-
*/
|
|
35
|
-
export function normalizeOrderBy(orderBy?: OrderBySpec): OrderByTuple[] | undefined {
|
|
36
|
-
if (!orderBy || orderBy.length === 0) return undefined;
|
|
37
|
-
// An aggregate key is an object, so the first element being an array still
|
|
38
|
-
// tells the list form from the single-tuple one — no field name is an
|
|
39
|
-
// array, and neither is an aggregate key.
|
|
40
|
-
const list = Array.isArray(orderBy[0])
|
|
41
|
-
? orderBy as OrderBySortTuple[]
|
|
42
|
-
: [orderBy as OrderBySortTuple];
|
|
43
|
-
if (list.length === 0) return undefined;
|
|
44
|
-
return list.map(([key, direction]) => [sortKeyToString(key), direction] as OrderByTuple);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* The most significant sort key, for a caller that can only express one —
|
|
49
|
-
* a column header's arrow, a URL parameter, a driver that has not been taught
|
|
50
|
-
* the list form.
|
|
51
|
-
*/
|
|
52
|
-
export function primaryOrderBy(orderBy?: OrderBySpec): OrderByTuple | undefined {
|
|
53
|
-
return normalizeOrderBy(orderBy)?.[0];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Collapse the driver-level `{orderBy, order}` pair into the list form.
|
|
58
|
-
*
|
|
59
|
-
* The driver contract spells a single-column sort as a field name plus a
|
|
60
|
-
* separate direction, and a multi-column one as a list of tuples that leaves
|
|
61
|
-
* `order` meaningless. Every driver reads both through here so neither
|
|
62
|
-
* spelling has to be handled twice.
|
|
63
|
-
*
|
|
64
|
-
* An absent direction means ascending — the same thing a bare `?orderBy=name`
|
|
65
|
-
* has always meant over HTTP. The Postgres driver used to read the same pair as
|
|
66
|
-
* *descending* while Mongo read it as ascending, so one field name and no
|
|
67
|
-
* direction described two different queries depending on which database was
|
|
68
|
-
* underneath. Neither had a caller: every path in the workspace passes a
|
|
69
|
-
* direction, which is why the disagreement went unnoticed rather than being
|
|
70
|
-
* load-bearing.
|
|
71
|
-
*/
|
|
72
|
-
export function normalizeDriverOrderBy(
|
|
73
|
-
orderBy?: string | OrderByTuple[],
|
|
74
|
-
order?: "asc" | "desc"
|
|
75
|
-
): OrderByTuple[] | undefined {
|
|
76
|
-
if (!orderBy) return undefined;
|
|
77
|
-
if (typeof orderBy === "string") return [[orderBy, order === "desc" ? "desc" : "asc"]];
|
|
78
|
-
return orderBy.length > 0 ? orderBy : undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** A sort whose *shape* is unusable, as opposed to one naming a field that does not exist. */
|
|
82
|
-
export class OrderBySpecError extends Error {
|
|
83
|
-
readonly code = "INVALID_ORDER_BY";
|
|
84
|
-
constructor(detail: string) {
|
|
85
|
-
super(
|
|
86
|
-
`Invalid \`orderBy\`: ${detail}. Expected a field name, or a list of ` +
|
|
87
|
-
"[field, direction] pairs like [[\"roles\",\"asc\"],[\"created_at\",\"desc\"]]"
|
|
88
|
-
);
|
|
89
|
-
this.name = "OrderBySpecError";
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Validate an `orderBy` that arrived from outside this process — a WebSocket
|
|
95
|
-
* subscribe frame, a driver call from untyped JavaScript — and return it in the
|
|
96
|
-
* list form.
|
|
97
|
-
*
|
|
98
|
-
* Strict on purpose, in the same way the REST `parseOrderByParam` is: the
|
|
99
|
-
* failure mode for a shape nobody checks is not a crash but a *silently
|
|
100
|
-
* different query*. A malformed entry read as a field name resolves to no
|
|
101
|
-
* column, and under the lenient unknown-field mode the sort is then dropped and
|
|
102
|
-
* the rows come back in whatever order the database pleased — sorted, as far as
|
|
103
|
-
* the subscriber can tell, by whatever they asked for.
|
|
104
|
-
*/
|
|
105
|
-
export function parseOrderBySpecStrict(raw: unknown, order?: "asc" | "desc"): OrderByTuple[] | undefined {
|
|
106
|
-
if (raw === undefined || raw === null || raw === "") return undefined;
|
|
107
|
-
// The string spelling is the driver contract's, so it takes its direction
|
|
108
|
-
// from the same companion `order` — and defaults the same way it does.
|
|
109
|
-
if (typeof raw === "string") return normalizeDriverOrderBy(raw, order);
|
|
110
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
111
|
-
throw new OrderBySpecError(`${typeof raw} is not a field name or a list of sort keys`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// The single-tuple spelling, `["created_at", "desc"]` — or the same shape
|
|
115
|
-
// with an aggregate key in place of the field name.
|
|
116
|
-
if (typeof raw[0] === "string" || isRelationAggregateSort(raw[0])) return [toStrictTuple(raw, 0)];
|
|
117
|
-
|
|
118
|
-
return raw.map(toStrictTuple);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function toStrictTuple(raw: unknown, index: number): OrderByTuple {
|
|
122
|
-
if (!Array.isArray(raw)) {
|
|
123
|
-
throw new OrderBySpecError(`entry ${index} has no field name`);
|
|
124
|
-
}
|
|
125
|
-
// The object spelling of an aggregate key, from an untyped caller that did
|
|
126
|
-
// not go through `normalizeOrderBy`. Encoded rather than refused: it is a
|
|
127
|
-
// sort this understands, and rejecting the shape a typed caller writes
|
|
128
|
-
// would be a distinction between the two spellings that nothing else makes.
|
|
129
|
-
const key = isRelationAggregateSort(raw[0]) ? sortKeyToString(raw[0]) : raw[0];
|
|
130
|
-
if (typeof key !== "string" || key.trim() === "") {
|
|
131
|
-
throw new OrderBySpecError(`entry ${index} has no field name`);
|
|
132
|
-
}
|
|
133
|
-
const direction = raw[1];
|
|
134
|
-
if (direction !== undefined && direction !== "asc" && direction !== "desc") {
|
|
135
|
-
throw new OrderBySpecError(`entry ${index} has direction '${String(direction)}'`);
|
|
136
|
-
}
|
|
137
|
-
return [key, direction ?? "asc"];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Serialize a sort to the wire.
|
|
142
|
-
*
|
|
143
|
-
* A single key keeps the `"field:direction"` shorthand it has always used —
|
|
144
|
-
* short, readable in a URL, and what every existing client and test expects.
|
|
145
|
-
* Several keys are emitted as the canonical JSON array the server already
|
|
146
|
-
* accepts, because the shorthand has no separator to spare: a comma-joined
|
|
147
|
-
* `"a:asc,b:desc"` parses as one field named `a` with the direction
|
|
148
|
-
* `"asc,b:desc"`, which the server refuses.
|
|
149
|
-
*
|
|
150
|
-
* **Runtime tolerance:** if the input is already a well-formed wire string
|
|
151
|
-
* (from an untyped JS caller), it is returned unchanged.
|
|
152
|
-
* This is undocumented tolerance, not public API — don't rely on it.
|
|
153
|
-
*
|
|
154
|
-
* @param orderBy - A canonical tuple or list of tuples, or at runtime
|
|
155
|
-
* possibly a pre-serialized string (undocumented tolerance).
|
|
156
|
-
* @returns The wire-format string, or `undefined` if the input is falsy.
|
|
157
|
-
*
|
|
158
|
-
* @remarks
|
|
159
|
-
* Field names containing `:` are representable in the tuple form but
|
|
160
|
-
* **not** in the single-key wire encoding — this is an inherent limitation of
|
|
161
|
-
* the colon-delimited shorthand and is not resolved here.
|
|
162
|
-
*/
|
|
163
|
-
export function serializeOrderBy(orderBy?: OrderBySpec | string): string | undefined {
|
|
164
|
-
if (!orderBy) return undefined;
|
|
165
|
-
// Runtime tolerance: pass through a pre-serialized wire string unchanged.
|
|
166
|
-
if (typeof orderBy === "string") return orderBy;
|
|
167
|
-
// `normalizeOrderBy` has already encoded any aggregate key to its string
|
|
168
|
-
// spelling, which is why the shorthand below can assume a string: neither
|
|
169
|
-
// `min(applications.created_at)` nor `count(applications)` contains a `:`.
|
|
170
|
-
const list = normalizeOrderBy(orderBy);
|
|
171
|
-
if (!list) return undefined;
|
|
172
|
-
if (list.length === 1) return `${list[0][0]}:${list[0][1]}`;
|
|
173
|
-
return JSON.stringify(list.map(([field, direction]) => ({ field,
|
|
174
|
-
direction })));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Deserialize a wire-format `"field:direction"` string into an {@link OrderByTuple}.
|
|
179
|
-
*
|
|
180
|
-
* Lenient parsing (matches existing server behaviour):
|
|
181
|
-
* - Bare field name (no colon): `"name"` → `["name", "asc"]`
|
|
182
|
-
* - Unknown direction: `"name:foo"` → `["name", "asc"]`
|
|
183
|
-
* - Empty / falsy input: → `undefined`
|
|
184
|
-
*
|
|
185
|
-
* Reads the single-key shorthand only. For a value that may carry several keys,
|
|
186
|
-
* use {@link deserializeOrderByList} — handed a JSON array this returns the
|
|
187
|
-
* whole array as one nonsensical field name.
|
|
188
|
-
*
|
|
189
|
-
* @param raw - The wire-format string from an HTTP query parameter.
|
|
190
|
-
* @returns The canonical tuple, or `undefined` if the input is empty/falsy.
|
|
191
|
-
*/
|
|
192
|
-
export function deserializeOrderBy(raw?: string): OrderByTuple | undefined {
|
|
193
|
-
if (!raw) return undefined;
|
|
194
|
-
const idx = raw.indexOf(":");
|
|
195
|
-
if (idx === -1) return [raw, "asc"];
|
|
196
|
-
const field = raw.slice(0, idx);
|
|
197
|
-
const dir = raw.slice(idx + 1);
|
|
198
|
-
return [field, dir === "desc" ? "desc" : "asc"];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Deserialize either wire spelling — the single-key shorthand or the JSON
|
|
203
|
-
* array — into the list form.
|
|
204
|
-
*
|
|
205
|
-
* Lenient in the same way {@link deserializeOrderBy} is: this is the client end
|
|
206
|
-
* of the codec, where the value was produced by {@link serializeOrderBy} a
|
|
207
|
-
* moment earlier. The *server* end parses the same shapes strictly, in
|
|
208
|
-
* `parseOrderByParam`, because there the value came from a stranger and a
|
|
209
|
-
* direction it cannot read has to be refused rather than quietly turned into
|
|
210
|
-
* `"asc"`.
|
|
211
|
-
*/
|
|
212
|
-
export function deserializeOrderByList(raw?: string): OrderByTuple[] | undefined {
|
|
213
|
-
if (!raw) return undefined;
|
|
214
|
-
const trimmed = raw.trim();
|
|
215
|
-
if (trimmed.startsWith("[")) {
|
|
216
|
-
try {
|
|
217
|
-
const parsed = JSON.parse(trimmed);
|
|
218
|
-
if (Array.isArray(parsed)) {
|
|
219
|
-
const list = parsed
|
|
220
|
-
.map((entry): OrderByTuple | undefined => {
|
|
221
|
-
if (typeof entry === "string") return deserializeOrderBy(entry);
|
|
222
|
-
if (entry && typeof entry === "object" && typeof entry.field === "string") {
|
|
223
|
-
return [entry.field, entry.direction === "desc" ? "desc" : "asc"];
|
|
224
|
-
}
|
|
225
|
-
return undefined;
|
|
226
|
-
})
|
|
227
|
-
.filter((entry): entry is OrderByTuple => entry !== undefined);
|
|
228
|
-
return list.length > 0 ? list : undefined;
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
// Not JSON after all — fall through to the shorthand, which is what
|
|
232
|
-
// a field name that merely begins with "[" would be.
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
const single = deserializeOrderBy(trimmed);
|
|
236
|
-
return single ? [single] : undefined;
|
|
237
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export * from "./util";
|
|
2
|
-
export * from "./collections";
|
|
3
|
-
export * from "./data/buildRebaseData";
|
|
4
|
-
export * from "./data/buildRoutedRebaseData";
|
|
5
|
-
export * from "./data/resolveDataSource";
|
|
6
|
-
export * from "./data/query_builder";
|
|
7
|
-
export * from "./data/paginate";
|
|
8
|
-
export * from "./data/filter-conditions";
|
|
9
|
-
export * from "./data/filter-dialect";
|
|
10
|
-
export * from "./data/sort-dialect";
|
|
11
|
-
export * from "./table-classification";
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table Classification
|
|
3
|
-
*
|
|
4
|
-
* Shared constants and pure functions for classifying database tables.
|
|
5
|
-
* Used by both the server-side PostgresBackendDriver and the Studio RLS editor.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Possible categories a database table can belong to. */
|
|
9
|
-
export type TableCategory = "rebase-internal" | "junction" | "user";
|
|
10
|
-
|
|
11
|
-
/** Schemas that are always considered Rebase-internal. */
|
|
12
|
-
export const REBASE_INTERNAL_SCHEMAS: readonly string[] = ["rebase", "auth"];
|
|
13
|
-
|
|
14
|
-
/** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
|
|
15
|
-
export const REBASE_INTERNAL_PREFIXES: readonly string[] = [
|
|
16
|
-
"_rebase_",
|
|
17
|
-
"_auth_",
|
|
18
|
-
"drizzle_",
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Synchronously classify a table based on naming conventions.
|
|
23
|
-
*
|
|
24
|
-
* @param tableName - The unqualified name of the table.
|
|
25
|
-
* @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
|
|
26
|
-
* @returns `"rebase-internal"` when the table belongs to a reserved schema or
|
|
27
|
-
* carries a reserved prefix; `"user"` otherwise.
|
|
28
|
-
*
|
|
29
|
-
* @remarks
|
|
30
|
-
* Junction-table detection requires an async database query and is therefore
|
|
31
|
-
* **not** handled by this function. Use {@link detectJunctionTables} to obtain
|
|
32
|
-
* the set of junction tables, then reclassify as needed.
|
|
33
|
-
*/
|
|
34
|
-
export function classifyTable(
|
|
35
|
-
tableName: string,
|
|
36
|
-
schemaName: string,
|
|
37
|
-
): TableCategory {
|
|
38
|
-
if (
|
|
39
|
-
REBASE_INTERNAL_SCHEMAS.includes(schemaName) ||
|
|
40
|
-
REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))
|
|
41
|
-
) {
|
|
42
|
-
return "rebase-internal";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return "user";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Convenience predicate that checks whether a table is Rebase-internal.
|
|
50
|
-
*
|
|
51
|
-
* @param tableName - The unqualified name of the table.
|
|
52
|
-
* @param schemaName - The schema the table belongs to.
|
|
53
|
-
* @returns `true` if the table is classified as `"rebase-internal"`.
|
|
54
|
-
*/
|
|
55
|
-
export function isRebaseInternalTable(
|
|
56
|
-
tableName: string,
|
|
57
|
-
schemaName: string,
|
|
58
|
-
): boolean {
|
|
59
|
-
return classifyTable(tableName, schemaName) === "rebase-internal";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** SQL query that detects junction tables in the `public` schema. */
|
|
63
|
-
export const JUNCTION_TABLES_SQL = `
|
|
64
|
-
SELECT t.table_name
|
|
65
|
-
FROM information_schema.tables t
|
|
66
|
-
WHERE t.table_schema = 'public'
|
|
67
|
-
AND t.table_type = 'BASE TABLE'
|
|
68
|
-
AND NOT EXISTS (
|
|
69
|
-
SELECT 1
|
|
70
|
-
FROM information_schema.columns c
|
|
71
|
-
WHERE c.table_schema = t.table_schema
|
|
72
|
-
AND c.table_name = t.table_name
|
|
73
|
-
AND c.column_name NOT IN (
|
|
74
|
-
SELECT kcu.column_name
|
|
75
|
-
FROM information_schema.key_column_usage kcu
|
|
76
|
-
JOIN information_schema.table_constraints tc
|
|
77
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
78
|
-
AND tc.table_schema = kcu.table_schema
|
|
79
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
80
|
-
AND kcu.table_schema = t.table_schema
|
|
81
|
-
AND kcu.table_name = t.table_name
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Asynchronously detect junction (link) tables in the `public` schema.
|
|
88
|
-
*
|
|
89
|
-
* A junction table is defined as a table where **every** column participates in
|
|
90
|
-
* at least one foreign-key constraint.
|
|
91
|
-
*
|
|
92
|
-
* @param executeSql - A callback that executes a raw SQL string and returns the
|
|
93
|
-
* resulting rows.
|
|
94
|
-
* @returns A `Set` containing the names of all detected junction tables.
|
|
95
|
-
*/
|
|
96
|
-
export async function detectJunctionTables(
|
|
97
|
-
executeSql: (sql: string) => Promise<Record<string, unknown>[]>,
|
|
98
|
-
): Promise<Set<string>> {
|
|
99
|
-
const rows = await executeSql(JUNCTION_TABLES_SQL);
|
|
100
|
-
const junctionTables = new Set<string>();
|
|
101
|
-
|
|
102
|
-
for (const row of rows) {
|
|
103
|
-
if (typeof row.table_name === "string") {
|
|
104
|
-
junctionTables.add(row.table_name);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return junctionTables;
|
|
109
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { CollectionConfig, SecurityRule, SecurityOperation, AuthCollectionConfig, PolicyExpression, isPostgresCollectionConfig, policy } from "@rebasepro/types";
|
|
2
|
-
import { getTableName } from "./relations";
|
|
3
|
-
import { getPolicyNamesForRules } from "@rebasepro/utils";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Default RLS policies injected by the schema generator.
|
|
7
|
-
*
|
|
8
|
-
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
9
|
-
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
10
|
-
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
11
|
-
* authorization model. The server context (auth flows, migrations, raw
|
|
12
|
-
* `rebase.sql`) runs as the owner and bypasses RLS.
|
|
13
|
-
*
|
|
14
|
-
* `rebase.dataAsAdmin` is **not** in that set, despite the name: it is scoped as
|
|
15
|
-
* `{ uid: "service", roles: ["admin"] }`, so it runs as `rebase_user` like any
|
|
16
|
-
* other caller and clears the baseline below through the *admin* arm, not the
|
|
17
|
-
* server arm. Which is why `disableDefaultPolicies` plus a lone
|
|
18
|
-
* `policy.serverContext()` rule locks it out too.
|
|
19
|
-
*
|
|
20
|
-
* Because RLS default-denies, every collection is **locked by default**: with
|
|
21
|
-
* no rules, only the server context and admins can touch it. The generator
|
|
22
|
-
* injects that safe baseline:
|
|
23
|
-
*
|
|
24
|
-
* **For every collection**
|
|
25
|
-
* 1. A permissive **server-or-admin SELECT** grant.
|
|
26
|
-
* 2. A permissive **server-or-admin write** grant (insert/update/delete).
|
|
27
|
-
*
|
|
28
|
-
* Author `securityRules` are permissive and OR together, so explicit rules only
|
|
29
|
-
* *broaden* access from this locked baseline (e.g. "users read/write their own
|
|
30
|
-
* rows").
|
|
31
|
-
*
|
|
32
|
-
* **For auth collections additionally**
|
|
33
|
-
* 3. A permissive **self SELECT** grant (`id = rebase.uid()`), so users can read
|
|
34
|
-
* their own row (profile, session bootstrap) without every app re-declaring
|
|
35
|
-
* it.
|
|
36
|
-
* 4. A **restrictive** admin write gate. Restrictive policies are AND'd with
|
|
37
|
-
* every other policy, so a write is rejected unless the caller is an admin
|
|
38
|
-
* (or the server context) — even if the author also wrote a permissive rule
|
|
39
|
-
* such as "a user may edit their own row". Without this, a permissive owner
|
|
40
|
-
* rule would let a user change their own `roles`.
|
|
41
|
-
*
|
|
42
|
-
* The server context is recognised as `rebase.uid() IS NULL` (`policy.serverContext()`)
|
|
43
|
-
* — the built-in flows that run without a user (signup, migrations) set no user
|
|
44
|
-
* GUC — which also lets the owner connection satisfy these policies even under
|
|
45
|
-
* FORCE RLS. A *user* request never reaches that state: an anonymous one carries
|
|
46
|
-
* `ANONYMOUS_USER_ID`, precisely so it cannot pass for the server here.
|
|
47
|
-
*
|
|
48
|
-
* Opt out with `disableDefaultPolicies: true` to take full responsibility for
|
|
49
|
-
* the collection's RLS.
|
|
50
|
-
*/
|
|
51
|
-
// Expressed structurally (not as raw SQL) so the admin UI can evaluate it
|
|
52
|
-
// exactly — the framework's most security-critical policies must be reflected
|
|
53
|
-
// precisely, not left as un-evaluable raw clauses. Compiles to
|
|
54
|
-
// `rebase.uid() IS NULL OR (string_to_array(rebase.roles(), ',') && ARRAY['admin'])`.
|
|
55
|
-
//
|
|
56
|
-
// `serverContext()`, emphatically not `not(authenticated())`: the server arm of
|
|
57
|
-
// this grant must match the server context and nothing else. Anonymous visitors
|
|
58
|
-
// are not signed in either, so a negated `authenticated()` would hand them the
|
|
59
|
-
// server-or-admin grant on every collection's default policy.
|
|
60
|
-
const SERVER_OR_ADMIN_EXPR: PolicyExpression = policy.or(
|
|
61
|
-
policy.serverContext(),
|
|
62
|
-
policy.rolesOverlap(["admin"])
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
/** Write operations that must be admin-gated by default on auth collections. */
|
|
66
|
-
const DEFAULT_GUARDED_OPS: SecurityOperation[] = ["insert", "update", "delete"];
|
|
67
|
-
|
|
68
|
-
/** Whether a collection is flagged as an authentication collection. */
|
|
69
|
-
function isAuthCollection(collection: CollectionConfig): boolean {
|
|
70
|
-
const auth = collection.auth;
|
|
71
|
-
return auth === true || (typeof auth === "object" && (auth as AuthCollectionConfig)?.enabled === true);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The property marked as the row id (falls back to `id`). */
|
|
75
|
-
function getIdPropertyName(collection: CollectionConfig): string {
|
|
76
|
-
for (const [name, prop] of Object.entries(collection.properties ?? {})) {
|
|
77
|
-
if (prop && typeof prop === "object" && "isId" in prop && (prop as { isId?: unknown }).isId) {
|
|
78
|
-
return name;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return "id";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Returns the security rules that should be applied to a collection: the
|
|
86
|
-
* author's explicit `securityRules` plus the framework defaults described in
|
|
87
|
-
* the module doc (baseline server/admin read for all collections; self-read
|
|
88
|
-
* and the admin write gate for auth collections).
|
|
89
|
-
*
|
|
90
|
-
* Collections that opt out via `disableDefaultPolicies` are returned unchanged.
|
|
91
|
-
*/
|
|
92
|
-
/**
|
|
93
|
-
* The restrictive write gate for an auth collection.
|
|
94
|
-
*
|
|
95
|
-
* Restrictive, so it is ANDed with everything else: whatever an author's
|
|
96
|
-
* permissive rules allow, a write to this table still has to satisfy this too.
|
|
97
|
-
* It is the only thing standing between "users may edit their own row" and
|
|
98
|
-
* "users may grant themselves any role".
|
|
99
|
-
*/
|
|
100
|
-
function adminWriteGate(tableName: string): SecurityRule {
|
|
101
|
-
return {
|
|
102
|
-
name: `${tableName}_require_admin_write`,
|
|
103
|
-
mode: "restrictive",
|
|
104
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
105
|
-
condition: SERVER_OR_ADMIN_EXPR,
|
|
106
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function getEffectiveSecurityRules(collection: CollectionConfig): SecurityRule[] {
|
|
111
|
-
const explicit = [...(collection.securityRules ?? [])];
|
|
112
|
-
|
|
113
|
-
const tableName = getTableName(collection);
|
|
114
|
-
const injected: SecurityRule[] = [];
|
|
115
|
-
|
|
116
|
-
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) {
|
|
117
|
-
// The opt-out drops the *permissive* defaults — the ones that grant.
|
|
118
|
-
// The restrictive admin-write gate on an auth collection is not among
|
|
119
|
-
// them, because it is different in kind: a restrictive policy is ANDed
|
|
120
|
-
// with every other policy and can only ever remove access, so opting
|
|
121
|
-
// out of it cannot express anything except "let more people write".
|
|
122
|
-
//
|
|
123
|
-
// Dropping it did exactly that. `{ disableDefaultPolicies: true,
|
|
124
|
-
// securityRules: [{ operation: "all", ownerField: "id" }] }` — an
|
|
125
|
-
// ordinary "users may edit their own row" configuration — let any
|
|
126
|
-
// signed-in user set their own `roles` to `["admin"]`, with no warning
|
|
127
|
-
// from any boot guard, doctor check or validator.
|
|
128
|
-
//
|
|
129
|
-
// An author who needs a different gate can add their own restrictive
|
|
130
|
-
// rule; they cannot end up with none by accident.
|
|
131
|
-
return isAuthCollection(collection)
|
|
132
|
-
? [...explicit, adminWriteGate(tableName)]
|
|
133
|
-
: explicit;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Baseline read + write: the server context and admins can always operate.
|
|
137
|
-
// RLS default-denies under the user role, so without these a rule-less
|
|
138
|
-
// collection would be locked to everyone — including the admin studio.
|
|
139
|
-
// Author rules are permissive and broaden access from here.
|
|
140
|
-
injected.push({
|
|
141
|
-
name: `${tableName}_default_admin_read`,
|
|
142
|
-
operations: ["select"],
|
|
143
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
144
|
-
});
|
|
145
|
-
injected.push({
|
|
146
|
-
name: `${tableName}_default_admin_write`,
|
|
147
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
148
|
-
condition: SERVER_OR_ADMIN_EXPR,
|
|
149
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
if (isAuthCollection(collection)) {
|
|
153
|
-
// Self-read: a user can always read their own row.
|
|
154
|
-
injected.push({
|
|
155
|
-
name: `${tableName}_default_self_read`,
|
|
156
|
-
operations: ["select"],
|
|
157
|
-
condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Restrictive gate: AND'd with all other policies, so no permissive rule
|
|
161
|
-
// (e.g. an owner "edit your own row" rule) can let a non-admin change
|
|
162
|
-
// privileged columns like `roles`. Survives `disableDefaultPolicies` —
|
|
163
|
-
// see the note above the opt-out.
|
|
164
|
-
injected.push(adminWriteGate(tableName));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return [...explicit, ...injected];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* The framework defaults that {@link getEffectiveSecurityRules} would add to a
|
|
172
|
-
* collection, without the author's own rules.
|
|
173
|
-
*
|
|
174
|
-
* These policies appear in the database under names the author never wrote, and
|
|
175
|
-
* a permissive policy ORs with every other permissive policy — so someone
|
|
176
|
-
* reading their `securityRules` and then the real ACL sees more access than they
|
|
177
|
-
* declared. Dropping them by hand does nothing either: `db push` is declarative,
|
|
178
|
-
* so the next push asserts them again. Callers use this to say, in the generated
|
|
179
|
-
* DDL, which policies are injected and how to take them off.
|
|
180
|
-
*/
|
|
181
|
-
export function getInjectedSecurityRules(collection: CollectionConfig): SecurityRule[] {
|
|
182
|
-
if (isPostgresCollectionConfig(collection) && collection.disableDefaultPolicies) {
|
|
183
|
-
// Not empty for an auth collection: the restrictive write gate is still
|
|
184
|
-
// injected, and the generated DDL has to say so — a policy in the
|
|
185
|
-
// database that the author never wrote and cannot find in this list is
|
|
186
|
-
// exactly the surprise this function exists to prevent.
|
|
187
|
-
return isAuthCollection(collection) ? [adminWriteGate(getTableName(collection))] : [];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const explicitCount = (collection.securityRules ?? []).length;
|
|
191
|
-
// getEffectiveSecurityRules appends the defaults after the author's rules,
|
|
192
|
-
// so everything past the author's count is injected.
|
|
193
|
-
return getEffectiveSecurityRules(collection).slice(explicitCount);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Every policy name `rebase db push` would write for a collection.
|
|
198
|
-
*
|
|
199
|
-
* This is the answer to "did the codebase produce this live policy?", and it is
|
|
200
|
-
* more than `securityRules.map(r => r.name)` for two reasons:
|
|
201
|
-
*
|
|
202
|
-
* - a rule without an explicit `name` compiles to `<table>_<op>_<hash>`, one
|
|
203
|
-
* per operation, so comparing `rule.name` to `policyname` never matches it;
|
|
204
|
-
* - the generator also injects the safe-by-default baseline
|
|
205
|
-
* (`<table>_default_admin_*`), which is in no collection's `securityRules`.
|
|
206
|
-
*
|
|
207
|
-
* Every UI that flags drift has to get both right, and each one that derived it
|
|
208
|
-
* by hand got a different subset — which is how four policies *Rebase itself
|
|
209
|
-
* wrote* came to be badged as hand-written drift on every table in a project,
|
|
210
|
-
* with a button offering to import them back into the codebase that produced
|
|
211
|
-
* them. There is one derivation now, and this is it.
|
|
212
|
-
*/
|
|
213
|
-
export function getGeneratedPolicyNames(collection: CollectionConfig): Set<string> {
|
|
214
|
-
return getPolicyNamesForRules(getEffectiveSecurityRules(collection), getTableName(collection));
|
|
215
|
-
}
|
package/src/util/builders.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CollectionConfig,
|
|
3
|
-
FirebaseCollectionConfig,
|
|
4
|
-
FirebaseProperties,
|
|
5
|
-
InferEntityType,
|
|
6
|
-
MongoDBCollectionConfig,
|
|
7
|
-
MongoProperties,
|
|
8
|
-
PostgresCollectionConfig,
|
|
9
|
-
PostgresProperties,
|
|
10
|
-
User
|
|
11
|
-
} from "@rebasepro/types";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// ── defineCollection ─────────────────────────────────────────────────────
|
|
15
|
-
// A smarter builder that uses `const` type-parameter inference (TS 5.0+)
|
|
16
|
-
// to capture literal property types automatically. This gives you
|
|
17
|
-
// autocomplete on `display.title`, `sort`, `propertiesOrder`, `fixedFilter`,
|
|
18
|
-
// callbacks, etc. — without writing `as const` or passing manual generics.
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Define a PostgreSQL-backed collection with full type inference.
|
|
22
|
-
*
|
|
23
|
-
* The `const P` generic captures literal property types from your
|
|
24
|
-
* `properties` object, which enables autocomplete on `display.title`,
|
|
25
|
-
* `sort`, `propertiesOrder`, `fixedFilter`, and entity callbacks.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* const products = defineCollection({
|
|
30
|
-
* name: "Products",
|
|
31
|
-
* slug: "products",
|
|
32
|
-
* table: "products",
|
|
33
|
-
* properties: {
|
|
34
|
-
* name: { name: "Name", type: "string", validation: { required: true } },
|
|
35
|
-
* price: { name: "Price", type: "number" },
|
|
36
|
-
* },
|
|
37
|
-
* display: { title: "name" }, // ✅ autocomplete: "name" | "price"
|
|
38
|
-
* sort: ["price", "asc"], // ✅ autocomplete on first element
|
|
39
|
-
* });
|
|
40
|
-
* ```
|
|
41
|
-
*
|
|
42
|
-
* @group Builder
|
|
43
|
-
*/
|
|
44
|
-
export function defineCollection<
|
|
45
|
-
const P extends PostgresProperties,
|
|
46
|
-
USER extends User = User
|
|
47
|
-
>(
|
|
48
|
-
collection: Omit<PostgresCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
49
|
-
): PostgresCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Define a Firestore-backed collection with full type inference.
|
|
53
|
-
* @group Builder
|
|
54
|
-
*/
|
|
55
|
-
export function defineCollection<
|
|
56
|
-
const P extends FirebaseProperties,
|
|
57
|
-
USER extends User = User
|
|
58
|
-
>(
|
|
59
|
-
collection: Omit<FirebaseCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
60
|
-
): FirebaseCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Define a MongoDB-backed collection with full type inference.
|
|
64
|
-
* @group Builder
|
|
65
|
-
*/
|
|
66
|
-
export function defineCollection<
|
|
67
|
-
const P extends MongoProperties,
|
|
68
|
-
USER extends User = User
|
|
69
|
-
>(
|
|
70
|
-
collection: Omit<MongoDBCollectionConfig<InferEntityType<P>, USER>, "properties"> & { properties: P }
|
|
71
|
-
): MongoDBCollectionConfig<InferEntityType<P>, USER> & { properties: P };
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Implementation — delegates to the correct overload at the type level.
|
|
75
|
-
* At runtime this is a plain identity function.
|
|
76
|
-
*/
|
|
77
|
-
export function defineCollection(
|
|
78
|
-
collection: CollectionConfig
|
|
79
|
-
): CollectionConfig {
|
|
80
|
-
return collection;
|
|
81
|
-
}
|
|
82
|
-
|