supatool 0.4.3 → 0.6.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/README.md +86 -22
- package/dist/bin/helptext.js +40 -31
- package/dist/bin/supatool.js +74 -149
- package/dist/sync/config.js +72 -13
- package/dist/sync/definitionExtractor.js +14 -1
- package/dist/sync/fetchRemoteSchemas.js +4 -2
- package/dist/sync/generateMigration.js +129 -28
- package/dist/sync/migrateRemote.js +114 -0
- package/dist/sync/seedGenerator.js +15 -4
- package/dist/sync/sync.js +272 -2
- package/package.json +7 -6
- package/dist/integrations/supabase/crud-autogen/tasks.js +0 -220
- package/dist/integrations/supabase/crud-autogen/workflows.js +0 -220
package/dist/sync/config.js
CHANGED
|
@@ -72,27 +72,86 @@ function loadConfig(configPath) {
|
|
|
72
72
|
*/
|
|
73
73
|
function resolveConfig(options, configPath) {
|
|
74
74
|
const fileConfig = loadConfig(configPath);
|
|
75
|
+
const connectionString = options.connectionString ||
|
|
76
|
+
process.env.DB_CONNECTION_STRING ||
|
|
77
|
+
process.env.SUPABASE_CONNECTION_STRING ||
|
|
78
|
+
process.env.DATABASE_URL ||
|
|
79
|
+
fileConfig.connectionString;
|
|
75
80
|
return {
|
|
76
|
-
connectionString
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
schemaDir: options.schemaDir || fileConfig.schemaDir || './supabase/schemas',
|
|
81
|
-
tablePattern: options.tablePattern || fileConfig.tablePattern || '*'
|
|
81
|
+
connectionString,
|
|
82
|
+
schemaDir: options.schemaDir || fileConfig.schemaDir || './db/schemas',
|
|
83
|
+
tablePattern: options.tablePattern || fileConfig.tablePattern || '*',
|
|
84
|
+
migration: fileConfig.migration
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
|
-
* Generate config file template
|
|
88
|
+
* Generate config file template (no connection string — use .env.local)
|
|
86
89
|
*/
|
|
87
90
|
function createConfigTemplate(outputPath) {
|
|
88
91
|
const template = {
|
|
89
|
-
|
|
90
|
-
schemaDir: "./supabase/schemas",
|
|
92
|
+
schemaDir: "./db/schemas",
|
|
91
93
|
tablePattern: "*",
|
|
92
|
-
|
|
94
|
+
migration: {
|
|
95
|
+
naming: "timestamp",
|
|
96
|
+
"_naming_comment": "Use 'sequential' for NNN_description.sql format, 'timestamp' for YYYYMMDDHHMMSS_description.sql",
|
|
97
|
+
dir: "db/migrations"
|
|
98
|
+
},
|
|
99
|
+
"_comment": "Set credentials in .env.local — never put secrets in this file."
|
|
93
100
|
};
|
|
94
101
|
fs.writeFileSync(outputPath, JSON.stringify(template, null, 2), 'utf-8');
|
|
95
|
-
console.log(`
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
console.log(`Config template generated: ${outputPath}`);
|
|
103
|
+
ensureEnvLocalTemplate();
|
|
104
|
+
checkGitignore(outputPath);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Write .env.local template if it doesn't exist yet.
|
|
108
|
+
*/
|
|
109
|
+
function ensureEnvLocalTemplate() {
|
|
110
|
+
const envLocalPath = path.join(process.cwd(), '.env.local');
|
|
111
|
+
if (fs.existsSync(envLocalPath))
|
|
112
|
+
return;
|
|
113
|
+
const template = [
|
|
114
|
+
'# supatool credentials — never commit this file',
|
|
115
|
+
'# PostgreSQL connection string (Cloud SQL, Supabase, or any PostgreSQL)',
|
|
116
|
+
'DB_CONNECTION_STRING=postgresql://user:password@host:port/database',
|
|
117
|
+
'',
|
|
118
|
+
'# Legacy aliases (still accepted for backward compatibility)',
|
|
119
|
+
'# SUPABASE_CONNECTION_STRING=postgresql://user:password@host:port/database',
|
|
120
|
+
'# DATABASE_URL=postgresql://user:password@host:port/database',
|
|
121
|
+
].join('\n') + '\n';
|
|
122
|
+
fs.writeFileSync(envLocalPath, template, 'utf-8');
|
|
123
|
+
console.log('.env.local template created — fill in your credentials.');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Warn if the config file or .env.local are not covered by .gitignore.
|
|
127
|
+
*/
|
|
128
|
+
function checkGitignore(configPath) {
|
|
129
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
130
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
131
|
+
console.warn('Warning: .gitignore not found. Make sure to exclude .env.local and supatool.config.json.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
135
|
+
const lines = content.split('\n').map(l => l.trim());
|
|
136
|
+
const missing = [];
|
|
137
|
+
const configFile = path.basename(configPath);
|
|
138
|
+
if (!lines.some(l => l === configFile || l === `/${configFile}`)) {
|
|
139
|
+
missing.push(configFile);
|
|
140
|
+
}
|
|
141
|
+
if (!lines.some(l => l === '.env.local' || l === '*.local')) {
|
|
142
|
+
missing.push('.env.local');
|
|
143
|
+
}
|
|
144
|
+
if (missing.length > 0) {
|
|
145
|
+
console.warn(`\nWarning: The following are NOT in .gitignore — add them to avoid committing secrets:`);
|
|
146
|
+
for (const f of missing) {
|
|
147
|
+
console.warn(` ${f}`);
|
|
148
|
+
}
|
|
149
|
+
// Auto-append to .gitignore
|
|
150
|
+
const toAdd = missing.map(f => f).join('\n') + '\n';
|
|
151
|
+
fs.appendFileSync(gitignorePath, '\n# supatool\n' + toAdd);
|
|
152
|
+
console.log(`Auto-added to .gitignore: ${missing.join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
console.log('.gitignore OK — credentials files are excluded.');
|
|
156
|
+
}
|
|
98
157
|
}
|
|
@@ -1344,7 +1344,9 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
|
|
|
1344
1344
|
* Classify and output definitions
|
|
1345
1345
|
*/
|
|
1346
1346
|
async function extractDefinitions(options) {
|
|
1347
|
-
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
|
|
1347
|
+
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas: schemasOption = ['public'], excludeSchemas = [], allSchemas: useAllSchemas = false, version } = options;
|
|
1348
|
+
// schemas will be resolved after DB connect when useAllSchemas is true
|
|
1349
|
+
let schemas = schemasOption;
|
|
1348
1350
|
// Disable Node.js SSL certificate verification
|
|
1349
1351
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1350
1352
|
// Connection string validation
|
|
@@ -1463,6 +1465,17 @@ async function extractDefinitions(options) {
|
|
|
1463
1465
|
console.log(` Connection string length: ${encodedConnectionString.length}`);
|
|
1464
1466
|
await client.connect();
|
|
1465
1467
|
spinner.text = 'Connected to database';
|
|
1468
|
+
// Resolve schemas: when --all-schemas, fetch all from DB and subtract excludeSchemas
|
|
1469
|
+
if (useAllSchemas) {
|
|
1470
|
+
const SYSTEM_SCHEMAS = ['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1'];
|
|
1471
|
+
const discovered = await fetchAllSchemas(client);
|
|
1472
|
+
schemas = discovered.filter(s => !SYSTEM_SCHEMAS.includes(s) && !excludeSchemas.includes(s));
|
|
1473
|
+
console.log(`Schemas (all minus excluded): ${schemas.join(', ')}`);
|
|
1474
|
+
}
|
|
1475
|
+
else if (excludeSchemas.length > 0) {
|
|
1476
|
+
schemas = schemas.filter(s => !excludeSchemas.includes(s));
|
|
1477
|
+
console.log(`Schemas (filtered): ${schemas.join(', ')}`);
|
|
1478
|
+
}
|
|
1466
1479
|
let allDefinitions = [];
|
|
1467
1480
|
// Initialize progress tracker
|
|
1468
1481
|
const progress = {
|
|
@@ -56,8 +56,10 @@ async function fetchRemoteSchemas(connectionString, targetTableNames) {
|
|
|
56
56
|
// console.log('Connecting to database...');
|
|
57
57
|
try {
|
|
58
58
|
// Basic connection string check
|
|
59
|
-
if (!connectionString ||
|
|
60
|
-
|
|
59
|
+
if (!connectionString ||
|
|
60
|
+
(!connectionString.startsWith('postgresql://') &&
|
|
61
|
+
!connectionString.startsWith('postgres://'))) {
|
|
62
|
+
throw new Error('Invalid connection string. Please specify a valid postgresql:// or postgres:// format.');
|
|
61
63
|
}
|
|
62
64
|
// Parse URL and display connection info
|
|
63
65
|
const url = new URL(connectionString);
|
|
@@ -34,9 +34,36 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.generateMigrationFile = generateMigrationFile;
|
|
37
|
+
exports.generateRenameTableMigrationFile = generateRenameTableMigrationFile;
|
|
38
|
+
exports.generateFunctionMigrationFile = generateFunctionMigrationFile;
|
|
39
|
+
exports.generateRlsMigrationFile = generateRlsMigrationFile;
|
|
37
40
|
const fs = __importStar(require("fs"));
|
|
38
41
|
const path = __importStar(require("path"));
|
|
39
42
|
const diff_1 = require("diff");
|
|
43
|
+
/**
|
|
44
|
+
* Resolve migration filename based on naming config
|
|
45
|
+
*/
|
|
46
|
+
function resolveMigrationFilename(migrationDir, description, naming = 'timestamp') {
|
|
47
|
+
if (naming === 'sequential') {
|
|
48
|
+
// Find max NNN from existing files
|
|
49
|
+
let max = 0;
|
|
50
|
+
if (fs.existsSync(migrationDir)) {
|
|
51
|
+
for (const f of fs.readdirSync(migrationDir)) {
|
|
52
|
+
const m = f.match(/^(\d+)_/);
|
|
53
|
+
if (m)
|
|
54
|
+
max = Math.max(max, parseInt(m[1], 10));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const next = String(max + 1).padStart(3, '0');
|
|
58
|
+
return `${next}_${description}.sql`;
|
|
59
|
+
}
|
|
60
|
+
// Default: timestamp
|
|
61
|
+
const ts = new Date().toISOString()
|
|
62
|
+
.replace(/[-:]/g, '')
|
|
63
|
+
.replace(/\..+/, '')
|
|
64
|
+
.replace('T', '');
|
|
65
|
+
return `${ts}_${description}.sql`;
|
|
66
|
+
}
|
|
40
67
|
/**
|
|
41
68
|
* Generate ALTER TABLE statements from DDL diff
|
|
42
69
|
*/
|
|
@@ -112,8 +139,8 @@ function generateAlterStatements(tableName, fromDdl, toDdl) {
|
|
|
112
139
|
/**
|
|
113
140
|
* Generate migration file
|
|
114
141
|
*/
|
|
115
|
-
async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.') {
|
|
116
|
-
const migrationDir = path.join(projectDir,
|
|
142
|
+
async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.', migrationConfig) {
|
|
143
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
117
144
|
// Create migrations directory
|
|
118
145
|
if (!fs.existsSync(migrationDir)) {
|
|
119
146
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
@@ -121,20 +148,14 @@ async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.'
|
|
|
121
148
|
// Generate ALTER TABLE statements
|
|
122
149
|
const alterStatements = generateAlterStatements(tableName, fromDdl, toDdl);
|
|
123
150
|
if (alterStatements.length === 0) {
|
|
124
|
-
return await generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir);
|
|
151
|
+
return await generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir, migrationConfig);
|
|
125
152
|
}
|
|
126
|
-
|
|
127
|
-
const now = new Date();
|
|
128
|
-
const timestamp = now.toISOString()
|
|
129
|
-
.replace(/[-:]/g, '')
|
|
130
|
-
.replace(/\..+/, '')
|
|
131
|
-
.replace('T', '');
|
|
132
|
-
const filename = `${timestamp}_update_${tableName}.sql`;
|
|
153
|
+
const filename = resolveMigrationFilename(migrationDir, `update_${tableName}`, migrationConfig?.naming);
|
|
133
154
|
const filepath = path.join(migrationDir, filename);
|
|
134
155
|
// Build migration file content
|
|
135
156
|
const content = `-- Migration generated by supatool
|
|
136
157
|
-- Table: ${tableName}
|
|
137
|
-
-- Generated at: ${
|
|
158
|
+
-- Generated at: ${new Date().toISOString()}
|
|
138
159
|
|
|
139
160
|
${alterStatements.join('\n')}
|
|
140
161
|
`;
|
|
@@ -187,20 +208,14 @@ function analyzeDiffForTemplate(fromDdl, toDdl) {
|
|
|
187
208
|
/**
|
|
188
209
|
* Generate manual migration template
|
|
189
210
|
*/
|
|
190
|
-
async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir) {
|
|
191
|
-
const migrationDir = path.join(projectDir,
|
|
211
|
+
async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir, migrationConfig) {
|
|
212
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
192
213
|
if (!fs.existsSync(migrationDir)) {
|
|
193
214
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
194
215
|
}
|
|
195
216
|
// Analyze diff
|
|
196
217
|
const { removedColumns, addedColumns } = analyzeDiffForTemplate(fromDdl, toDdl);
|
|
197
|
-
|
|
198
|
-
const now = new Date();
|
|
199
|
-
const timestamp = now.toISOString()
|
|
200
|
-
.replace(/[-:]/g, '')
|
|
201
|
-
.replace(/\..+/, '')
|
|
202
|
-
.replace('T', '');
|
|
203
|
-
const filename = `${timestamp}_manual_update_${tableName}.sql`;
|
|
218
|
+
const filename = resolveMigrationFilename(migrationDir, `manual_update_${tableName}`, migrationConfig?.naming);
|
|
204
219
|
const filepath = path.join(migrationDir, filename);
|
|
205
220
|
// Build template from actual changes
|
|
206
221
|
let migrationStatements = [];
|
|
@@ -263,14 +278,100 @@ ${migrationStatements.join('\n')}
|
|
|
263
278
|
return filepath;
|
|
264
279
|
}
|
|
265
280
|
/**
|
|
266
|
-
*
|
|
281
|
+
* Generate a RENAME TABLE migration when a table appears renamed.
|
|
282
|
+
* (old exists on remote, new exists on local, columns are highly similar)
|
|
283
|
+
*/
|
|
284
|
+
async function generateRenameTableMigrationFile(schema, oldName, newName, projectDir = '.', migrationConfig) {
|
|
285
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
286
|
+
if (!fs.existsSync(migrationDir)) {
|
|
287
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
const filename = resolveMigrationFilename(migrationDir, `rename_${schema}_${oldName}_to_${newName}`, migrationConfig?.naming);
|
|
290
|
+
const filepath = path.join(migrationDir, filename);
|
|
291
|
+
const content = `-- Migration generated by supatool
|
|
292
|
+
-- Rename table: ${schema}.${oldName} → ${schema}.${newName}
|
|
293
|
+
-- Generated at: ${new Date().toISOString()}
|
|
294
|
+
-- WARNING: Review carefully before applying. supatool detected this as a rename
|
|
295
|
+
-- based on column similarity, but it may be an unrelated add/drop pair.
|
|
296
|
+
|
|
297
|
+
ALTER TABLE ${schema === 'public' ? '' : schema + '.'}${oldName} RENAME TO ${newName};
|
|
298
|
+
`;
|
|
299
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
|
300
|
+
console.log(`Rename migration generated: ${filename}`);
|
|
301
|
+
return filepath;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Normalize function DDL for comparison (strip supatool header comment, trailing whitespace)
|
|
267
305
|
*/
|
|
268
|
-
function
|
|
306
|
+
function normalizeFunctionDdl(ddl) {
|
|
307
|
+
return ddl
|
|
308
|
+
.split('\n')
|
|
309
|
+
.filter(line => !line.startsWith('-- Generated by supatool'))
|
|
310
|
+
.join('\n')
|
|
311
|
+
.replace(/\s+$/, '')
|
|
312
|
+
.trim();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Generate a migration file for a changed function.
|
|
316
|
+
* localDdl is used as-is (must be valid CREATE OR REPLACE FUNCTION DDL).
|
|
317
|
+
*/
|
|
318
|
+
async function generateFunctionMigrationFile(schema, funcName, localDdl, remoteDdl, projectDir = '.', migrationConfig) {
|
|
319
|
+
const normalizedLocal = normalizeFunctionDdl(localDdl);
|
|
320
|
+
const normalizedRemote = normalizeFunctionDdl(remoteDdl);
|
|
321
|
+
if (normalizedLocal === normalizedRemote)
|
|
322
|
+
return null;
|
|
323
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
324
|
+
if (!fs.existsSync(migrationDir)) {
|
|
325
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
const filename = resolveMigrationFilename(migrationDir, `update_fn_${schema}_${funcName}`, migrationConfig?.naming);
|
|
328
|
+
const filepath = path.join(migrationDir, filename);
|
|
329
|
+
const content = `-- Migration generated by supatool
|
|
330
|
+
-- Function: ${schema}.${funcName}
|
|
331
|
+
-- Generated at: ${new Date().toISOString()}
|
|
332
|
+
|
|
333
|
+
${normalizedLocal.endsWith(';') ? normalizedLocal : normalizedLocal + ';'}
|
|
334
|
+
`;
|
|
335
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
|
336
|
+
console.log(`Function migration generated: ${filename}`);
|
|
337
|
+
return filepath;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Generate DROP POLICY IF EXISTS + CREATE POLICY statements for changed policies.
|
|
341
|
+
*/
|
|
342
|
+
async function generateRlsMigrationFile(changedPolicies, droppedPolicies, projectDir = '.', migrationConfig) {
|
|
343
|
+
if (changedPolicies.length === 0 && droppedPolicies.length === 0)
|
|
344
|
+
return null;
|
|
345
|
+
const migrationDir = path.join(projectDir, migrationConfig?.dir ?? 'db/migrations');
|
|
346
|
+
if (!fs.existsSync(migrationDir)) {
|
|
347
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
|
348
|
+
}
|
|
269
349
|
const statements = [];
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
350
|
+
for (const p of droppedPolicies) {
|
|
351
|
+
statements.push(`DROP POLICY IF EXISTS "${p.policyName}" ON ${p.tableName};`);
|
|
352
|
+
}
|
|
353
|
+
for (const p of changedPolicies) {
|
|
354
|
+
statements.push(`DROP POLICY IF EXISTS "${p.policyName}" ON ${p.tableName};`);
|
|
355
|
+
const permissive = p.permissive ? 'PERMISSIVE' : 'RESTRICTIVE';
|
|
356
|
+
const roles = p.roles || 'public';
|
|
357
|
+
let sql = `CREATE POLICY "${p.policyName}" ON ${p.tableName}\n` +
|
|
358
|
+
` AS ${permissive} FOR ${p.cmd} TO ${roles}`;
|
|
359
|
+
if (p.qual)
|
|
360
|
+
sql += `\n USING (${p.qual})`;
|
|
361
|
+
if (p.withCheck)
|
|
362
|
+
sql += `\n WITH CHECK (${p.withCheck})`;
|
|
363
|
+
sql += ';';
|
|
364
|
+
statements.push(sql);
|
|
365
|
+
}
|
|
366
|
+
const filename = resolveMigrationFilename(migrationDir, 'update_rls', migrationConfig?.naming);
|
|
367
|
+
const filepath = path.join(migrationDir, filename);
|
|
368
|
+
const content = `-- Migration generated by supatool
|
|
369
|
+
-- RLS policies: ${changedPolicies.length} changed, ${droppedPolicies.length} dropped
|
|
370
|
+
-- Generated at: ${new Date().toISOString()}
|
|
371
|
+
|
|
372
|
+
${statements.join('\n\n')}
|
|
373
|
+
`;
|
|
374
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
|
375
|
+
console.log(`RLS migration generated: ${filename}`);
|
|
376
|
+
return filepath;
|
|
276
377
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.migrateRemote = migrateRemote;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const pg_1 = require("pg");
|
|
40
|
+
const MIGRATIONS_TABLE = '_supatool_migrations';
|
|
41
|
+
/**
|
|
42
|
+
* Apply pending SQL migration files to remote DB.
|
|
43
|
+
*
|
|
44
|
+
* Tracks applied migrations in _supatool_migrations table.
|
|
45
|
+
* Files are applied in alphabetical order (timestamp or sequential naming).
|
|
46
|
+
*/
|
|
47
|
+
async function migrateRemote(options) {
|
|
48
|
+
const { connectionString, migrationsDir, dryRun = false } = options;
|
|
49
|
+
// Collect .sql files
|
|
50
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
51
|
+
console.error(`❌ Migrations directory not found: ${migrationsDir}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const allFiles = fs.readdirSync(migrationsDir)
|
|
55
|
+
.filter(f => f.endsWith('.sql'))
|
|
56
|
+
.sort();
|
|
57
|
+
if (allFiles.length === 0) {
|
|
58
|
+
console.log('No migration files found.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const client = new pg_1.Client({
|
|
62
|
+
connectionString,
|
|
63
|
+
ssl: { rejectUnauthorized: false }
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
await client.connect();
|
|
67
|
+
// Ensure tracking table exists
|
|
68
|
+
await client.query(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
70
|
+
id SERIAL PRIMARY KEY,
|
|
71
|
+
filename TEXT NOT NULL UNIQUE,
|
|
72
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
73
|
+
)
|
|
74
|
+
`);
|
|
75
|
+
// Get already-applied migrations
|
|
76
|
+
const applied = await client.query(`SELECT filename FROM ${MIGRATIONS_TABLE} ORDER BY filename`);
|
|
77
|
+
const appliedSet = new Set(applied.rows.map(r => r.filename));
|
|
78
|
+
const pending = allFiles.filter(f => !appliedSet.has(f));
|
|
79
|
+
if (pending.length === 0) {
|
|
80
|
+
console.log('✅ All migrations already applied.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`Pending migrations: ${pending.length}`);
|
|
84
|
+
for (const f of pending) {
|
|
85
|
+
console.log(` • ${f}`);
|
|
86
|
+
}
|
|
87
|
+
if (dryRun) {
|
|
88
|
+
console.log('\n(dry-run) No changes applied.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Apply each pending migration in a transaction
|
|
92
|
+
for (const filename of pending) {
|
|
93
|
+
const filepath = path.join(migrationsDir, filename);
|
|
94
|
+
const sql = fs.readFileSync(filepath, 'utf-8');
|
|
95
|
+
process.stdout.write(`Applying ${filename}... `);
|
|
96
|
+
await client.query('BEGIN');
|
|
97
|
+
try {
|
|
98
|
+
await client.query(sql);
|
|
99
|
+
await client.query(`INSERT INTO ${MIGRATIONS_TABLE} (filename) VALUES ($1)`, [filename]);
|
|
100
|
+
await client.query('COMMIT');
|
|
101
|
+
console.log('✅');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
await client.query('ROLLBACK');
|
|
105
|
+
console.log('❌');
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log(`\n✅ Applied ${pending.length} migration(s).`);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await client.end();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -56,6 +56,7 @@ function parseTablesYaml(yamlPath) {
|
|
|
56
56
|
* @param options SeedGenOptions
|
|
57
57
|
*/
|
|
58
58
|
async function generateSeedsFromRemote(options) {
|
|
59
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
59
60
|
const tables = parseTablesYaml(options.tablesYamlPath);
|
|
60
61
|
// Generate datetime subdir name (e.g. 20250705_1116_supatool)
|
|
61
62
|
const now = new Date();
|
|
@@ -67,7 +68,10 @@ async function generateSeedsFromRemote(options) {
|
|
|
67
68
|
const folderName = `${y}${m}${d}_${hh}${mm}_supatool`;
|
|
68
69
|
const outDir = path_1.default.join(options.outputDir, folderName);
|
|
69
70
|
// DB connection
|
|
70
|
-
const client = new pg_1.Client({
|
|
71
|
+
const client = new pg_1.Client({
|
|
72
|
+
connectionString: options.connectionString,
|
|
73
|
+
ssl: { rejectUnauthorized: false }
|
|
74
|
+
});
|
|
71
75
|
await client.connect();
|
|
72
76
|
const processedFiles = [];
|
|
73
77
|
for (const { schema, table } of tables) {
|
|
@@ -77,8 +81,15 @@ async function generateSeedsFromRemote(options) {
|
|
|
77
81
|
fs_1.default.mkdirSync(schemaDir, { recursive: true });
|
|
78
82
|
}
|
|
79
83
|
// Fetch data
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
let rows;
|
|
85
|
+
try {
|
|
86
|
+
const res = await client.query(`SELECT * FROM "${schema}"."${table}"`);
|
|
87
|
+
rows = res.rows;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.warn(`⚠️ Skip: ${schema}.${table} — ${err.message}`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
82
93
|
// Output JSON
|
|
83
94
|
const fileName = `${table}_seed.json`;
|
|
84
95
|
const filePath = path_1.default.join(schemaDir, fileName);
|
|
@@ -118,7 +129,7 @@ async function generateSeedsFromRemote(options) {
|
|
|
118
129
|
}
|
|
119
130
|
/** Utility to get table comment */
|
|
120
131
|
async function getTableComment(connectionString, schema, table) {
|
|
121
|
-
const client = new pg_1.Client({ connectionString });
|
|
132
|
+
const client = new pg_1.Client({ connectionString, ssl: { rejectUnauthorized: false } });
|
|
122
133
|
await client.connect();
|
|
123
134
|
try {
|
|
124
135
|
const res = await client.query(`SELECT obj_description(c.oid) as comment FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'`, [table, schema]);
|