@apso/cli 0.30.0 → 0.32.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.
- package/dist/commands/import/data.d.ts +22 -0
- package/dist/commands/import/data.js +158 -0
- package/dist/commands/import/supabase.d.ts +42 -0
- package/dist/commands/import/supabase.js +196 -0
- package/dist/lib/import/copy-data.d.ts +106 -0
- package/dist/lib/import/copy-data.js +266 -0
- package/dist/lib/import/index.d.ts +7 -0
- package/dist/lib/import/index.js +25 -0
- package/dist/lib/import/introspect.d.ts +42 -0
- package/dist/lib/import/introspect.js +327 -0
- package/dist/lib/import/pg-data.d.ts +33 -0
- package/dist/lib/import/pg-data.js +112 -0
- package/dist/lib/import/pg-to-apsorc.d.ts +104 -0
- package/dist/lib/import/pg-to-apsorc.js +399 -0
- package/dist/lib/import/types.d.ts +115 -0
- package/dist/lib/import/types.js +11 -0
- package/npm-shrinkwrap.json +153 -3
- package/oclif.manifest.json +118 -1
- package/package.json +6 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database Import — data copy (phase 2)
|
|
4
|
+
*
|
|
5
|
+
* Plans and executes a row copy from a source Postgres/Supabase database into a
|
|
6
|
+
* target database that was rebuilt from the imported schema (phase 1). The
|
|
7
|
+
* planning logic is pure and fully unit-testable; the actual reads/writes go
|
|
8
|
+
* through the injectable `SourceReader` / `TargetWriter` interfaces so the
|
|
9
|
+
* executor can be tested with fakes (mirroring phase 1's `Introspector`).
|
|
10
|
+
*
|
|
11
|
+
* Mapping rules mirror phase 1 exactly:
|
|
12
|
+
* - target table name = snakeCase(entity name) = snakeCase(source table name)
|
|
13
|
+
* - single-column FK = renamed to the platform's camelCase `<ref>Id`
|
|
14
|
+
* - every other column = copied under its original name
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.targetColumnsFromSchema = exports.executeCopy = exports.buildInsertSql = exports.coerceValue = exports.planCopy = exports.topoSortTables = exports.buildColumnMapping = exports.targetTableName = void 0;
|
|
18
|
+
const casing_1 = require("../utils/casing");
|
|
19
|
+
const relationships_1 = require("../utils/relationships");
|
|
20
|
+
const pg_to_apsorc_1 = require("./pg-to-apsorc");
|
|
21
|
+
const INTEGER_PK_UDTS = new Set(["int2", "int4", "int8"]);
|
|
22
|
+
/** The target table name the generator produces for a given source table. */
|
|
23
|
+
function targetTableName(sourceTable) {
|
|
24
|
+
return (0, casing_1.snakeCase)(sourceTable);
|
|
25
|
+
}
|
|
26
|
+
exports.targetTableName = targetTableName;
|
|
27
|
+
/** Map every source column to its target column name (FK rename or identity). */
|
|
28
|
+
function buildColumnMapping(table) {
|
|
29
|
+
const fkRefByColumn = new Map();
|
|
30
|
+
for (const fk of table.foreignKeys) {
|
|
31
|
+
if (fk.columns.length === 1) {
|
|
32
|
+
fkRefByColumn.set(fk.columns[0], fk.referencedTable);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...table.columns]
|
|
36
|
+
.sort((a, b) => a.ordinal - b.ordinal)
|
|
37
|
+
.map((col) => {
|
|
38
|
+
const refTable = fkRefByColumn.get(col.name);
|
|
39
|
+
if (refTable) {
|
|
40
|
+
const { toName } = (0, pg_to_apsorc_1.deriveToName)(col.name, refTable);
|
|
41
|
+
const targetColumn = (0, relationships_1.getRelationshipIdField)({
|
|
42
|
+
name: refTable,
|
|
43
|
+
type: "ManyToOne",
|
|
44
|
+
referenceName: toName,
|
|
45
|
+
});
|
|
46
|
+
return { sourceColumn: col.name, targetColumn };
|
|
47
|
+
}
|
|
48
|
+
return { sourceColumn: col.name, targetColumn: col.name };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
exports.buildColumnMapping = buildColumnMapping;
|
|
52
|
+
/**
|
|
53
|
+
* Order tables so each is copied after the tables it references via FK
|
|
54
|
+
* (parents before children). Self-references are ignored; tables in a cycle are
|
|
55
|
+
* reported and appended best-effort at the end.
|
|
56
|
+
*/
|
|
57
|
+
function topoSortTables(tables) {
|
|
58
|
+
const names = new Set(tables.map((t) => t.name));
|
|
59
|
+
const deps = new Map();
|
|
60
|
+
for (const t of tables) {
|
|
61
|
+
const d = new Set();
|
|
62
|
+
for (const fk of t.foreignKeys) {
|
|
63
|
+
if (fk.referencedTable !== t.name && names.has(fk.referencedTable)) {
|
|
64
|
+
d.add(fk.referencedTable);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
deps.set(t.name, d);
|
|
68
|
+
}
|
|
69
|
+
const order = [];
|
|
70
|
+
const visited = new Set();
|
|
71
|
+
let progress = true;
|
|
72
|
+
while (order.length < tables.length && progress) {
|
|
73
|
+
progress = false;
|
|
74
|
+
for (const t of tables) {
|
|
75
|
+
if (visited.has(t.name))
|
|
76
|
+
continue;
|
|
77
|
+
if ([...deps.get(t.name)].every((x) => visited.has(x))) {
|
|
78
|
+
order.push(t.name);
|
|
79
|
+
visited.add(t.name);
|
|
80
|
+
progress = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const cyclic = tables
|
|
85
|
+
.filter((t) => !visited.has(t.name))
|
|
86
|
+
.map((t) => t.name);
|
|
87
|
+
for (const c of cyclic)
|
|
88
|
+
order.push(c); // best-effort append
|
|
89
|
+
return { order, cyclic };
|
|
90
|
+
}
|
|
91
|
+
exports.topoSortTables = topoSortTables;
|
|
92
|
+
/**
|
|
93
|
+
* Build a full copy plan from the source schema and the set of columns that
|
|
94
|
+
* actually exist in each target table. Source columns whose mapped target
|
|
95
|
+
* column is missing are dropped (and reported); source tables with no target
|
|
96
|
+
* table are skipped entirely.
|
|
97
|
+
*/
|
|
98
|
+
function planCopy(source, targetColumnsByTable, tableFilter) {
|
|
99
|
+
var _a;
|
|
100
|
+
const filter = tableFilter && tableFilter.length > 0 ? new Set(tableFilter) : null;
|
|
101
|
+
const consideredTables = source.tables.filter((t) => !filter || filter.has(t.name));
|
|
102
|
+
const { order, cyclic } = topoSortTables(consideredTables);
|
|
103
|
+
const byName = new Map(consideredTables.map((t) => [t.name, t]));
|
|
104
|
+
const tables = [];
|
|
105
|
+
const skippedColumns = [];
|
|
106
|
+
const missingTargetTables = [];
|
|
107
|
+
for (const sourceTable of order) {
|
|
108
|
+
const table = byName.get(sourceTable);
|
|
109
|
+
const targetTable = targetTableName(sourceTable);
|
|
110
|
+
const targetColumns = targetColumnsByTable.get(targetTable);
|
|
111
|
+
if (!targetColumns) {
|
|
112
|
+
missingTargetTables.push(sourceTable);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const mapping = buildColumnMapping(table);
|
|
116
|
+
const keptSource = [];
|
|
117
|
+
const keptTarget = [];
|
|
118
|
+
for (const m of mapping) {
|
|
119
|
+
if (targetColumns.has(m.targetColumn)) {
|
|
120
|
+
keptSource.push(m.sourceColumn);
|
|
121
|
+
keptTarget.push(m.targetColumn);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
skippedColumns.push({
|
|
125
|
+
table: sourceTable,
|
|
126
|
+
column: m.sourceColumn,
|
|
127
|
+
reason: `target column "${m.targetColumn}" not found in ${targetTable}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Determine ordering + serial PK handling from a single-column PK.
|
|
132
|
+
let orderBy = null;
|
|
133
|
+
let serialPkColumn = null;
|
|
134
|
+
if (table.primaryKey.length === 1) {
|
|
135
|
+
const pkCol = table.primaryKey[0];
|
|
136
|
+
orderBy = pkCol;
|
|
137
|
+
const pk = table.columns.find((c) => c.name === pkCol);
|
|
138
|
+
const targetPk = (_a = mapping.find((m) => m.sourceColumn === pkCol)) === null || _a === void 0 ? void 0 : _a.targetColumn;
|
|
139
|
+
if (pk && targetPk && INTEGER_PK_UDTS.has(pk.udtName)) {
|
|
140
|
+
serialPkColumn = targetPk;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
tables.push({
|
|
144
|
+
sourceTable,
|
|
145
|
+
targetTable,
|
|
146
|
+
sourceColumns: keptSource,
|
|
147
|
+
targetColumns: keptTarget,
|
|
148
|
+
orderBy,
|
|
149
|
+
serialPkColumn,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return { tables, skippedColumns, missingTargetTables, cyclicTables: cyclic };
|
|
153
|
+
}
|
|
154
|
+
exports.planCopy = planCopy;
|
|
155
|
+
/** Coerce a value read from pg into something safe to bind on insert. */
|
|
156
|
+
function coerceValue(value) {
|
|
157
|
+
if (value === null || value === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
if (value instanceof Date)
|
|
160
|
+
return value;
|
|
161
|
+
if (Buffer.isBuffer(value))
|
|
162
|
+
return value;
|
|
163
|
+
if (typeof value === "object")
|
|
164
|
+
return JSON.stringify(value);
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
exports.coerceValue = coerceValue;
|
|
168
|
+
/** Build a parameterized multi-row INSERT statement for a batch. */
|
|
169
|
+
function buildInsertSql(table, columns, rows, schema) {
|
|
170
|
+
const tableRef = schema ? `"${schema}"."${table}"` : `"${table}"`;
|
|
171
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
172
|
+
const values = [];
|
|
173
|
+
const tuples = rows.map((row) => {
|
|
174
|
+
const placeholders = row.map((val) => {
|
|
175
|
+
values.push(coerceValue(val));
|
|
176
|
+
return `$${values.length}`;
|
|
177
|
+
});
|
|
178
|
+
return `(${placeholders.join(", ")})`;
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
text: `INSERT INTO ${tableRef} (${colList}) VALUES ${tuples.join(", ")}`,
|
|
182
|
+
values,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
exports.buildInsertSql = buildInsertSql;
|
|
186
|
+
/**
|
|
187
|
+
* Execute a copy plan. Insert-only: if any target table already has rows the
|
|
188
|
+
* run aborts before writing anything. The whole copy runs in one transaction
|
|
189
|
+
* on the target, so a failure rolls everything back.
|
|
190
|
+
*/
|
|
191
|
+
async function executeCopy(plan, source, target, options = {}) {
|
|
192
|
+
var _a, _b;
|
|
193
|
+
const batchSize = (_a = options.batchSize) !== null && _a !== void 0 ? _a : 500;
|
|
194
|
+
const dryRun = (_b = options.dryRun) !== null && _b !== void 0 ? _b : false;
|
|
195
|
+
// Pre-flight: detect non-empty target tables.
|
|
196
|
+
const blockedTables = [];
|
|
197
|
+
for (const t of plan.tables) {
|
|
198
|
+
// eslint-disable-next-line no-await-in-loop
|
|
199
|
+
const count = await target.rowCount(t.targetTable);
|
|
200
|
+
if (count > 0)
|
|
201
|
+
blockedTables.push(t.targetTable);
|
|
202
|
+
}
|
|
203
|
+
const perTable = [];
|
|
204
|
+
if (dryRun) {
|
|
205
|
+
for (const t of plan.tables) {
|
|
206
|
+
// eslint-disable-next-line no-await-in-loop
|
|
207
|
+
const rows = await source.totalRows(t.sourceTable);
|
|
208
|
+
perTable.push({ table: t.targetTable, rows });
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
dryRun: true,
|
|
212
|
+
perTable,
|
|
213
|
+
skippedColumns: plan.skippedColumns,
|
|
214
|
+
missingTargetTables: plan.missingTargetTables,
|
|
215
|
+
cyclicTables: plan.cyclicTables,
|
|
216
|
+
blockedTables,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (blockedTables.length > 0) {
|
|
220
|
+
throw new Error(`Aborting: target table(s) already contain rows: ${blockedTables.join(", ")}. Use a fresh target or clear these tables first.`);
|
|
221
|
+
}
|
|
222
|
+
await target.begin();
|
|
223
|
+
try {
|
|
224
|
+
for (const t of plan.tables) {
|
|
225
|
+
let offset = 0;
|
|
226
|
+
let copied = 0;
|
|
227
|
+
// eslint-disable-next-line no-constant-condition
|
|
228
|
+
while (true) {
|
|
229
|
+
// eslint-disable-next-line no-await-in-loop
|
|
230
|
+
const rows = await source.readBatch(t.sourceTable, t.sourceColumns, t.orderBy, offset, batchSize);
|
|
231
|
+
if (rows.length === 0)
|
|
232
|
+
break;
|
|
233
|
+
// eslint-disable-next-line no-await-in-loop
|
|
234
|
+
await target.insertBatch(t.targetTable, t.targetColumns, rows);
|
|
235
|
+
copied += rows.length;
|
|
236
|
+
offset += rows.length;
|
|
237
|
+
if (rows.length < batchSize)
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (t.serialPkColumn && copied > 0) {
|
|
241
|
+
// eslint-disable-next-line no-await-in-loop
|
|
242
|
+
await target.resetSequence(t.targetTable, t.serialPkColumn);
|
|
243
|
+
}
|
|
244
|
+
perTable.push({ table: t.targetTable, rows: copied });
|
|
245
|
+
}
|
|
246
|
+
await target.commit();
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
await target.rollback();
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
dryRun: false,
|
|
254
|
+
perTable,
|
|
255
|
+
skippedColumns: plan.skippedColumns,
|
|
256
|
+
missingTargetTables: plan.missingTargetTables,
|
|
257
|
+
cyclicTables: plan.cyclicTables,
|
|
258
|
+
blockedTables,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
exports.executeCopy = executeCopy;
|
|
262
|
+
/** Build the target column lookup used by planCopy from an introspected target. */
|
|
263
|
+
function targetColumnsFromSchema(target) {
|
|
264
|
+
return new Map(target.tables.map((t) => [t.name, new Set(t.columns.map((c) => c.name))]));
|
|
265
|
+
}
|
|
266
|
+
exports.targetColumnsFromSchema = targetColumnsFromSchema;
|
|
@@ -0,0 +1,7 @@
|
|
|
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";
|
|
5
|
+
export { planCopy, executeCopy, buildColumnMapping, topoSortTables, targetTableName, targetColumnsFromSchema, buildInsertSql, coerceValue, } from "./copy-data";
|
|
6
|
+
export type { CopyPlan, CopyResult, TableCopyPlan, ColumnMapping, SourceReader, TargetWriter, ExecuteOptions, } from "./copy-data";
|
|
7
|
+
export { PgSourceReader, PgTargetWriter } from "./pg-data";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PgTargetWriter = exports.PgSourceReader = exports.coerceValue = exports.buildInsertSql = exports.targetColumnsFromSchema = exports.targetTableName = exports.topoSortTables = exports.buildColumnMapping = exports.executeCopy = exports.planCopy = 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; } });
|
|
14
|
+
var copy_data_1 = require("./copy-data");
|
|
15
|
+
Object.defineProperty(exports, "planCopy", { enumerable: true, get: function () { return copy_data_1.planCopy; } });
|
|
16
|
+
Object.defineProperty(exports, "executeCopy", { enumerable: true, get: function () { return copy_data_1.executeCopy; } });
|
|
17
|
+
Object.defineProperty(exports, "buildColumnMapping", { enumerable: true, get: function () { return copy_data_1.buildColumnMapping; } });
|
|
18
|
+
Object.defineProperty(exports, "topoSortTables", { enumerable: true, get: function () { return copy_data_1.topoSortTables; } });
|
|
19
|
+
Object.defineProperty(exports, "targetTableName", { enumerable: true, get: function () { return copy_data_1.targetTableName; } });
|
|
20
|
+
Object.defineProperty(exports, "targetColumnsFromSchema", { enumerable: true, get: function () { return copy_data_1.targetColumnsFromSchema; } });
|
|
21
|
+
Object.defineProperty(exports, "buildInsertSql", { enumerable: true, get: function () { return copy_data_1.buildInsertSql; } });
|
|
22
|
+
Object.defineProperty(exports, "coerceValue", { enumerable: true, get: function () { return copy_data_1.coerceValue; } });
|
|
23
|
+
var pg_data_1 = require("./pg-data");
|
|
24
|
+
Object.defineProperty(exports, "PgSourceReader", { enumerable: true, get: function () { return pg_data_1.PgSourceReader; } });
|
|
25
|
+
Object.defineProperty(exports, "PgTargetWriter", { enumerable: true, get: function () { return pg_data_1.PgTargetWriter; } });
|
|
@@ -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;
|