@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/util/permissions.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { AuthState, Entity, CollectionConfig, SecurityOperation, SecurityRule, User } from "@rebasepro/types";
|
|
2
|
-
import { securityRuleToConditions } from "./policy/securityRuleToConditions";
|
|
3
|
-
import { evaluatePolicy, PolicyEvalContext, TriState } from "./policy/evaluatePolicy";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Minimal auth context for permission checking.
|
|
7
|
-
* Only requires the user object — avoids forcing callers to construct
|
|
8
|
-
* a full AuthController just to check permissions.
|
|
9
|
-
*
|
|
10
|
-
* An alias, not a second definition: {@link AuthState} in `@rebasepro/types` is
|
|
11
|
-
* the same shape and is what `dynamicProps` and the JSON-Logic condition context
|
|
12
|
-
* now take, so declaring it twice would be the `WhereFilterOp` mistake again —
|
|
13
|
-
* two copies that agree only by luck.
|
|
14
|
-
*/
|
|
15
|
-
export type AuthContext<USER extends User = User> = AuthState<USER>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* How to resolve a policy result that cannot be decided client-side (a raw-SQL
|
|
19
|
-
* escape-hatch rule, or a row-column reference with no row in hand).
|
|
20
|
-
*
|
|
21
|
-
* - `"allow"` (default): optimistic — used for admin-UI gating, where Postgres
|
|
22
|
-
* remains the authoritative gate and hiding a working action is worse than
|
|
23
|
-
* showing one the server may reject.
|
|
24
|
-
* - `"deny"`: fail-closed — used by real enforcement callers (e.g. a driver
|
|
25
|
-
* applying policies in-process), so an undecidable rule never silently allows.
|
|
26
|
-
*/
|
|
27
|
-
export type UnknownResolution = "allow" | "deny";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Which half of a rule to evaluate.
|
|
31
|
-
*
|
|
32
|
-
* Postgres evaluates `USING` against the row as it is *now* and `WITH CHECK`
|
|
33
|
-
* against the row as it *will be*, both inside the transaction. A driver
|
|
34
|
-
* enforcing an update in-process has two different rows in hand and therefore
|
|
35
|
-
* needs to ask the two questions separately — asking one question about one row
|
|
36
|
-
* either checks the new values against the old row's ownership or the reverse.
|
|
37
|
-
*
|
|
38
|
-
* - `"both"` (default): what a single-row decision means (`USING ∧ WITH CHECK`).
|
|
39
|
-
* - `"using"`: the read/target clause only — ask it about the stored row.
|
|
40
|
-
* - `"withCheck"`: the write clause only — ask it about the row being written.
|
|
41
|
-
*
|
|
42
|
-
* Rule *selection* is unaffected: the target operation still decides which rules
|
|
43
|
-
* apply, so `"using"` on an `update` evaluates the update rules' USING clause,
|
|
44
|
-
* not the delete rules'.
|
|
45
|
-
*/
|
|
46
|
-
export type PolicyClauses = "both" | "using" | "withCheck";
|
|
47
|
-
|
|
48
|
-
export interface CheckOperationOptions {
|
|
49
|
-
onUnknown?: UnknownResolution;
|
|
50
|
-
clauses?: PolicyClauses;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Combine clause results with AND under three-valued (Kleene) logic. */
|
|
54
|
-
function kleeneAnd(values: TriState[]): TriState {
|
|
55
|
-
if (values.some(v => v === false)) return false;
|
|
56
|
-
if (values.some(v => v === "unknown")) return "unknown";
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The operations a rule covers, mirroring the Postgres generator's resolution. */
|
|
61
|
-
function ruleOperations(rule: SecurityRule): readonly SecurityOperation[] {
|
|
62
|
-
return rule.operations && rule.operations.length > 0
|
|
63
|
-
? rule.operations
|
|
64
|
-
: [rule.operation ?? "all"];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function ruleApplies(rule: SecurityRule, targetOperation: SecurityOperation): boolean {
|
|
68
|
-
const ops = ruleOperations(rule);
|
|
69
|
-
return ops.includes(targetOperation) || ops.includes("all");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Evaluate a single rule for one operation, returning a tri-state.
|
|
74
|
-
*
|
|
75
|
-
* A `null` clause (the rule contributes no condition for a required clause)
|
|
76
|
-
* denies — matching Postgres, which emits `USING (false)` / `WITH CHECK (false)`
|
|
77
|
-
* in that case. USING applies to SELECT/UPDATE/DELETE; WITH CHECK to
|
|
78
|
-
* INSERT/UPDATE; both must pass for UPDATE.
|
|
79
|
-
*/
|
|
80
|
-
function evaluateRuleForOperation(
|
|
81
|
-
rule: SecurityRule,
|
|
82
|
-
ctx: PolicyEvalContext,
|
|
83
|
-
targetOperation: SecurityOperation,
|
|
84
|
-
clauses: PolicyClauses = "both"
|
|
85
|
-
): TriState {
|
|
86
|
-
const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);
|
|
87
|
-
const clause = (expr: typeof usingExpr): TriState => expr === null ? false : evaluatePolicy(expr, ctx);
|
|
88
|
-
|
|
89
|
-
const needsUsing = targetOperation !== "insert" && clauses !== "withCheck";
|
|
90
|
-
const needsWithCheck = (targetOperation === "insert" || targetOperation === "update") && clauses !== "using";
|
|
91
|
-
|
|
92
|
-
const results: TriState[] = [];
|
|
93
|
-
if (needsUsing) results.push(clause(usingExpr));
|
|
94
|
-
if (needsWithCheck) results.push(clause(withCheckExpr));
|
|
95
|
-
return kleeneAnd(results);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function resolveTriState(value: TriState, onUnknown: UnknownResolution): boolean {
|
|
99
|
-
if (value === "unknown") return onUnknown === "allow";
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Decide whether an operation is permitted for a user on a (possibly null) row,
|
|
105
|
-
* by evaluating the collection's security rules with the shared policy model —
|
|
106
|
-
* the same model compiled to Postgres RLS DDL, so the decision matches database
|
|
107
|
-
* enforcement for every non-raw rule.
|
|
108
|
-
*
|
|
109
|
-
* Engine-independent by design. `securityRules` are a declaration about the
|
|
110
|
-
* data, not about Postgres: the engine decides *who* enforces them (Postgres
|
|
111
|
-
* compiles them to RLS DDL, a document driver applies them in-process), never
|
|
112
|
-
* *whether* they hold. Gating this function on the engine's `supportsRLS`
|
|
113
|
-
* capability is what made every `{ onUnknown: "deny" }` call site in the Mongo
|
|
114
|
-
* driver return `true` before it evaluated anything — and it did so only for
|
|
115
|
-
* collections that spelled their engine out, so declaring `engine: "mongodb"`
|
|
116
|
-
* was what switched authorization off.
|
|
117
|
-
*
|
|
118
|
-
* @param options.onUnknown how to treat rules that cannot be decided
|
|
119
|
-
* client-side (raw SQL, or row predicates with no row). Defaults to `"allow"`
|
|
120
|
-
* for optimistic UI gating; enforcement callers should pass `"deny"`.
|
|
121
|
-
* @param options.clauses which half of each rule to evaluate. See
|
|
122
|
-
* {@link PolicyClauses}; defaults to `"both"`.
|
|
123
|
-
*/
|
|
124
|
-
export function checkOperation<M extends Record<string, unknown>, USER extends User>(
|
|
125
|
-
collection: CollectionConfig<M>,
|
|
126
|
-
authContext: AuthContext<USER>,
|
|
127
|
-
entity: Entity<M> | null,
|
|
128
|
-
targetOperation: SecurityOperation,
|
|
129
|
-
options?: CheckOperationOptions
|
|
130
|
-
): boolean {
|
|
131
|
-
const onUnknown = options?.onUnknown ?? "allow";
|
|
132
|
-
const clauses = options?.clauses ?? "both";
|
|
133
|
-
const securityRules = collection.securityRules;
|
|
134
|
-
if (!securityRules || securityRules.length === 0) {
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const applicableRules = securityRules.filter((r: SecurityRule) => ruleApplies(r, targetOperation));
|
|
139
|
-
if (applicableRules.length === 0) return false;
|
|
140
|
-
|
|
141
|
-
const ctx: PolicyEvalContext = {
|
|
142
|
-
uid: authContext.user?.uid,
|
|
143
|
-
roles: authContext.user?.roles ?? [],
|
|
144
|
-
entity
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
let grantedByPermissive = false;
|
|
148
|
-
let deniedByRestrictive = false;
|
|
149
|
-
let hasPermissive = false;
|
|
150
|
-
|
|
151
|
-
for (const rule of applicableRules) {
|
|
152
|
-
const mode = rule.mode || "permissive";
|
|
153
|
-
const passed = resolveTriState(evaluateRuleForOperation(rule, ctx, targetOperation, clauses), onUnknown);
|
|
154
|
-
|
|
155
|
-
if (mode === "restrictive") {
|
|
156
|
-
if (!passed) {
|
|
157
|
-
deniedByRestrictive = true;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
hasPermissive = true;
|
|
162
|
-
if (passed) grantedByPermissive = true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (deniedByRestrictive) return false;
|
|
167
|
-
return hasPermissive ? grantedByPermissive : false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function canReadCollection<M extends Record<string, unknown>, USER extends User>
|
|
171
|
-
(
|
|
172
|
-
collection: CollectionConfig<M>,
|
|
173
|
-
authContext: AuthContext<USER>
|
|
174
|
-
): boolean {
|
|
175
|
-
return checkOperation(collection, authContext, null, "select");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function canEditEntity<M extends Record<string, unknown>, USER extends User>
|
|
179
|
-
(
|
|
180
|
-
collection: CollectionConfig<M>,
|
|
181
|
-
authContext: AuthContext<USER>,
|
|
182
|
-
path: string,
|
|
183
|
-
entity: Entity<M> | null
|
|
184
|
-
): boolean {
|
|
185
|
-
return checkOperation(collection, authContext, entity, "update");
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function canCreateEntity<M extends Record<string, unknown>, USER extends User>
|
|
189
|
-
(
|
|
190
|
-
collection: CollectionConfig<M>,
|
|
191
|
-
authContext: AuthContext<USER>,
|
|
192
|
-
path: string,
|
|
193
|
-
entity: Entity<M> | null
|
|
194
|
-
): boolean {
|
|
195
|
-
return checkOperation(collection, authContext, entity, "insert");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function canDeleteEntity<M extends Record<string, unknown>, USER extends User>
|
|
199
|
-
(
|
|
200
|
-
collection: CollectionConfig<M>,
|
|
201
|
-
authContext: AuthContext<USER>,
|
|
202
|
-
path: string,
|
|
203
|
-
entity: Entity<M> | null
|
|
204
|
-
): boolean {
|
|
205
|
-
return checkOperation(collection, authContext, entity, "delete");
|
|
206
|
-
}
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import { rewriteLegacyRlsFunctions } from "@rebasepro/types";
|
|
2
|
-
import type {
|
|
3
|
-
ArrayProperty,
|
|
4
|
-
NumberProperty,
|
|
5
|
-
PostgresProperties,
|
|
6
|
-
Property,
|
|
7
|
-
Relation,
|
|
8
|
-
SecurityOperation,
|
|
9
|
-
SecurityRule,
|
|
10
|
-
StringProperty,
|
|
11
|
-
TableColumnInfo,
|
|
12
|
-
TableMetadata
|
|
13
|
-
} from "@rebasepro/types";
|
|
14
|
-
import { firstFreeKey, prettifyIdentifier, toWireKey } from "@rebasepro/utils";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A collection as introspection can describe it: the table, its columns, the
|
|
18
|
-
* relations its foreign keys imply, and the RLS policies already on it.
|
|
19
|
-
*
|
|
20
|
-
* Deliberately not `Partial<AdminCollection>`, which is what this returned
|
|
21
|
-
* while it lived in `@rebasepro/studio`. `propertiesOrder` is the only admin
|
|
22
|
-
* key it produces, and naming the admin view model for one field would put
|
|
23
|
-
* `@rebasepro/cms-types` on the dependency path of a package the backend
|
|
24
|
-
* loads.
|
|
25
|
-
*/
|
|
26
|
-
export interface IntrospectedCollection {
|
|
27
|
-
name: string;
|
|
28
|
-
slug: string;
|
|
29
|
-
table: string;
|
|
30
|
-
properties: PostgresProperties;
|
|
31
|
-
propertiesOrder: string[];
|
|
32
|
-
relations?: Relation[];
|
|
33
|
-
securityRules?: SecurityRule[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Maps a PostgreSQL column data type to a Rebase property type.
|
|
38
|
-
*/
|
|
39
|
-
function pgTypeToRebaseProperty(column: TableColumnInfo): Property | null {
|
|
40
|
-
const {
|
|
41
|
-
column_name,
|
|
42
|
-
data_type,
|
|
43
|
-
udt_name,
|
|
44
|
-
is_nullable,
|
|
45
|
-
column_default,
|
|
46
|
-
character_maximum_length,
|
|
47
|
-
enum_values
|
|
48
|
-
} = column;
|
|
49
|
-
|
|
50
|
-
const required = is_nullable === "NO";
|
|
51
|
-
const prettifiedName = prettifyIdentifier(column_name);
|
|
52
|
-
|
|
53
|
-
// Detect if this column is a primary key (auto-generated id)
|
|
54
|
-
const isAutoId = column_default != null && (
|
|
55
|
-
column_default.includes("nextval") ||
|
|
56
|
-
column_default.includes("gen_random_uuid") ||
|
|
57
|
-
column_default.includes("uuid_generate") ||
|
|
58
|
-
column_default.includes("identity")
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// USER-DEFINED = PostgreSQL enums
|
|
62
|
-
if (data_type === "USER-DEFINED" && enum_values && enum_values.length > 0) {
|
|
63
|
-
return {
|
|
64
|
-
type: "string",
|
|
65
|
-
name: prettifiedName,
|
|
66
|
-
enum: enum_values.map((v: string) => ({ id: v,
|
|
67
|
-
label: prettifyIdentifier(v) })),
|
|
68
|
-
validation: required ? { required: true } : undefined
|
|
69
|
-
} as StringProperty;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const dt = data_type.toLowerCase();
|
|
73
|
-
switch (dt) {
|
|
74
|
-
case "character varying":
|
|
75
|
-
case "varchar":
|
|
76
|
-
case "text":
|
|
77
|
-
case "char":
|
|
78
|
-
case "character":
|
|
79
|
-
case "citext": {
|
|
80
|
-
let colType: "varchar" | "text" | "char" = "varchar";
|
|
81
|
-
if (dt === "text" || dt === "citext") colType = "text";
|
|
82
|
-
if (dt === "char" || dt === "character") colType = "char";
|
|
83
|
-
// Carry the declared width across. Dropping it made introspection
|
|
84
|
-
// lossy in the one direction that costs data: a `character
|
|
85
|
-
// varying(500)` column read back as a bare `varchar` regenerates as
|
|
86
|
-
// `VARCHAR(255)`, narrowing a column that already holds longer
|
|
87
|
-
// values. TEXT has no width, and reporting one would invent a limit
|
|
88
|
-
// the database does not have.
|
|
89
|
-
const declaredLength = colType === "text" ? null : character_maximum_length;
|
|
90
|
-
const prop: StringProperty = {
|
|
91
|
-
type: "string",
|
|
92
|
-
name: prettifiedName,
|
|
93
|
-
columnType: colType,
|
|
94
|
-
validation: required || declaredLength
|
|
95
|
-
? {
|
|
96
|
-
...(required ? { required: true } : {}),
|
|
97
|
-
...(declaredLength ? { max: declaredLength } : {})
|
|
98
|
-
}
|
|
99
|
-
: undefined
|
|
100
|
-
};
|
|
101
|
-
if (isAutoId) {
|
|
102
|
-
prop.isId = "manual";
|
|
103
|
-
}
|
|
104
|
-
return prop;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
case "uuid": {
|
|
108
|
-
const prop: StringProperty = {
|
|
109
|
-
type: "string",
|
|
110
|
-
name: prettifiedName,
|
|
111
|
-
validation: required ? { required: true } : undefined
|
|
112
|
-
};
|
|
113
|
-
if (isAutoId) {
|
|
114
|
-
prop.isId = "uuid";
|
|
115
|
-
}
|
|
116
|
-
return prop;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
case "integer":
|
|
120
|
-
case "bigint":
|
|
121
|
-
case "smallint": {
|
|
122
|
-
const colType = dt === "bigint" ? "bigint" : "integer";
|
|
123
|
-
const prop: NumberProperty = {
|
|
124
|
-
type: "number",
|
|
125
|
-
name: prettifiedName,
|
|
126
|
-
columnType: colType,
|
|
127
|
-
validation: {
|
|
128
|
-
...(required ? { required: true } : {}),
|
|
129
|
-
integer: true
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
if (isAutoId) {
|
|
133
|
-
prop.isId = "increment";
|
|
134
|
-
}
|
|
135
|
-
return prop;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
case "serial":
|
|
139
|
-
case "bigserial":
|
|
140
|
-
case "smallserial": {
|
|
141
|
-
const colType = dt === "bigserial" ? "bigserial" : "serial";
|
|
142
|
-
return {
|
|
143
|
-
type: "number",
|
|
144
|
-
name: prettifiedName,
|
|
145
|
-
columnType: colType,
|
|
146
|
-
isId: "increment",
|
|
147
|
-
validation: {
|
|
148
|
-
...(required ? { required: true } : {}),
|
|
149
|
-
integer: true
|
|
150
|
-
}
|
|
151
|
-
} as NumberProperty;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
case "numeric":
|
|
155
|
-
case "decimal":
|
|
156
|
-
case "real":
|
|
157
|
-
case "double precision": {
|
|
158
|
-
let colType: "numeric" | "real" | "double precision" = "numeric";
|
|
159
|
-
if (dt === "real") colType = "real";
|
|
160
|
-
if (dt === "double precision") colType = "double precision";
|
|
161
|
-
return {
|
|
162
|
-
type: "number",
|
|
163
|
-
name: prettifiedName,
|
|
164
|
-
columnType: colType,
|
|
165
|
-
validation: required ? { required: true } : undefined
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
case "boolean":
|
|
170
|
-
return {
|
|
171
|
-
type: "boolean",
|
|
172
|
-
name: prettifiedName,
|
|
173
|
-
validation: required ? { required: true } : undefined
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
case "timestamp with time zone":
|
|
177
|
-
case "timestamp without time zone":
|
|
178
|
-
case "timestamp":
|
|
179
|
-
case "timestamptz":
|
|
180
|
-
case "date":
|
|
181
|
-
case "time with time zone":
|
|
182
|
-
case "time without time zone":
|
|
183
|
-
case "time": {
|
|
184
|
-
let colType: "timestamp" | "date" | "time" = "timestamp";
|
|
185
|
-
if (dt.startsWith("date")) colType = "date";
|
|
186
|
-
if (dt.startsWith("time ") || dt === "time") colType = "time";
|
|
187
|
-
return {
|
|
188
|
-
type: "date",
|
|
189
|
-
name: prettifiedName,
|
|
190
|
-
columnType: colType,
|
|
191
|
-
validation: required ? { required: true } : undefined
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
case "jsonb":
|
|
196
|
-
case "json":
|
|
197
|
-
return {
|
|
198
|
-
type: "map",
|
|
199
|
-
name: prettifiedName,
|
|
200
|
-
columnType: dt === "jsonb" ? "jsonb" : "json",
|
|
201
|
-
keyValue: true,
|
|
202
|
-
properties: {}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
case "array":
|
|
206
|
-
case "ARRAY": {
|
|
207
|
-
let innerType = "string";
|
|
208
|
-
let colType: ArrayProperty["columnType"] = undefined;
|
|
209
|
-
if (udt_name === "_text" || udt_name === "_varchar") {
|
|
210
|
-
innerType = "string";
|
|
211
|
-
colType = "text[]";
|
|
212
|
-
} else if (udt_name === "_int4" || udt_name === "_int2" || udt_name === "_int8") {
|
|
213
|
-
innerType = "number";
|
|
214
|
-
colType = "integer[]";
|
|
215
|
-
} else if (udt_name === "_bool") {
|
|
216
|
-
innerType = "boolean";
|
|
217
|
-
colType = "boolean[]";
|
|
218
|
-
} else if (udt_name === "_numeric") {
|
|
219
|
-
innerType = "number";
|
|
220
|
-
colType = "numeric[]";
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
type: "array",
|
|
224
|
-
name: prettifiedName,
|
|
225
|
-
columnType: colType,
|
|
226
|
-
of: { type: innerType }
|
|
227
|
-
} as ArrayProperty;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
default:
|
|
231
|
-
// Fallback: treat unknown types as string
|
|
232
|
-
return {
|
|
233
|
-
type: "string",
|
|
234
|
-
name: prettifiedName,
|
|
235
|
-
validation: required ? { required: true } : undefined
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Builds a collection description from PostgreSQL table metadata.
|
|
242
|
-
* This is used when creating a new collection from an existing database table.
|
|
243
|
-
*/
|
|
244
|
-
export function buildCollectionFromTableMetadata(
|
|
245
|
-
tableName: string,
|
|
246
|
-
metadata: TableMetadata
|
|
247
|
-
): IntrospectedCollection {
|
|
248
|
-
const properties: Record<string, Property> = {};
|
|
249
|
-
const propertiesOrder: string[] = [];
|
|
250
|
-
// Introspection can only ever produce two shapes: a foreign key on this
|
|
251
|
-
// table, or a junction between two. Both are named by their kind.
|
|
252
|
-
const relations: Array<{
|
|
253
|
-
id: string;
|
|
254
|
-
relationName: string;
|
|
255
|
-
target: string;
|
|
256
|
-
kind: "belongsTo" | "manyToMany";
|
|
257
|
-
localKey?: string;
|
|
258
|
-
through?: { table: string; sourceColumn: string; targetColumn: string };
|
|
259
|
-
}> = [];
|
|
260
|
-
const securityRules: SecurityRule[] = [];
|
|
261
|
-
|
|
262
|
-
// Parse columns
|
|
263
|
-
for (const column of metadata.columns) {
|
|
264
|
-
const property = pgTypeToRebaseProperty(column);
|
|
265
|
-
if (property) {
|
|
266
|
-
const propRecord = property as unknown as Record<string, unknown>;
|
|
267
|
-
Object.keys(propRecord).forEach(key => propRecord[key] === undefined && delete propRecord[key]);
|
|
268
|
-
|
|
269
|
-
// The key is the wire name; `columnName` carries the column. This
|
|
270
|
-
// used to key by the column and rely on the two being the same
|
|
271
|
-
// string, which is what put `user_id` on the API of an imported
|
|
272
|
-
// collection and `displayName` on the API of an authored one.
|
|
273
|
-
//
|
|
274
|
-
// `columnName` is stamped unconditionally rather than left to the
|
|
275
|
-
// snake_case default, because the default is not the inverse of
|
|
276
|
-
// camel-casing for every name — the mapping has to be recorded, not
|
|
277
|
-
// recomputed.
|
|
278
|
-
//
|
|
279
|
-
// First free candidate: `user_id` and `userId` as two real columns
|
|
280
|
-
// camel-case to one key, and one of them would otherwise overwrite
|
|
281
|
-
// the other and be silently dropped.
|
|
282
|
-
const key = firstFreeKey(
|
|
283
|
-
[toWireKey(column.column_name), column.column_name],
|
|
284
|
-
{ has: (candidate: string) => candidate in properties }
|
|
285
|
-
);
|
|
286
|
-
if (key !== column.column_name) propRecord.columnName = column.column_name;
|
|
287
|
-
properties[key] = property;
|
|
288
|
-
propertiesOrder.push(key);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Parse Outgoing Foreign Keys -> Many-to-One / One-to-One
|
|
293
|
-
if (metadata.foreignKeys) {
|
|
294
|
-
for (const fk of metadata.foreignKeys) {
|
|
295
|
-
const relName = toWireKey(
|
|
296
|
-
fk.column_name.endsWith("_id")
|
|
297
|
-
? fk.column_name.substring(0, fk.column_name.length - 3)
|
|
298
|
-
: fk.column_name
|
|
299
|
-
);
|
|
300
|
-
relations.push({
|
|
301
|
-
id: fk.column_name,
|
|
302
|
-
relationName: relName,
|
|
303
|
-
target: fk.foreign_table_name, // Will be hydrated later
|
|
304
|
-
kind: "belongsTo",
|
|
305
|
-
localKey: fk.column_name
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Parse Incoming Junctions -> Many-to-Many
|
|
311
|
-
if (metadata.junctions) {
|
|
312
|
-
for (const junction of metadata.junctions) {
|
|
313
|
-
const relName = junction.target_table_name; // E.g., 'roles'
|
|
314
|
-
relations.push({
|
|
315
|
-
id: junction.target_table_name + "_relation",
|
|
316
|
-
relationName: relName,
|
|
317
|
-
target: junction.target_table_name, // Will be hydrated later
|
|
318
|
-
kind: "manyToMany",
|
|
319
|
-
through: {
|
|
320
|
-
table: junction.junction_table_name,
|
|
321
|
-
sourceColumn: junction.source_column_name,
|
|
322
|
-
targetColumn: junction.target_column_name
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Parse RLS Policies
|
|
329
|
-
if (metadata.policies) {
|
|
330
|
-
for (const policy of metadata.policies) {
|
|
331
|
-
// Attempt to map typical cmds to operations.
|
|
332
|
-
// Postgres cmd: SELECT, INSERT, UPDATE, DELETE, ALL
|
|
333
|
-
let operations: SecurityOperation[] = [];
|
|
334
|
-
switch (policy.cmd) {
|
|
335
|
-
case "ALL": operations = ["all"]; break;
|
|
336
|
-
case "SELECT": operations = ["select"]; break;
|
|
337
|
-
case "INSERT": operations = ["insert"]; break;
|
|
338
|
-
case "UPDATE": operations = ["update"]; break;
|
|
339
|
-
case "DELETE": operations = ["delete"]; break;
|
|
340
|
-
}
|
|
341
|
-
// Normalised on the way in, the same way `sqlToPolicy` normalises
|
|
342
|
-
// what the admin UI reads back. Without it, importing a table from a
|
|
343
|
-
// database provisioned before 1.0 copies `auth.uid()` straight into
|
|
344
|
-
// the project's config — a call to a function the framework no
|
|
345
|
-
// longer creates, which then boots with a legacy-helper warning
|
|
346
|
-
// forever and holds the `auth` schema open.
|
|
347
|
-
const qual = policy.qual ? rewriteLegacyRlsFunctions(policy.qual) : undefined;
|
|
348
|
-
const withCheck = policy.with_check ? rewriteLegacyRlsFunctions(policy.with_check) : undefined;
|
|
349
|
-
if (qual) {
|
|
350
|
-
securityRules.push({
|
|
351
|
-
name: policy.policy_name,
|
|
352
|
-
operations,
|
|
353
|
-
roles: policy.roles ?? [],
|
|
354
|
-
using: qual,
|
|
355
|
-
...(withCheck ? { withCheck } : {})
|
|
356
|
-
});
|
|
357
|
-
} else {
|
|
358
|
-
securityRules.push({
|
|
359
|
-
name: policy.policy_name,
|
|
360
|
-
operations,
|
|
361
|
-
roles: policy.roles ?? []
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
name: prettifyIdentifier(tableName),
|
|
369
|
-
slug: tableName,
|
|
370
|
-
table: tableName,
|
|
371
|
-
properties: properties as PostgresProperties,
|
|
372
|
-
propertiesOrder,
|
|
373
|
-
// `target` is still a slug here — the caller hydrates it into a thunk.
|
|
374
|
-
...(relations.length > 0 ? { relations: relations as unknown as Relation[] } : {}),
|
|
375
|
-
...(securityRules.length > 0 ? { securityRules } : {})
|
|
376
|
-
};
|
|
377
|
-
}
|