@apso/cli 0.30.0 → 0.31.0

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.
@@ -0,0 +1,42 @@
1
+ /// <reference types="node" />
2
+ import BaseCommand from "../../lib/base-command";
3
+ import { ApsorcImportOutput, ImportReport, Introspector } from "../../lib/import";
4
+ /**
5
+ * Resolve the Postgres connection string from an explicit flag or, failing
6
+ * that, the SUPABASE_DB_URL / DATABASE_URL environment variables. Returns
7
+ * undefined if none is set (the command then prompts interactively).
8
+ */
9
+ export declare function resolveConnectionString(flagValue: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
10
+ /**
11
+ * Run the full introspect -> convert -> validate pipeline against any
12
+ * Introspector. Throws if the generated relationships fail to parse. Pure of
13
+ * file/console I/O so it can be unit-tested with a fake Introspector.
14
+ */
15
+ export declare function importFromIntrospector(introspector: Introspector, schema: string): Promise<{
16
+ apsorc: ApsorcImportOutput;
17
+ report: ImportReport;
18
+ unknownTypes: string[];
19
+ }>;
20
+ /** Render the import report as a human-readable summary (contains no secrets). */
21
+ export declare function formatReport(report: ImportReport): string;
22
+ /**
23
+ * Write the generated .apsorc to disk, backing up any existing file first.
24
+ * Returns the backup path if one was made.
25
+ */
26
+ export declare function writeApsorcFile(apsorc: ApsorcImportOutput, outPath: string): {
27
+ backupPath?: string;
28
+ };
29
+ export default class ImportSupabase extends BaseCommand {
30
+ static description: string;
31
+ static examples: string[];
32
+ static flags: {
33
+ "connection-string": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
34
+ schema: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
35
+ out: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser").CustomOptions>;
36
+ "dry-run": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
37
+ yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
38
+ };
39
+ /** Overridable for testing. */
40
+ protected makeIntrospector(connectionString: string): Introspector;
41
+ run(): Promise<void>;
42
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeApsorcFile = exports.formatReport = exports.importFromIntrospector = exports.resolveConnectionString = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const core_1 = require("@oclif/core");
6
+ const fs = tslib_1.__importStar(require("fs"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
9
+ const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
10
+ const config_1 = require("../../lib/config");
11
+ const apsorc_parser_1 = require("../../lib/apsorc-parser");
12
+ const relationships_1 = require("../../lib/utils/relationships");
13
+ const import_1 = require("../../lib/import");
14
+ /**
15
+ * Resolve the Postgres connection string from an explicit flag or, failing
16
+ * that, the SUPABASE_DB_URL / DATABASE_URL environment variables. Returns
17
+ * undefined if none is set (the command then prompts interactively).
18
+ */
19
+ function resolveConnectionString(flagValue, env = process.env) {
20
+ return flagValue || env.SUPABASE_DB_URL || env.DATABASE_URL || undefined;
21
+ }
22
+ exports.resolveConnectionString = resolveConnectionString;
23
+ /**
24
+ * Run the full introspect -> convert -> validate pipeline against any
25
+ * Introspector. Throws if the generated relationships fail to parse. Pure of
26
+ * file/console I/O so it can be unit-tested with a fake Introspector.
27
+ */
28
+ async function importFromIntrospector(introspector, schema) {
29
+ const introspected = await introspector.introspect(schema);
30
+ const { apsorc, report } = (0, import_1.pgToApsorc)(introspected);
31
+ // Validate by running the generated relationships through the real parser.
32
+ (0, relationships_1.parseRelationships)(apsorc.relationships);
33
+ return {
34
+ apsorc,
35
+ report,
36
+ unknownTypes: (0, import_1.findUnknownEmittedTypes)(apsorc),
37
+ };
38
+ }
39
+ exports.importFromIntrospector = importFromIntrospector;
40
+ /** Render the import report as a human-readable summary (contains no secrets). */
41
+ function formatReport(report) {
42
+ const lines = [];
43
+ lines.push(`Imported ${report.tablesImported.length} table(s) and ${report.relationships} relationship(s).`);
44
+ if (report.tablesImported.length > 0) {
45
+ lines.push(` Tables: ${report.tablesImported.join(", ")}`);
46
+ }
47
+ if (report.viewsSkipped.length > 0) {
48
+ lines.push(` Skipped views: ${report.viewsSkipped.join(", ")}`);
49
+ }
50
+ if (report.systemSchemasSkipped.length > 0) {
51
+ lines.push(` Skipped system schemas: ${report.systemSchemasSkipped.join(", ")}`);
52
+ }
53
+ const w = report.warnings;
54
+ const review = [];
55
+ if (w.compositePks.length > 0)
56
+ review.push(` - Composite primary keys (review): ${w.compositePks.join(", ")}`);
57
+ if (w.nonStandardPks.length > 0)
58
+ review.push(` - Non-"id" primary keys (kept as fields): ${w.nonStandardPks.join(", ")}`);
59
+ if (w.noPrimaryKey.length > 0)
60
+ review.push(` - Tables with no primary key: ${w.noPrimaryKey.join(", ")}`);
61
+ if (w.compositeFks.length > 0)
62
+ review.push(` - Composite foreign keys (kept as columns, no relationship): ${w.compositeFks.join(", ")}`);
63
+ if (w.fkColumnNameUnmapped.length > 0)
64
+ review.push(` - Foreign keys whose generated column may differ: ${w.fkColumnNameUnmapped.join(", ")}`);
65
+ if (w.joinTablesDetected.length > 0)
66
+ review.push(` - Join tables (could be modeled as ManyToMany): ${w.joinTablesDetected.join(", ")}`);
67
+ if (w.arraysLossy.length > 0)
68
+ review.push(` - Array columns reduced to text: ${w.arraysLossy.join(", ")}`);
69
+ if (w.typesDefaulted.length > 0)
70
+ review.push(` - Unknown types defaulted to text: ${w.typesDefaulted
71
+ .map((t) => `${t.column} (${t.udt})`)
72
+ .join(", ")}`);
73
+ if (w.defaultsDropped.length > 0)
74
+ review.push(` - Column defaults dropped: ${w.defaultsDropped.join(", ")}`);
75
+ if (review.length > 0) {
76
+ lines.push("", "Review the following (not fully represented):", ...review);
77
+ }
78
+ return lines.join("\n");
79
+ }
80
+ exports.formatReport = formatReport;
81
+ /**
82
+ * Write the generated .apsorc to disk, backing up any existing file first.
83
+ * Returns the backup path if one was made.
84
+ */
85
+ function writeApsorcFile(apsorc, outPath) {
86
+ let backupPath;
87
+ if (fs.existsSync(outPath)) {
88
+ backupPath = (0, config_1.createBackup)(outPath);
89
+ }
90
+ fs.writeFileSync(outPath, JSON.stringify(apsorc, null, 2), "utf-8");
91
+ return { backupPath };
92
+ }
93
+ exports.writeApsorcFile = writeApsorcFile;
94
+ class ImportSupabase extends base_command_1.default {
95
+ /** Overridable for testing. */
96
+ makeIntrospector(connectionString) {
97
+ return new import_1.PgIntrospector(connectionString);
98
+ }
99
+ async run() {
100
+ var _a, _b;
101
+ const { flags } = await this.parse(ImportSupabase);
102
+ let connectionString = resolveConnectionString(flags["connection-string"]);
103
+ if (!connectionString) {
104
+ const answer = await inquirer_1.default.prompt([
105
+ {
106
+ type: "password",
107
+ name: "connectionString",
108
+ mask: "*",
109
+ message: "Supabase Postgres connection string:",
110
+ validate: (input) => input.trim().length > 0 || "A connection string is required",
111
+ },
112
+ ]);
113
+ connectionString = answer.connectionString.trim();
114
+ }
115
+ try {
116
+ this.log("Connecting and introspecting schema (read-only)...");
117
+ const introspector = this.makeIntrospector(connectionString);
118
+ const { apsorc, report, unknownTypes } = await importFromIntrospector(introspector, flags.schema);
119
+ this.log("");
120
+ this.log(formatReport(report));
121
+ if (unknownTypes.length > 0) {
122
+ this.warn(`Some column types were not recognized and defaulted to text: ${[
123
+ ...new Set(unknownTypes),
124
+ ].join(", ")}`);
125
+ }
126
+ if (report.tablesImported.length === 0) {
127
+ this.warn(`No base tables found in schema "${flags.schema}". Nothing to write.`);
128
+ return;
129
+ }
130
+ if (flags["dry-run"]) {
131
+ this.log("");
132
+ this.log("Dry run — no file written.");
133
+ return;
134
+ }
135
+ const outPath = (_b = (_a = flags.out) !== null && _a !== void 0 ? _a : (0, apsorc_parser_1.findConfigPath)()) !== null && _b !== void 0 ? _b : path.join(process.cwd(), ".apsorc");
136
+ if (fs.existsSync(outPath) && !flags.yes) {
137
+ const { confirm } = await inquirer_1.default.prompt([
138
+ {
139
+ type: "confirm",
140
+ name: "confirm",
141
+ message: `Overwrite ${outPath} with the imported schema?`,
142
+ default: false,
143
+ },
144
+ ]);
145
+ if (!confirm) {
146
+ this.log("Import cancelled.");
147
+ return;
148
+ }
149
+ }
150
+ const { backupPath } = writeApsorcFile(apsorc, outPath);
151
+ if (backupPath)
152
+ this.log(`Backup created: ${backupPath}`);
153
+ this.log(`Schema written to ${outPath}`);
154
+ this.log("");
155
+ this.log("Next steps:");
156
+ this.log(" - Review the .apsorc, especially any items flagged above");
157
+ this.log(" - Run 'apso migrate' to validate the schema locally");
158
+ this.log(" - Run 'apso generate' to scaffold code");
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ // Defense-in-depth: never surface the connection string in an error.
163
+ this.error((0, import_1.redactConnectionString)(message.split(connectionString).join("****")));
164
+ }
165
+ }
166
+ }
167
+ ImportSupabase.description = "Introspect an existing Supabase (Postgres) database and generate a local .apsorc schema. Read-only: it never writes to the source database.";
168
+ ImportSupabase.examples = [
169
+ `$ apso import supabase`,
170
+ `$ apso import supabase -c "postgresql://postgres:[pw]@db.[ref].supabase.co:5432/postgres"`,
171
+ `$ SUPABASE_DB_URL="postgresql://..." apso import supabase --dry-run`,
172
+ ];
173
+ ImportSupabase.flags = {
174
+ "connection-string": core_1.Flags.string({
175
+ char: "c",
176
+ description: "Postgres connection string. Falls back to SUPABASE_DB_URL or DATABASE_URL, then prompts.",
177
+ }),
178
+ schema: core_1.Flags.string({
179
+ description: "Database schema to import",
180
+ default: "public",
181
+ }),
182
+ out: core_1.Flags.string({
183
+ char: "o",
184
+ description: "Path to write the .apsorc (default: existing .apsorc or ./.apsorc)",
185
+ }),
186
+ "dry-run": core_1.Flags.boolean({
187
+ description: "Introspect and print a summary without writing any file",
188
+ default: false,
189
+ }),
190
+ yes: core_1.Flags.boolean({
191
+ char: "y",
192
+ description: "Skip the overwrite confirmation prompt",
193
+ default: false,
194
+ }),
195
+ };
196
+ exports.default = ImportSupabase;
@@ -0,0 +1,4 @@
1
+ export * from "./types";
2
+ export { pgToApsorc, findUnknownEmittedTypes } from "./pg-to-apsorc";
3
+ export type { ApsorcImportOutput, ApsorcImportEntityOutput, ApsorcImportFieldOutput, ApsorcImportRelationshipOutput, } from "./pg-to-apsorc";
4
+ export { PgIntrospector, redactConnectionString, buildSchema, SYSTEM_SCHEMAS } from "./introspect";
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SYSTEM_SCHEMAS = exports.buildSchema = exports.redactConnectionString = exports.PgIntrospector = exports.findUnknownEmittedTypes = exports.pgToApsorc = void 0;
4
+ const tslib_1 = require("tslib");
5
+ tslib_1.__exportStar(require("./types"), exports);
6
+ var pg_to_apsorc_1 = require("./pg-to-apsorc");
7
+ Object.defineProperty(exports, "pgToApsorc", { enumerable: true, get: function () { return pg_to_apsorc_1.pgToApsorc; } });
8
+ Object.defineProperty(exports, "findUnknownEmittedTypes", { enumerable: true, get: function () { return pg_to_apsorc_1.findUnknownEmittedTypes; } });
9
+ var introspect_1 = require("./introspect");
10
+ Object.defineProperty(exports, "PgIntrospector", { enumerable: true, get: function () { return introspect_1.PgIntrospector; } });
11
+ Object.defineProperty(exports, "redactConnectionString", { enumerable: true, get: function () { return introspect_1.redactConnectionString; } });
12
+ Object.defineProperty(exports, "buildSchema", { enumerable: true, get: function () { return introspect_1.buildSchema; } });
13
+ Object.defineProperty(exports, "SYSTEM_SCHEMAS", { enumerable: true, get: function () { return introspect_1.SYSTEM_SCHEMAS; } });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Database Import — introspection layer
3
+ *
4
+ * Reads the schema of a live Postgres/Supabase database. This layer is
5
+ * deliberately thin and side-effect-light: it only issues read-only SELECTs
6
+ * against information_schema/pg_catalog and returns an `IntrospectedSchema`.
7
+ * All interpretation lives in the conversion layer (pg-to-apsorc.ts).
8
+ *
9
+ * `pg` is loaded lazily so importing this module (e.g. from the command) does
10
+ * not pull the driver until an actual connection is made.
11
+ */
12
+ import { IntrospectedSchema, Introspector } from "./types";
13
+ /** Supabase- and Postgres-managed schemas we never import from. */
14
+ export declare const SYSTEM_SCHEMAS: Set<string>;
15
+ /** Strip credentials from a connection string for safe display in errors. */
16
+ export declare function redactConnectionString(input: string): string;
17
+ /**
18
+ * Reads schema metadata from a Postgres database via a connection string.
19
+ * SSL is enabled by default (Supabase requires it).
20
+ */
21
+ export declare class PgIntrospector implements Introspector {
22
+ private readonly connectionString;
23
+ constructor(connectionString: string);
24
+ private connect;
25
+ introspect(schema: string): Promise<IntrospectedSchema>;
26
+ }
27
+ interface RawRows {
28
+ tableRows: any[];
29
+ columnRows: any[];
30
+ pkRows: any[];
31
+ fkRows: any[];
32
+ uniqueRows: any[];
33
+ indexRows: any[];
34
+ enumRows: any[];
35
+ schemaRows: any[];
36
+ }
37
+ /**
38
+ * Assemble raw query rows into an IntrospectedSchema. Exported (separate from
39
+ * the DB connection) so it can be unit-tested with fixture rows.
40
+ */
41
+ export declare function buildSchema(schema: string, raw: RawRows): IntrospectedSchema;
42
+ export {};
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+ /**
3
+ * Database Import — introspection layer
4
+ *
5
+ * Reads the schema of a live Postgres/Supabase database. This layer is
6
+ * deliberately thin and side-effect-light: it only issues read-only SELECTs
7
+ * against information_schema/pg_catalog and returns an `IntrospectedSchema`.
8
+ * All interpretation lives in the conversion layer (pg-to-apsorc.ts).
9
+ *
10
+ * `pg` is loaded lazily so importing this module (e.g. from the command) does
11
+ * not pull the driver until an actual connection is made.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.buildSchema = exports.PgIntrospector = exports.redactConnectionString = exports.SYSTEM_SCHEMAS = void 0;
38
+ /** Supabase- and Postgres-managed schemas we never import from. */
39
+ exports.SYSTEM_SCHEMAS = new Set([
40
+ "auth",
41
+ "storage",
42
+ "realtime",
43
+ "_realtime",
44
+ "vault",
45
+ "extensions",
46
+ "graphql",
47
+ "graphql_public",
48
+ "pgsodium",
49
+ "pgsodium_masks",
50
+ "pgbouncer",
51
+ "cron",
52
+ "net",
53
+ "supabase_functions",
54
+ "supabase_migrations",
55
+ "_analytics",
56
+ "information_schema",
57
+ "pg_catalog",
58
+ "pg_toast",
59
+ ]);
60
+ const CONFDELTYPE_TO_ACTION = {
61
+ a: "NO ACTION",
62
+ r: "RESTRICT",
63
+ c: "CASCADE",
64
+ n: "SET NULL",
65
+ d: "SET DEFAULT",
66
+ };
67
+ /** Strip credentials from a connection string for safe display in errors. */
68
+ function redactConnectionString(input) {
69
+ return input.replace(/(\/\/[^/:@]+:)[^@]*(@)/, "$1****$2");
70
+ }
71
+ exports.redactConnectionString = redactConnectionString;
72
+ /**
73
+ * Reads schema metadata from a Postgres database via a connection string.
74
+ * SSL is enabled by default (Supabase requires it).
75
+ */
76
+ class PgIntrospector {
77
+ constructor(connectionString) {
78
+ this.connectionString = connectionString;
79
+ }
80
+ async connect() {
81
+ var _a, _b;
82
+ // Lazy import keeps `pg` out of the module graph until we actually connect.
83
+ const pg = await Promise.resolve().then(() => __importStar(require("pg")));
84
+ const Client = (_a = pg.Client) !== null && _a !== void 0 ? _a : (_b = pg.default) === null || _b === void 0 ? void 0 : _b.Client;
85
+ const client = new Client({
86
+ connectionString: this.connectionString,
87
+ ssl: { rejectUnauthorized: false },
88
+ });
89
+ await client.connect();
90
+ return client;
91
+ }
92
+ async introspect(schema) {
93
+ let client;
94
+ try {
95
+ client = await this.connect();
96
+ const [tableRows, columnRows, pkRows, fkRows, uniqueRows, indexRows, enumRows, schemaRows,] = [
97
+ await client.query(TABLES_SQL, [schema]),
98
+ await client.query(COLUMNS_SQL, [schema]),
99
+ await client.query(PRIMARY_KEYS_SQL, [schema]),
100
+ await client.query(FOREIGN_KEYS_SQL, [schema]),
101
+ await client.query(UNIQUE_SQL, [schema]),
102
+ await client.query(INDEXES_SQL, [schema]),
103
+ await client.query(ENUMS_SQL, [schema]),
104
+ await client.query(SCHEMAS_SQL),
105
+ ];
106
+ return buildSchema(schema, {
107
+ tableRows: tableRows.rows,
108
+ columnRows: columnRows.rows,
109
+ pkRows: pkRows.rows,
110
+ fkRows: fkRows.rows,
111
+ uniqueRows: uniqueRows.rows,
112
+ indexRows: indexRows.rows,
113
+ enumRows: enumRows.rows,
114
+ schemaRows: schemaRows.rows,
115
+ });
116
+ }
117
+ catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ throw new Error(`Failed to introspect database: ${redactConnectionString(message)}`);
120
+ }
121
+ finally {
122
+ if (client)
123
+ await client.end();
124
+ }
125
+ }
126
+ }
127
+ exports.PgIntrospector = PgIntrospector;
128
+ const TABLES_SQL = `
129
+ SELECT table_name, table_type
130
+ FROM information_schema.tables
131
+ WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
132
+ ORDER BY table_name`;
133
+ const COLUMNS_SQL = `
134
+ SELECT table_name, column_name, udt_name, data_type, is_nullable,
135
+ column_default, character_maximum_length, numeric_precision,
136
+ numeric_scale, ordinal_position
137
+ FROM information_schema.columns
138
+ WHERE table_schema = $1
139
+ ORDER BY table_name, ordinal_position`;
140
+ const PRIMARY_KEYS_SQL = `
141
+ SELECT cl.relname AS table_name, att.attname AS column_name, ord.n AS key_index
142
+ FROM pg_constraint con
143
+ JOIN pg_class cl ON cl.oid = con.conrelid
144
+ JOIN pg_namespace ns ON ns.oid = cl.relnamespace
145
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS ord(attnum, n) ON true
146
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ord.attnum
147
+ WHERE con.contype = 'p' AND ns.nspname = $1
148
+ ORDER BY cl.relname, ord.n`;
149
+ const FOREIGN_KEYS_SQL = `
150
+ SELECT con.conname, cl.relname AS table_name, att.attname AS column_name,
151
+ clf.relname AS referenced_table, attf.attname AS referenced_column,
152
+ con.confdeltype, ord.n AS key_index
153
+ FROM pg_constraint con
154
+ JOIN pg_class cl ON cl.oid = con.conrelid
155
+ JOIN pg_namespace ns ON ns.oid = cl.relnamespace
156
+ JOIN pg_class clf ON clf.oid = con.confrelid
157
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS ord(attnum, n) ON true
158
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ord.attnum
159
+ JOIN pg_attribute attf ON attf.attrelid = con.confrelid AND attf.attnum = con.confkey[ord.n]
160
+ WHERE con.contype = 'f' AND ns.nspname = $1
161
+ ORDER BY con.conname, ord.n`;
162
+ const UNIQUE_SQL = `
163
+ SELECT con.conname, cl.relname AS table_name, att.attname AS column_name, ord.n AS key_index
164
+ FROM pg_constraint con
165
+ JOIN pg_class cl ON cl.oid = con.conrelid
166
+ JOIN pg_namespace ns ON ns.oid = cl.relnamespace
167
+ JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS ord(attnum, n) ON true
168
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ord.attnum
169
+ WHERE con.contype = 'u' AND ns.nspname = $1
170
+ ORDER BY con.conname, ord.n`;
171
+ const INDEXES_SQL = `
172
+ SELECT tc.relname AS table_name, ic.relname AS index_name,
173
+ a.attname AS column_name, ix.indisunique, k.n AS ord
174
+ FROM pg_index ix
175
+ JOIN pg_class ic ON ic.oid = ix.indexrelid
176
+ JOIN pg_class tc ON tc.oid = ix.indrelid
177
+ JOIN pg_namespace ns ON ns.oid = tc.relnamespace
178
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
179
+ JOIN pg_attribute a ON a.attrelid = tc.oid AND a.attnum = k.attnum
180
+ WHERE ns.nspname = $1 AND ix.indisprimary = false
181
+ ORDER BY tc.relname, ic.relname, k.n`;
182
+ const ENUMS_SQL = `
183
+ SELECT t.typname AS enum_name, e.enumlabel AS label
184
+ FROM pg_type t
185
+ JOIN pg_enum e ON e.enumtypid = t.oid
186
+ JOIN pg_namespace n ON n.oid = t.typnamespace
187
+ WHERE n.nspname = $1
188
+ ORDER BY t.typname, e.enumsortorder`;
189
+ const SCHEMAS_SQL = `SELECT schema_name FROM information_schema.schemata`;
190
+ /**
191
+ * Assemble raw query rows into an IntrospectedSchema. Exported (separate from
192
+ * the DB connection) so it can be unit-tested with fixture rows.
193
+ */
194
+ function buildSchema(schema, raw) {
195
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
196
+ // Enums
197
+ const enumMap = new Map();
198
+ for (const row of raw.enumRows) {
199
+ const list = (_a = enumMap.get(row.enum_name)) !== null && _a !== void 0 ? _a : [];
200
+ list.push(row.label);
201
+ enumMap.set(row.enum_name, list);
202
+ }
203
+ const enums = [...enumMap.entries()].map(([name, labels]) => ({ name, labels }));
204
+ // Tables / views
205
+ const baseTables = raw.tableRows
206
+ .filter((r) => r.table_type === "BASE TABLE")
207
+ .map((r) => r.table_name);
208
+ const views = raw.tableRows
209
+ .filter((r) => r.table_type === "VIEW")
210
+ .map((r) => r.table_name);
211
+ // Columns grouped by table
212
+ const columnsByTable = new Map();
213
+ for (const row of raw.columnRows) {
214
+ const isEnum = row.data_type === "USER-DEFINED" && enumMap.has(row.udt_name);
215
+ const col = {
216
+ name: row.column_name,
217
+ udtName: row.udt_name,
218
+ dataType: row.data_type,
219
+ nullable: row.is_nullable === "YES",
220
+ default: (_b = row.column_default) !== null && _b !== void 0 ? _b : null,
221
+ charMaxLength: (_c = row.character_maximum_length) !== null && _c !== void 0 ? _c : null,
222
+ numericPrecision: (_d = row.numeric_precision) !== null && _d !== void 0 ? _d : null,
223
+ numericScale: (_e = row.numeric_scale) !== null && _e !== void 0 ? _e : null,
224
+ ordinal: row.ordinal_position,
225
+ isEnum,
226
+ enumTypeName: isEnum ? row.udt_name : undefined,
227
+ };
228
+ const list = (_f = columnsByTable.get(row.table_name)) !== null && _f !== void 0 ? _f : [];
229
+ list.push(col);
230
+ columnsByTable.set(row.table_name, list);
231
+ }
232
+ // Primary keys
233
+ const pkByTable = new Map();
234
+ for (const row of raw.pkRows) {
235
+ const list = (_g = pkByTable.get(row.table_name)) !== null && _g !== void 0 ? _g : [];
236
+ list.push(row.column_name);
237
+ pkByTable.set(row.table_name, list);
238
+ }
239
+ // Foreign keys (grouped by constraint name to handle composite keys)
240
+ const fkByConstraint = new Map();
241
+ for (const row of raw.fkRows) {
242
+ const existing = fkByConstraint.get(row.conname);
243
+ if (existing) {
244
+ existing.columns.push(row.column_name);
245
+ existing.refColumns.push(row.referenced_column);
246
+ }
247
+ else {
248
+ fkByConstraint.set(row.conname, {
249
+ table: row.table_name,
250
+ columns: [row.column_name],
251
+ refTable: row.referenced_table,
252
+ refColumns: [row.referenced_column],
253
+ confdeltype: row.confdeltype,
254
+ });
255
+ }
256
+ }
257
+ const fksByTable = new Map();
258
+ for (const fk of fkByConstraint.values()) {
259
+ const list = (_h = fksByTable.get(fk.table)) !== null && _h !== void 0 ? _h : [];
260
+ list.push({
261
+ columns: fk.columns,
262
+ referencedTable: fk.refTable,
263
+ referencedColumns: fk.refColumns,
264
+ onDelete: (_j = CONFDELTYPE_TO_ACTION[fk.confdeltype]) !== null && _j !== void 0 ? _j : "NO ACTION",
265
+ });
266
+ fksByTable.set(fk.table, list);
267
+ }
268
+ // Unique constraints (grouped by constraint name)
269
+ const uniqueByConstraint = new Map();
270
+ for (const row of raw.uniqueRows) {
271
+ const existing = uniqueByConstraint.get(row.conname);
272
+ if (existing)
273
+ existing.columns.push(row.column_name);
274
+ else
275
+ uniqueByConstraint.set(row.conname, {
276
+ table: row.table_name,
277
+ columns: [row.column_name],
278
+ });
279
+ }
280
+ const uniquesByTable = new Map();
281
+ for (const [name, uc] of uniqueByConstraint.entries()) {
282
+ const list = (_k = uniquesByTable.get(uc.table)) !== null && _k !== void 0 ? _k : [];
283
+ list.push({ name, columns: uc.columns });
284
+ uniquesByTable.set(uc.table, list);
285
+ }
286
+ // Indexes (grouped by index name)
287
+ const indexByName = new Map();
288
+ for (const row of raw.indexRows) {
289
+ const existing = indexByName.get(row.index_name);
290
+ if (existing)
291
+ existing.columns.push(row.column_name);
292
+ else
293
+ indexByName.set(row.index_name, {
294
+ table: row.table_name,
295
+ columns: [row.column_name],
296
+ unique: row.indisunique,
297
+ });
298
+ }
299
+ const indexesByTable = new Map();
300
+ for (const [name, idx] of indexByName.entries()) {
301
+ const list = (_l = indexesByTable.get(idx.table)) !== null && _l !== void 0 ? _l : [];
302
+ list.push({ name, columns: idx.columns, unique: idx.unique });
303
+ indexesByTable.set(idx.table, list);
304
+ }
305
+ const tables = baseTables.map((name) => {
306
+ var _a, _b, _c, _d, _e;
307
+ return ({
308
+ name,
309
+ columns: (_a = columnsByTable.get(name)) !== null && _a !== void 0 ? _a : [],
310
+ primaryKey: (_b = pkByTable.get(name)) !== null && _b !== void 0 ? _b : [],
311
+ foreignKeys: (_c = fksByTable.get(name)) !== null && _c !== void 0 ? _c : [],
312
+ uniqueConstraints: (_d = uniquesByTable.get(name)) !== null && _d !== void 0 ? _d : [],
313
+ indexes: (_e = indexesByTable.get(name)) !== null && _e !== void 0 ? _e : [],
314
+ });
315
+ });
316
+ const systemSchemas = raw.schemaRows
317
+ .map((r) => r.schema_name)
318
+ .filter((s) => exports.SYSTEM_SCHEMAS.has(s) && s !== schema)
319
+ .sort();
320
+ return {
321
+ schema,
322
+ tables,
323
+ enums,
324
+ skipped: { views, systemSchemas },
325
+ };
326
+ }
327
+ exports.buildSchema = buildSchema;