@rebasepro/server-postgres 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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/BranchService-ByLfM_Tt.js +360 -0
- package/dist/BranchService-ByLfM_Tt.js.map +1 -0
- package/dist/__vite-browser-external-BnuHet1e.js +46 -0
- package/dist/__vite-browser-external-BnuHet1e.js.map +1 -0
- package/dist/arg-Dni7MzLB.js +87 -0
- package/dist/arg-Dni7MzLB.js.map +1 -0
- package/dist/auth-users-columns-D2LBFrMH.js +116 -0
- package/dist/auth-users-columns-D2LBFrMH.js.map +1 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/backup-cli-oM9WQVlV.js +408 -0
- package/dist/backup-cli-oM9WQVlV.js.map +1 -0
- package/dist/backup-service-Br3tqwsy.js +715 -0
- package/dist/backup-service-Br3tqwsy.js.map +1 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors-DxN4yzdH.js +657 -0
- package/dist/cli-errors-DxN4yzdH.js.map +1 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/cli-output-CNdMql-L.js +47 -0
- package/dist/cli-output-CNdMql-L.js.map +1 -0
- package/dist/cli.js +2020 -0
- package/dist/cli.js.map +1 -0
- package/dist/collection-index-BHgT_TKd.js +276 -0
- package/dist/collection-index-BHgT_TKd.js.map +1 -0
- package/dist/{collections-schema-version-BMeu3cgv.js → collections-schema-version-T6cGvrDi.js} +1 -2
- package/dist/{collections-schema-version-BMeu3cgv.js.map → collections-schema-version-T6cGvrDi.js.map} +1 -1
- package/dist/{connection-GOKU3Hu5.js → databasePoolManager-Bj5FbeAs.js} +112 -40
- package/dist/databasePoolManager-Bj5FbeAs.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/doctor-D7HWmGzs.js +974 -0
- package/dist/doctor-D7HWmGzs.js.map +1 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-2c2NV8zX.js} +7 -27
- package/dist/ensure-collection-policies-2c2NV8zX.js.map +1 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js +820 -0
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +1 -0
- package/dist/ensure-tables-CIojK-Rx.js +650 -0
- package/dist/ensure-tables-CIojK-Rx.js.map +1 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js +123 -0
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +1 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +528 -0
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +1 -0
- package/dist/{ensure-collection-tables-DgVixhX3.js → generate-postgres-ddl-logic-CtnpIBVv.js} +126 -984
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +1 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js +157 -0
- package/dist/generated-schema-staleness-DQdfJl2A.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +7423 -1133
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js +1378 -0
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -0
- package/dist/policy-drift-kNJ6X2Uj.js +313 -0
- package/dist/policy-drift-kNJ6X2Uj.js.map +1 -0
- package/dist/{backup-service-DCk7KhhL.js → rls-bootstrap-sql-DLDnUjtx.js} +737 -2689
- package/dist/rls-bootstrap-sql-DLDnUjtx.js.map +1 -0
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-DeYEjYLt.js} +65 -190
- package/dist/rls-enforcement-DeYEjYLt.js.map +1 -0
- package/dist/schema/bin/generate-drizzle-schema.d.ts +1 -0
- package/dist/schema/bin/generate-postgres-ddl.d.ts +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor-cli.js +129 -0
- package/dist/schema/doctor-cli.js.map +1 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-drizzle-schema.d.ts +1 -1
- package/dist/schema/generate-drizzle-schema.js +24 -0
- package/dist/schema/generate-drizzle-schema.js.map +1 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generate-postgres-ddl.d.ts +1 -1
- package/dist/schema/generate-postgres-ddl.js +138 -0
- package/dist/schema/generate-postgres-ddl.js.map +1 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/schema/introspect-db.js +866 -0
- package/dist/schema/introspect-db.js.map +1 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/source-Br7L7GOI.js +410 -0
- package/dist/source-Br7L7GOI.js.map +1 -0
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/dist/vector-index-DkWxm2de.js +309 -0
- package/dist/vector-index-DkWxm2de.js.map +1 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-policies-UCqgv_8c.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -1,56 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import pg from "pg";
|
|
5
|
-
import arg from "arg";
|
|
6
|
-
import * as dotenv from "dotenv";
|
|
7
|
-
import readline from "readline";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
buildTablesMap,
|
|
11
|
-
buildEnumMap,
|
|
12
|
-
generateCollectionFile,
|
|
13
|
-
generateIndexContent,
|
|
14
|
-
mergeIndexContent,
|
|
15
|
-
safeHostFromUrl
|
|
16
|
-
} from "./introspect-db-logic";
|
|
17
|
-
import { detectCollectionBuilder } from "./introspect-db-project";
|
|
18
|
-
import { countRowsUpTo, readSchemaMetadata } from "./introspect-db-queries";
|
|
19
|
-
import { classifyTables, lookupCandidates, LOOKUP_MAX_ROWS } from "./introspect-db-structure";
|
|
20
|
-
import { parseCheckConstraints } from "./introspect-db-constraints";
|
|
21
|
-
import { out, outWarn, outError } from "../cli-output";
|
|
22
|
-
|
|
23
|
-
async function main() {
|
|
24
|
-
const args = arg(
|
|
25
|
-
{
|
|
26
|
-
"--output": String,
|
|
27
|
-
"--collections": String,
|
|
28
|
-
"--force": Boolean,
|
|
29
|
-
"--schema": String,
|
|
30
|
-
"--data-inference": Boolean,
|
|
31
|
-
"--no-data-inference": Boolean,
|
|
32
|
-
"-o": "--output",
|
|
33
|
-
"-c": "--collections",
|
|
34
|
-
"-f": "--force"
|
|
35
|
-
},
|
|
36
|
-
{ permissive: true }
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const cwd = process.cwd();
|
|
40
|
-
const isBackendDir = path.basename(cwd) === "backend";
|
|
41
|
-
const defaultOutDir = isBackendDir
|
|
42
|
-
? path.resolve(cwd, "..", "config", "collections")
|
|
43
|
-
: path.resolve(cwd, "config", "collections");
|
|
44
|
-
|
|
45
|
-
const outDir = args["--output"] || args["--collections"] || defaultOutDir;
|
|
46
|
-
const force = args["--force"] || false;
|
|
47
|
-
const pgSchema = args["--schema"] || "public";
|
|
48
|
-
|
|
49
|
-
if (!fs.existsSync(outDir)) {
|
|
50
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Load env
|
|
54
|
-
const envPaths = [
|
|
55
|
-
process.env.DOTENV_CONFIG_PATH,
|
|
56
|
-
path.resolve(process.cwd(), ".env"),
|
|
57
|
-
path.resolve(process.cwd(), "../.env"),
|
|
58
|
-
path.resolve(process.cwd(), "../../.env")
|
|
59
|
-
].filter(Boolean) as string[];
|
|
60
|
-
|
|
61
|
-
for (const p of envPaths) {
|
|
62
|
-
if (fs.existsSync(p)) {
|
|
63
|
-
dotenv.config({ path: p, quiet: true });
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const databaseUrl = process.env.DATABASE_URL || process.env.ADMIN_CONNECTION_STRING;
|
|
69
|
-
if (!databaseUrl) {
|
|
70
|
-
outError(chalk.red("✗ DATABASE_URL is not set. Make sure your .env file is configured."));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const client = new pg.Client({ connectionString: databaseUrl });
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
await client.connect();
|
|
78
|
-
} catch (err) {
|
|
79
|
-
outError(chalk.red(`✗ Failed to connect to database: ${err instanceof Error ? err.message : String(err)}`));
|
|
80
|
-
outError(chalk.gray(" Check your DATABASE_URL and ensure the database is reachable."));
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Log the host portion safely — handle URLs without "@"
|
|
85
|
-
const hostPart = safeHostFromUrl(databaseUrl);
|
|
86
|
-
out(chalk.gray(`Connected to database: ${hostPart}`));
|
|
87
|
-
out(chalk.gray(`Introspecting schema '${pgSchema}'...`));
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const metadata = await readSchemaMetadata(client, pgSchema);
|
|
91
|
-
const enumMap = buildEnumMap(metadata.enumValues);
|
|
92
|
-
const tablesMap = buildTablesMap(metadata.tables, metadata.columns, metadata.pks, metadata.fks);
|
|
93
|
-
const fks = metadata.fks;
|
|
94
|
-
|
|
95
|
-
// Only tables that could structurally be a code list are counted, and
|
|
96
|
-
// each count stops at the threshold — see `countRowsUpTo`. Introspection
|
|
97
|
-
// runs against a database it does not own, so "cheap on a table of any
|
|
98
|
-
// size" is a requirement, not an optimization.
|
|
99
|
-
for (const table of lookupCandidates(metadata, tablesMap)) {
|
|
100
|
-
try {
|
|
101
|
-
metadata.rowCounts[table] = await countRowsUpTo(client, pgSchema, table, LOOKUP_MAX_ROWS);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
// A table this run cannot read is simply not classified as a code
|
|
104
|
-
// list; everything else about it still generates.
|
|
105
|
-
out(chalk.gray(` (skipped row count for ${table}: ${err instanceof Error ? err.message : String(err)})`));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const classifications = classifyTables(metadata, tablesMap);
|
|
110
|
-
const checkFacts = parseCheckConstraints(metadata.checks);
|
|
111
|
-
|
|
112
|
-
// Which builder the generated files may import — read from the manifests
|
|
113
|
-
// above `outDir`, because a project without `@rebasepro/cms-types` cannot
|
|
114
|
-
// resolve that import and has no `admin` block to write into either.
|
|
115
|
-
const builder = detectCollectionBuilder(outDir);
|
|
116
|
-
if (builder === "annotation") {
|
|
117
|
-
outWarn(chalk.yellow(
|
|
118
|
-
"⚠ Neither @rebasepro/cms-types nor @rebasepro/common is declared above " +
|
|
119
|
-
`${outDir}.`
|
|
120
|
-
));
|
|
121
|
-
outWarn(chalk.gray(
|
|
122
|
-
" Generating plain type annotations, which widen the property keys to `string`: " +
|
|
123
|
-
"`propertiesOrder` and friends will not be checked. Add @rebasepro/common to the " +
|
|
124
|
-
"config package to turn that checking on."
|
|
125
|
-
));
|
|
126
|
-
}
|
|
127
|
-
const joinTables = new Set(
|
|
128
|
-
Array.from(classifications.values())
|
|
129
|
-
.filter((c) => c.role === "junction")
|
|
130
|
-
.map((c) => c.table)
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const roleCount = (role: string) =>
|
|
134
|
-
Array.from(classifications.values()).filter((c) => c.role === role).length;
|
|
135
|
-
|
|
136
|
-
out(chalk.blue(`Found ${tablesMap.size} tables.`));
|
|
137
|
-
out(chalk.gray(
|
|
138
|
-
` ${roleCount("entity")} entities, ${joinTables.size} join tables (folded into relations), ` +
|
|
139
|
-
`${roleCount("lookup")} code lists, ${roleCount("owned-child")} owned by another table (hidden from navigation).`
|
|
140
|
-
));
|
|
141
|
-
|
|
142
|
-
let runDataInference = false;
|
|
143
|
-
if (args["--no-data-inference"]) {
|
|
144
|
-
runDataInference = false;
|
|
145
|
-
} else if (args["--data-inference"] !== undefined) {
|
|
146
|
-
runDataInference = args["--data-inference"];
|
|
147
|
-
} else if (!process.stdin.isTTY) {
|
|
148
|
-
// No terminal to answer the question below (scaffolding scripts, CI,
|
|
149
|
-
// `rebase init --introspect`) — asking would hang forever.
|
|
150
|
-
out(chalk.gray("Skipping data inference (non-interactive run; pass --data-inference to enable)."));
|
|
151
|
-
} else {
|
|
152
|
-
const rl = readline.createInterface({
|
|
153
|
-
input: process.stdin,
|
|
154
|
-
output: process.stdout
|
|
155
|
-
});
|
|
156
|
-
const answer = await new Promise<string>((resolve) => rl.question(chalk.yellow("? Do you want to run comprehensive data inference on sampled rows to auto-detect types, formats, constraints, and UI configurations? (y/N) "), resolve));
|
|
157
|
-
runDataInference = answer.trim().toLowerCase() === "y";
|
|
158
|
-
rl.close();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (runDataInference) {
|
|
162
|
-
out(chalk.gray("Sampling database rows for data inference..."));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Generate Collections
|
|
166
|
-
const generatedFiles: string[] = [];
|
|
167
|
-
const skippedFiles: string[] = [];
|
|
168
|
-
|
|
169
|
-
const tablesToProcess = Array.from(tablesMap.entries()).filter(([tableName]) => !joinTables.has(tableName));
|
|
170
|
-
|
|
171
|
-
const BATCH_SIZE = 10;
|
|
172
|
-
for (let i = 0; i < tablesToProcess.length; i += BATCH_SIZE) {
|
|
173
|
-
const batch = tablesToProcess.slice(i, i + BATCH_SIZE);
|
|
174
|
-
|
|
175
|
-
await Promise.all(batch.map(async ([tableName, meta]) => {
|
|
176
|
-
// ── File overwrite protection ──────────────────────────────
|
|
177
|
-
const filePath = path.join(outDir, `${tableName}.ts`);
|
|
178
|
-
if (fs.existsSync(filePath) && !force) {
|
|
179
|
-
skippedFiles.push(tableName);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
let sampleData: Record<string, unknown>[] | undefined = undefined;
|
|
184
|
-
if (runDataInference) {
|
|
185
|
-
try {
|
|
186
|
-
const { rows } = await client.query(`SELECT * FROM "${pgSchema}"."${tableName}" LIMIT 100`);
|
|
187
|
-
sampleData = rows;
|
|
188
|
-
} catch (err) {
|
|
189
|
-
outError(chalk.yellow(`⚠ Failed to sample data for table ${tableName}: ${err instanceof Error ? err.message : String(err)}`));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const fileContent = generateCollectionFile(
|
|
194
|
-
tableName,
|
|
195
|
-
meta,
|
|
196
|
-
fks,
|
|
197
|
-
joinTables,
|
|
198
|
-
tablesMap,
|
|
199
|
-
enumMap,
|
|
200
|
-
sampleData,
|
|
201
|
-
{ metadata, classifications, checkFacts, builder }
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
fs.writeFileSync(filePath, fileContent, "utf-8");
|
|
205
|
-
generatedFiles.push(tableName);
|
|
206
|
-
out(chalk.green(` ✓ ${filePath}`));
|
|
207
|
-
}));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Generate index.ts (sorted alphabetically for deterministic output)
|
|
211
|
-
if (generatedFiles.length > 0) {
|
|
212
|
-
const indexPath = path.join(outDir, "index.ts");
|
|
213
|
-
|
|
214
|
-
if (fs.existsSync(indexPath) && !force) {
|
|
215
|
-
// Merge: read existing index, add new exports that don't already exist
|
|
216
|
-
const existing = fs.readFileSync(indexPath, "utf-8");
|
|
217
|
-
const merged = mergeIndexContent(existing, generatedFiles);
|
|
218
|
-
fs.writeFileSync(indexPath, merged, "utf-8");
|
|
219
|
-
} else {
|
|
220
|
-
// --force replaces collections derived from the database, but the
|
|
221
|
-
// directory can also hold hand-written ones with no table in the
|
|
222
|
-
// introspected schema (the auth users collection lives in
|
|
223
|
-
// "rebase"). The backend discovers the whole directory, so an
|
|
224
|
-
// index listing only introspected tables would silently drop them
|
|
225
|
-
// from the admin UI while the API still served them.
|
|
226
|
-
const siblings = fs.readdirSync(outDir)
|
|
227
|
-
.filter(f => f.endsWith(".ts") && f !== "index.ts")
|
|
228
|
-
.map(f => f.replace(/\.ts$/, ""));
|
|
229
|
-
const allFiles = [...new Set([...generatedFiles, ...siblings])];
|
|
230
|
-
const indexContent = generateIndexContent(allFiles);
|
|
231
|
-
fs.writeFileSync(indexPath, indexContent, "utf-8");
|
|
232
|
-
}
|
|
233
|
-
out(chalk.green(` ✓ ${indexPath}`));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
out("");
|
|
237
|
-
if (skippedFiles.length > 0) {
|
|
238
|
-
out(chalk.yellow(`⚠ Skipped ${skippedFiles.length} existing file(s): ${skippedFiles.join(", ")}`));
|
|
239
|
-
out(chalk.gray(" Use --force to overwrite existing files."));
|
|
240
|
-
out("");
|
|
241
|
-
}
|
|
242
|
-
out(chalk.bold.green(`✓ Introspected ${tablesMap.size} tables — generated ${generatedFiles.length} collection(s).`));
|
|
243
|
-
out(chalk.gray(` Review the generated files in ${outDir} and customize properties as needed.`));
|
|
244
|
-
out("");
|
|
245
|
-
|
|
246
|
-
} catch (e) {
|
|
247
|
-
outError(chalk.red(`✗ Error introspecting database: ${e instanceof Error ? e.message : String(e)}`));
|
|
248
|
-
process.exit(1);
|
|
249
|
-
} finally {
|
|
250
|
-
await client.end();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
main().catch((err) => {
|
|
255
|
-
outError(String(err));
|
|
256
|
-
process.exit(1);
|
|
257
|
-
});
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "@jest/globals";
|
|
2
|
-
import { resolveCollectionRelations } from "@rebasepro/common";
|
|
3
|
-
import type { CollectionConfig } from "@rebasepro/types";
|
|
4
|
-
|
|
5
|
-
import { buildCollectionsFromSchema, introspectSchema, readRlsStatus, IntrospectedSchema, Queryable } from "./introspect-runtime";
|
|
6
|
-
import { buildTablesMap, identifyJoinTables, TableColumn, ForeignKeyRow, PrimaryKeyRow } from "./introspect-db-logic";
|
|
7
|
-
|
|
8
|
-
function column(overrides: Partial<TableColumn> & { table_name: string; column_name: string }): TableColumn {
|
|
9
|
-
return {
|
|
10
|
-
data_type: "text",
|
|
11
|
-
udt_name: "text",
|
|
12
|
-
is_nullable: "YES",
|
|
13
|
-
column_default: null,
|
|
14
|
-
atttypmod: null,
|
|
15
|
-
...overrides
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function schemaOf(
|
|
20
|
-
columns: TableColumn[],
|
|
21
|
-
pks: PrimaryKeyRow[],
|
|
22
|
-
fks: ForeignKeyRow[] = [],
|
|
23
|
-
enums: Map<string, string[]> = new Map()
|
|
24
|
-
): IntrospectedSchema {
|
|
25
|
-
const tableNames = [...new Set(columns.map((c) => c.table_name))].map((table_name) => ({ table_name }));
|
|
26
|
-
const tablesMap = buildTablesMap(tableNames, columns, pks, fks);
|
|
27
|
-
return { tablesMap, enumMap: enums, joinTables: identifyJoinTables(tablesMap) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("buildCollectionsFromSchema", () => {
|
|
31
|
-
it("derives a collection per table with humanized names and the pg schema", () => {
|
|
32
|
-
const schema = schemaOf(
|
|
33
|
-
[
|
|
34
|
-
column({ table_name: "blog_posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
35
|
-
column({ table_name: "blog_posts", column_name: "title", is_nullable: "NO" })
|
|
36
|
-
],
|
|
37
|
-
[{ table_name: "blog_posts", column_name: "id" }]
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const [collection] = buildCollectionsFromSchema(schema, "public");
|
|
41
|
-
|
|
42
|
-
expect(collection.slug).toBe("blog_posts");
|
|
43
|
-
expect(collection.table).toBe("blog_posts");
|
|
44
|
-
expect((collection as { schema?: string }).schema).toBe("public");
|
|
45
|
-
expect(collection.name).toBe("Blog Posts");
|
|
46
|
-
expect(collection.singularName).toBe("Blog Post");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("maps a uuid primary key to isId uuid and a serial one to increment", () => {
|
|
50
|
-
const schema = schemaOf(
|
|
51
|
-
[
|
|
52
|
-
column({ table_name: "users", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
53
|
-
column({ table_name: "orders", column_name: "id", data_type: "integer", udt_name: "int4", is_nullable: "NO" })
|
|
54
|
-
],
|
|
55
|
-
[
|
|
56
|
-
{ table_name: "users", column_name: "id" },
|
|
57
|
-
{ table_name: "orders", column_name: "id" }
|
|
58
|
-
]
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const collections = buildCollectionsFromSchema(schema, "public");
|
|
62
|
-
const users = collections.find((c) => c.slug === "users")!;
|
|
63
|
-
const orders = collections.find((c) => c.slug === "orders")!;
|
|
64
|
-
|
|
65
|
-
expect((users.properties as any).id.isId).toBe("uuid");
|
|
66
|
-
expect((orders.properties as any).id.isId).toBe("increment");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("marks NOT NULL columns without a default as required, and leaves others optional", () => {
|
|
70
|
-
const schema = schemaOf(
|
|
71
|
-
[
|
|
72
|
-
column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
73
|
-
column({ table_name: "posts", column_name: "title", is_nullable: "NO" }),
|
|
74
|
-
column({ table_name: "posts", column_name: "subtitle", is_nullable: "YES" }),
|
|
75
|
-
column({ table_name: "posts", column_name: "status", is_nullable: "NO", column_default: "'draft'" })
|
|
76
|
-
],
|
|
77
|
-
[{ table_name: "posts", column_name: "id" }]
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const props = buildCollectionsFromSchema(schema, "public")[0].properties as any;
|
|
81
|
-
|
|
82
|
-
expect(props.title.validation).toEqual({ required: true });
|
|
83
|
-
expect(props.subtitle.validation).toBeUndefined();
|
|
84
|
-
// A default means the database fills it in, so the API must not demand it.
|
|
85
|
-
expect(props.status.validation).toBeUndefined();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("expands a pg enum column into enum entries", () => {
|
|
89
|
-
const schema = schemaOf(
|
|
90
|
-
[
|
|
91
|
-
column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
92
|
-
column({ table_name: "posts", column_name: "status", data_type: "USER-DEFINED", udt_name: "post_status" })
|
|
93
|
-
],
|
|
94
|
-
[{ table_name: "posts", column_name: "id" }],
|
|
95
|
-
[],
|
|
96
|
-
new Map([["post_status", ["draft", "in_review"]]])
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const props = buildCollectionsFromSchema(schema, "public")[0].properties as any;
|
|
100
|
-
|
|
101
|
-
expect(props.status.type).toBe("string");
|
|
102
|
-
expect(props.status.enum).toEqual([
|
|
103
|
-
{ id: "draft", label: "Draft" },
|
|
104
|
-
{ id: "in_review", label: "In Review" }
|
|
105
|
-
]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("turns a foreign key into an owning relation and drops the raw fk column", () => {
|
|
109
|
-
const schema = schemaOf(
|
|
110
|
-
[
|
|
111
|
-
column({ table_name: "authors", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
112
|
-
column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
113
|
-
column({ table_name: "posts", column_name: "author_id", data_type: "uuid", udt_name: "uuid" })
|
|
114
|
-
],
|
|
115
|
-
[
|
|
116
|
-
{ table_name: "authors", column_name: "id" },
|
|
117
|
-
{ table_name: "posts", column_name: "id" }
|
|
118
|
-
],
|
|
119
|
-
[{ table_name: "posts", column_name: "author_id", foreign_table_name: "authors", foreign_column_name: "id" }]
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const posts = buildCollectionsFromSchema(schema, "public").find((c) => c.slug === "posts")!;
|
|
123
|
-
const props = posts.properties as any;
|
|
124
|
-
|
|
125
|
-
expect(props.author_id).toBeUndefined();
|
|
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");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("skips join tables — they are an edge between collections, not a collection", () => {
|
|
183
|
-
const schema = schemaOf(
|
|
184
|
-
[
|
|
185
|
-
column({ table_name: "posts", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
186
|
-
column({ table_name: "tags", column_name: "id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
187
|
-
column({ table_name: "posts_tags", column_name: "post_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" }),
|
|
188
|
-
column({ table_name: "posts_tags", column_name: "tag_id", data_type: "uuid", udt_name: "uuid", is_nullable: "NO" })
|
|
189
|
-
],
|
|
190
|
-
[
|
|
191
|
-
{ table_name: "posts", column_name: "id" },
|
|
192
|
-
{ table_name: "tags", column_name: "id" },
|
|
193
|
-
{ table_name: "posts_tags", column_name: "post_id" },
|
|
194
|
-
{ table_name: "posts_tags", column_name: "tag_id" }
|
|
195
|
-
],
|
|
196
|
-
[
|
|
197
|
-
{ table_name: "posts_tags", column_name: "post_id", foreign_table_name: "posts", foreign_column_name: "id" },
|
|
198
|
-
{ table_name: "posts_tags", column_name: "tag_id", foreign_table_name: "tags", foreign_column_name: "id" }
|
|
199
|
-
]
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const slugs = buildCollectionsFromSchema(schema, "public").map((c) => c.slug);
|
|
203
|
-
|
|
204
|
-
expect(slugs).toEqual(expect.arrayContaining(["posts", "tags"]));
|
|
205
|
-
expect(slugs).not.toContain("posts_tags");
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe("introspectSchema", () => {
|
|
210
|
-
it("scopes every query to the requested schema", async () => {
|
|
211
|
-
const calls: unknown[][] = [];
|
|
212
|
-
const client: Queryable = {
|
|
213
|
-
query: async (_text: string, values?: unknown[]) => {
|
|
214
|
-
calls.push(values ?? []);
|
|
215
|
-
return { rows: [] as any[] };
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
await introspectSchema(client, "analytics");
|
|
220
|
-
|
|
221
|
-
expect(calls.length).toBeGreaterThan(0);
|
|
222
|
-
for (const values of calls) {
|
|
223
|
-
expect(values).toEqual(["analytics"]);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("readRlsStatus", () => {
|
|
229
|
-
it("reports RLS state and policy count per table", async () => {
|
|
230
|
-
const client: Queryable = {
|
|
231
|
-
query: async () => ({
|
|
232
|
-
rows: [
|
|
233
|
-
{ table: "posts", rls_enabled: true, policy_count: "2" },
|
|
234
|
-
{ table: "secrets", rls_enabled: false, policy_count: "0" },
|
|
235
|
-
{ table: "locked", rls_enabled: true, policy_count: 0 }
|
|
236
|
-
] as never[]
|
|
237
|
-
})
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const status = await readRlsStatus(client, "public");
|
|
241
|
-
|
|
242
|
-
// Unprotected: no authorization model, so baas must not serve it.
|
|
243
|
-
expect(status.get("secrets")).toEqual({ table: "secrets", rlsEnabled: false, policyCount: 0 });
|
|
244
|
-
// Protected and policied.
|
|
245
|
-
expect(status.get("posts")).toEqual({ table: "posts", rlsEnabled: true, policyCount: 2 });
|
|
246
|
-
// RLS on, no policies — legal, and returns nothing at all.
|
|
247
|
-
expect(status.get("locked")).toEqual({ table: "locked", rlsEnabled: true, policyCount: 0 });
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("scopes the lookup to the requested schema", async () => {
|
|
251
|
-
const calls: unknown[][] = [];
|
|
252
|
-
const client: Queryable = {
|
|
253
|
-
query: async (_t: string, v?: unknown[]) => { calls.push(v ?? []); return { rows: [] as never[] }; }
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
await readRlsStatus(client, "analytics");
|
|
257
|
-
|
|
258
|
-
expect(calls[0]).toEqual(["analytics"]);
|
|
259
|
-
});
|
|
260
|
-
});
|