@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g009ed95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +33 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js +57 -0
- package/dist/ensure-collection-policies-BrUVgjz3.js.map +1 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js +650 -0
- package/dist/ensure-collection-tables-Da2oGkX2.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +1234 -9753
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +40 -2
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +24 -1
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +4 -2
- package/dist/{src-3VmUJ8Xn.js → src-CzbghKwf.js} +464 -187
- package/dist/src-CzbghKwf.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +162 -7
- package/dist/utils/pg-error-utils.d.ts +25 -3
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +212 -36
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/buildRegistry.ts +1 -1
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +33 -8
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +32 -10
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/FetchService.ts +148 -18
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +40 -1
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +4 -2
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +539 -50
- package/src/utils/pg-error-utils.ts +98 -3
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js +0 -304
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PostgreSQL type → Rebase property type mapping.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `introspect-db-logic` so that the structural analysis can use it
|
|
5
|
+
* without importing the generator, which imports the analysis. Re-exported from
|
|
6
|
+
* `introspect-db-logic` so existing callers keep their import path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Map a PostgreSQL data type to a Rebase property type.
|
|
11
|
+
*/
|
|
12
|
+
export function mapPgType(dataType: string): string {
|
|
13
|
+
const dt = dataType.toLowerCase();
|
|
14
|
+
|
|
15
|
+
// Interval MUST be checked before numeric ("interval" contains "int")
|
|
16
|
+
if (dt === "interval") return "string";
|
|
17
|
+
|
|
18
|
+
// Array types MUST be checked before numeric ("_int4" contains "int")
|
|
19
|
+
if (dt === "array" || dt.startsWith("_")) return "array";
|
|
20
|
+
|
|
21
|
+
// Numeric types
|
|
22
|
+
if (
|
|
23
|
+
dt.includes("int") || // integer, smallint, bigint
|
|
24
|
+
dt.includes("numeric") ||
|
|
25
|
+
dt.includes("decimal") ||
|
|
26
|
+
dt.includes("serial") || // serial, bigserial
|
|
27
|
+
dt === "real" ||
|
|
28
|
+
dt === "float4" ||
|
|
29
|
+
dt === "float8" ||
|
|
30
|
+
dt === "double precision" ||
|
|
31
|
+
dt === "money"
|
|
32
|
+
) {
|
|
33
|
+
return "number";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Boolean
|
|
37
|
+
if (dt.includes("bool")) return "boolean";
|
|
38
|
+
|
|
39
|
+
// Date / Time
|
|
40
|
+
if (dt.includes("time") || dt.includes("date")) return "date";
|
|
41
|
+
|
|
42
|
+
// JSON
|
|
43
|
+
if (dt === "json" || dt === "jsonb") return "map";
|
|
44
|
+
|
|
45
|
+
// Binary
|
|
46
|
+
if (dt === "bytea") return "binary";
|
|
47
|
+
|
|
48
|
+
// Network types
|
|
49
|
+
if (dt === "inet" || dt === "cidr" || dt === "macaddr" || dt === "macaddr8") return "string";
|
|
50
|
+
|
|
51
|
+
// UUID
|
|
52
|
+
if (dt === "uuid") return "string";
|
|
53
|
+
|
|
54
|
+
// Text/varchar/char — default to string
|
|
55
|
+
return "string";
|
|
56
|
+
}
|
|
@@ -7,19 +7,16 @@ import * as dotenv from "dotenv";
|
|
|
7
7
|
import readline from "readline";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
TableRow,
|
|
11
|
-
TableColumn,
|
|
12
|
-
EnumValue,
|
|
13
|
-
PrimaryKeyRow,
|
|
14
|
-
ForeignKeyRow,
|
|
15
10
|
buildTablesMap,
|
|
16
11
|
buildEnumMap,
|
|
17
|
-
identifyJoinTables,
|
|
18
12
|
generateCollectionFile,
|
|
19
13
|
generateIndexContent,
|
|
20
14
|
mergeIndexContent,
|
|
21
15
|
safeHostFromUrl
|
|
22
16
|
} from "./introspect-db-logic";
|
|
17
|
+
import { countRowsUpTo, readSchemaMetadata } from "./introspect-db-queries";
|
|
18
|
+
import { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from "./introspect-db-structure";
|
|
19
|
+
import { parseCheckConstraints } from "./introspect-db-constraints";
|
|
23
20
|
import { logger } from "@rebasepro/server";
|
|
24
21
|
|
|
25
22
|
async function main() {
|
|
@@ -89,82 +86,41 @@ async function main() {
|
|
|
89
86
|
logger.info(chalk.gray(`Introspecting schema '${pgSchema}'...`));
|
|
90
87
|
|
|
91
88
|
try {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
c.column_default,
|
|
111
|
-
(SELECT a.atttypmod FROM pg_attribute a
|
|
112
|
-
JOIN pg_class pc ON a.attrelid = pc.oid
|
|
113
|
-
WHERE pc.relname = c.table_name
|
|
114
|
-
AND a.attname = c.column_name
|
|
115
|
-
AND pc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)) as atttypmod
|
|
116
|
-
FROM information_schema.columns c
|
|
117
|
-
WHERE c.table_schema = $1
|
|
118
|
-
`, [pgSchema]);
|
|
119
|
-
|
|
120
|
-
// 2b. Get Enum Types and their values
|
|
121
|
-
const { rows: enumValues } = await client.query<EnumValue>(`
|
|
122
|
-
SELECT t.typname AS enum_name,
|
|
123
|
-
e.enumlabel AS enum_value,
|
|
124
|
-
e.enumsortorder AS sort_order
|
|
125
|
-
FROM pg_type t
|
|
126
|
-
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
127
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
128
|
-
WHERE n.nspname = $1
|
|
129
|
-
ORDER BY t.typname, e.enumsortorder
|
|
130
|
-
`, [pgSchema]);
|
|
131
|
-
|
|
132
|
-
// Build a map: enum_name -> ordered list of values
|
|
133
|
-
const enumMap = buildEnumMap(enumValues);
|
|
134
|
-
|
|
135
|
-
// 3. Get Primary Keys
|
|
136
|
-
const { rows: pks } = await client.query<PrimaryKeyRow>(`
|
|
137
|
-
SELECT t.relname as table_name, a.attname as column_name
|
|
138
|
-
FROM pg_index i
|
|
139
|
-
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
140
|
-
AND a.attnum = ANY(i.indkey)
|
|
141
|
-
JOIN pg_class t ON t.oid = i.indrelid
|
|
142
|
-
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
143
|
-
WHERE i.indisprimary AND n.nspname = $1
|
|
144
|
-
`, [pgSchema]);
|
|
89
|
+
const metadata = await readSchemaMetadata(client, pgSchema);
|
|
90
|
+
const enumMap = buildEnumMap(metadata.enumValues);
|
|
91
|
+
const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);
|
|
92
|
+
const fks = metadata.fks;
|
|
93
|
+
|
|
94
|
+
// Only tables that could structurally be a code list are counted, and
|
|
95
|
+
// each count stops at the threshold — see `countRowsUpTo`. Introspection
|
|
96
|
+
// runs against a database it does not own, so "cheap on a table of any
|
|
97
|
+
// size" is a requirement, not an optimization.
|
|
98
|
+
for (const table of lookupCandidates(metadata, tablesMap)) {
|
|
99
|
+
try {
|
|
100
|
+
metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// A table this run cannot read is simply not classified as a code
|
|
103
|
+
// list; everything else about it still generates.
|
|
104
|
+
logger.info(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
145
107
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
FROM
|
|
154
|
-
information_schema.table_constraints AS tc
|
|
155
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
156
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
157
|
-
AND tc.table_schema = kcu.table_schema
|
|
158
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
159
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
160
|
-
AND ccu.table_schema = tc.table_schema
|
|
161
|
-
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1
|
|
162
|
-
`, [pgSchema]);
|
|
108
|
+
const classifications = classifyTables(metadata, tablesMap);
|
|
109
|
+
const checkFacts = parseCheckConstraints(metadata.checks);
|
|
110
|
+
const joinTables = new Set(
|
|
111
|
+
Array.from(classifications.values())
|
|
112
|
+
.filter((c) => c.role === "junction")
|
|
113
|
+
.map((c) => c.table)
|
|
114
|
+
);
|
|
163
115
|
|
|
164
|
-
const
|
|
165
|
-
|
|
116
|
+
const roleCount = (role: string) =>
|
|
117
|
+
Array.from(classifications.values()).filter((c) => c.role === role).length;
|
|
166
118
|
|
|
167
|
-
logger.info(chalk.blue(`Found ${tablesMap.size} tables
|
|
119
|
+
logger.info(chalk.blue(`Found ${tablesMap.size} tables.`));
|
|
120
|
+
logger.info(chalk.gray(
|
|
121
|
+
` ${roleCount("entity")} entities, ${joinTables.size} join tables (folded into relations), ` +
|
|
122
|
+
`${roleCount("lookup")} code lists, ${roleCount("owned-child")} owned by another table (hidden from navigation).`
|
|
123
|
+
));
|
|
168
124
|
|
|
169
125
|
let runDataInference = false;
|
|
170
126
|
if (args["--no-data-inference"]) {
|
|
@@ -224,7 +180,8 @@ async function main() {
|
|
|
224
180
|
joinTables,
|
|
225
181
|
tablesMap,
|
|
226
182
|
enumMap,
|
|
227
|
-
sampleData
|
|
183
|
+
sampleData,
|
|
184
|
+
{ metadata, classifications, checkFacts }
|
|
228
185
|
);
|
|
229
186
|
|
|
230
187
|
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "@jest/globals";
|
|
2
|
+
import { resolveCollectionRelations } from "@rebasepro/common";
|
|
3
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
2
4
|
|
|
3
5
|
import { buildCollectionsFromSchema, introspectSchema, readRlsStatus, IntrospectedSchema, Queryable } from "./introspect-runtime";
|
|
4
6
|
import { buildTablesMap, identifyJoinTables, TableColumn, ForeignKeyRow, PrimaryKeyRow } from "./introspect-db-logic";
|
|
@@ -121,14 +123,60 @@ describe("buildCollectionsFromSchema", () => {
|
|
|
121
123
|
const props = posts.properties as any;
|
|
122
124
|
|
|
123
125
|
expect(props.author_id).toBeUndefined();
|
|
124
|
-
expect(props.author).
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
expect(props.author.name).toBe("Author");
|
|
127
|
+
expect(props.author.type).toBe("relation");
|
|
128
|
+
// The descriptor is nested under `relation`, carries `kind`, and its
|
|
129
|
+
// `target` is a thunk — see the resolvability test below for why none of
|
|
130
|
+
// those three are stylistic.
|
|
131
|
+
expect(props.author.relation).toMatchObject({ kind: "belongsTo", localKey: "author_id" });
|
|
132
|
+
expect(typeof props.author.relation.target).toBe("function");
|
|
133
|
+
expect(props.author.relation.target().slug).toBe("authors");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The shape has to be one the resolver actually reads.
|
|
138
|
+
*
|
|
139
|
+
* This path emitted `cardinality` / `direction` / a bare-string `target`
|
|
140
|
+
* flat on the property — the shape `Relation` stopped accepting, and the
|
|
141
|
+
* same drift `introspect-emits-valid-relations.test.ts` caught in the
|
|
142
|
+
* generated-file path and this one was missed by. Nothing threw:
|
|
143
|
+
* `resolveCollectionRelations` reads `property.relation`, found none, and
|
|
144
|
+
* reported a collection with no relations at all.
|
|
145
|
+
*
|
|
146
|
+
* The visible cost was writes. `assertKnownWriteFields` learns an owning
|
|
147
|
+
* relation's FK column from the *resolved* relation's `localKey`, and the FK
|
|
148
|
+
* column is deliberately absent from `properties` ("surfaces as a
|
|
149
|
+
* relation"), so with nothing resolving, `POST /api/data/orders` with
|
|
150
|
+
* `product_id` came back 400 `has no field 'product_id'` — the column was
|
|
151
|
+
* simultaneously the only way to set the relation and not a known field.
|
|
152
|
+
*
|
|
153
|
+
* Asserting the literal object is what let it drift, so this asserts
|
|
154
|
+
* through the resolver instead: a future reshape has to keep it resolvable,
|
|
155
|
+
* not merely keep the keys someone once wrote down.
|
|
156
|
+
*/
|
|
157
|
+
it("emits a relation the resolver can read, so the fk column is writable", () => {
|
|
158
|
+
const schema = schemaOf(
|
|
159
|
+
[
|
|
160
|
+
column({ table_name: "products", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" }),
|
|
161
|
+
column({ table_name: "orders", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" }),
|
|
162
|
+
column({ table_name: "orders", column_name: "product_id", data_type: "integer", udt_name: "int4" })
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
{ table_name: "products", column_name: "id" },
|
|
166
|
+
{ table_name: "orders", column_name: "id" }
|
|
167
|
+
],
|
|
168
|
+
[{ table_name: "orders", column_name: "product_id", foreign_table_name: "products", foreign_column_name: "id" }]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const orders = buildCollectionsFromSchema(schema, "public").find((c) => c.slug === "orders")!;
|
|
172
|
+
const resolved = resolveCollectionRelations(orders as unknown as CollectionConfig);
|
|
173
|
+
|
|
174
|
+
expect(Object.keys(resolved)).toEqual(["product"]);
|
|
175
|
+
expect(resolved.product.kind).toBe("belongsTo");
|
|
176
|
+
// `localKey` is the field `assertKnownWriteFields` adds to the known set,
|
|
177
|
+
// which is what makes `product_id` writable. The write itself is covered
|
|
178
|
+
// end-to-end by `scripts/smoke-baas.ts` against a real database.
|
|
179
|
+
expect((resolved.product as { localKey: string }).localKey).toBe("product_id");
|
|
132
180
|
});
|
|
133
181
|
|
|
134
182
|
it("skips join tables — they are an edge between collections, not a collection", () => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* single config file.
|
|
8
8
|
*
|
|
9
9
|
* Distinct from `introspect-db.ts`, which runs the same queries but emits
|
|
10
|
-
* TypeScript *source* for a developer to edit and commit (
|
|
10
|
+
* TypeScript *source* for a developer to edit and commit (declared collections). The two
|
|
11
11
|
* share the mapping helpers in `introspect-db-logic.ts` so a table is described
|
|
12
12
|
* the same way whether it was generated or introspected.
|
|
13
13
|
*/
|
|
@@ -207,13 +207,30 @@ function buildProperties(
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/**
|
|
210
|
-
* Owning relations, derived from this table's foreign keys
|
|
211
|
-
* `generateCollectionFile`
|
|
212
|
-
*
|
|
210
|
+
* Owning relations, derived from this table's foreign keys — the same shape
|
|
211
|
+
* `generateCollectionFile` writes into a collection file: a `relation` property
|
|
212
|
+
* whose nested descriptor carries `kind`, a `target` thunk and the `localKey`.
|
|
213
|
+
*
|
|
214
|
+
* The shape is load-bearing, not cosmetic. `resolveCollectionRelations` reads
|
|
215
|
+
* relations from `property.relation` and `resolveRelation` requires `target` to
|
|
216
|
+
* be a thunk; this used to emit `target`/`cardinality`/`localKey` flat on the
|
|
217
|
+
* property with the slug as a bare string, which satisfies neither. Nothing
|
|
218
|
+
* threw — the resolver simply skipped every such property and reported that the
|
|
219
|
+
* collection had no relations. So an introspected BaaS collection had its FK
|
|
220
|
+
* columns removed from `properties` (they "surface as relations") and then no
|
|
221
|
+
* resolvable relation to surface as, which is why writing the FK column
|
|
222
|
+
* directly came back as `has no field 'product_id'`: `assertKnownWriteFields`
|
|
223
|
+
* learns that column from the resolved relation's `localKey`.
|
|
224
|
+
*
|
|
225
|
+
* The thunk closes over the collections being built in this same pass rather
|
|
226
|
+
* than importing a module, which is what a runtime introspection has instead of
|
|
227
|
+
* generated files. It is called lazily, after the map is fully populated, so a
|
|
228
|
+
* table may reference one introspected later.
|
|
213
229
|
*/
|
|
214
230
|
function buildRelations(
|
|
215
231
|
meta: TableMeta,
|
|
216
|
-
slugByTable: Map<string, string
|
|
232
|
+
slugByTable: Map<string, string>,
|
|
233
|
+
collectionBySlug: Map<string, PostgresCollectionConfig>
|
|
217
234
|
): Record<string, Record<string, unknown>> {
|
|
218
235
|
const relations: Record<string, Record<string, unknown>> = {};
|
|
219
236
|
|
|
@@ -233,10 +250,11 @@ function buildRelations(
|
|
|
233
250
|
relations[key] = {
|
|
234
251
|
name: humanize(key),
|
|
235
252
|
type: "relation",
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
relation: {
|
|
254
|
+
kind: "belongsTo",
|
|
255
|
+
target: () => collectionBySlug.get(targetSlug),
|
|
256
|
+
localKey: fk.column_name
|
|
257
|
+
}
|
|
240
258
|
};
|
|
241
259
|
}
|
|
242
260
|
|
|
@@ -259,6 +277,9 @@ export function buildCollectionsFromSchema(
|
|
|
259
277
|
}
|
|
260
278
|
|
|
261
279
|
const collections: PostgresCollectionConfig[] = [];
|
|
280
|
+
// Filled as we go; the relation thunks read it lazily, so a table may point
|
|
281
|
+
// at one that has not been built yet at the moment its relation is created.
|
|
282
|
+
const collectionBySlug = new Map<string, PostgresCollectionConfig>();
|
|
262
283
|
|
|
263
284
|
for (const [tableName, meta] of tablesMap) {
|
|
264
285
|
if (joinTables.has(tableName)) continue;
|
|
@@ -273,11 +294,12 @@ export function buildCollectionsFromSchema(
|
|
|
273
294
|
icon: getIconForTable(tableName),
|
|
274
295
|
properties: {
|
|
275
296
|
...buildProperties(meta, enumMap),
|
|
276
|
-
...buildRelations(meta, slugByTable)
|
|
297
|
+
...buildRelations(meta, slugByTable, collectionBySlug)
|
|
277
298
|
}
|
|
278
299
|
} as unknown as PostgresCollectionConfig;
|
|
279
300
|
|
|
280
301
|
collections.push(collection);
|
|
302
|
+
collectionBySlug.set(tableName, collection);
|
|
281
303
|
}
|
|
282
304
|
|
|
283
305
|
return collections;
|
|
@@ -158,15 +158,23 @@ describe("checkPolicyDrift", () => {
|
|
|
158
158
|
it("also flags the tautology in a WITH CHECK clause", async () => {
|
|
159
159
|
const cols = [collection("posts")];
|
|
160
160
|
const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(cols));
|
|
161
|
+
const withCheck = expected.filter((p) => p.hasWithCheck);
|
|
162
|
+
// Asserted rather than assumed: if the generator ever stopped emitting a
|
|
163
|
+
// WITH CHECK for these rules the fixture would carry no tautology at
|
|
164
|
+
// all, and every assertion below would be vacuously satisfied.
|
|
165
|
+
expect(withCheck.length).toBeGreaterThan(0);
|
|
166
|
+
|
|
161
167
|
const live = expected.map((p) => liveRow(p, {
|
|
162
168
|
with_check: p.hasWithCheck ? "(auth.uid() IS NOT NULL)" : null
|
|
163
169
|
}));
|
|
164
170
|
|
|
165
171
|
const drift = await checkPolicyDrift(dbWith(live), cols);
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
173
|
+
// Every WITH CHECK policy carries the tautology and no USING clause
|
|
174
|
+
// does, so anything flagged here came from the WITH CHECK scan — the
|
|
175
|
+
// half that a check reading only `qual` would miss entirely.
|
|
176
|
+
expect(drift.insecure.map((i) => i.policy.name)).toEqual(withCheck.map((p) => p.name));
|
|
177
|
+
expect(drift.insecure.every((i) => /WITH CHECK/.test(i.reason))).toBe(true);
|
|
170
178
|
});
|
|
171
179
|
|
|
172
180
|
it("parses roles when the driver returns the raw {a,b} text form", async () => {
|