supatool 0.3.7 → 0.4.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/README.md +60 -281
- package/dist/bin/helptext.js +4 -3
- package/dist/bin/supatool.js +182 -74
- package/dist/generator/client.js +1 -2
- package/dist/generator/crudGenerator.js +22 -23
- package/dist/generator/docGenerator.js +12 -13
- package/dist/generator/rlsGenerator.js +21 -21
- package/dist/generator/sqlGenerator.js +20 -21
- package/dist/generator/typeGenerator.js +6 -7
- package/dist/index.js +23 -23
- package/dist/parser/modelParser.js +3 -4
- package/dist/sync/config.js +10 -10
- package/dist/sync/definitionExtractor.js +589 -321
- package/dist/sync/fetchRemoteSchemas.js +59 -43
- package/dist/sync/generateMigration.js +42 -42
- package/dist/sync/parseLocalSchemas.js +14 -11
- package/dist/sync/seedGenerator.js +15 -14
- package/dist/sync/sync.js +75 -140
- package/dist/sync/utils.js +19 -7
- package/dist/sync/writeSchema.js +22 -22
- package/package.json +8 -8
|
@@ -37,60 +37,57 @@ exports.extractDefinitions = extractDefinitions;
|
|
|
37
37
|
exports.generateCreateTableDDL = generateCreateTableDDL;
|
|
38
38
|
const pg_1 = require("pg");
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Display progress
|
|
41
41
|
*/
|
|
42
|
-
//
|
|
42
|
+
// Store global progress state
|
|
43
43
|
let globalProgress = null;
|
|
44
44
|
let progressUpdateInterval = null;
|
|
45
45
|
function displayProgress(progress, spinner) {
|
|
46
|
-
//
|
|
46
|
+
// Update global state
|
|
47
47
|
globalProgress = progress;
|
|
48
|
-
//
|
|
48
|
+
// Start periodic update if not already
|
|
49
49
|
if (!progressUpdateInterval) {
|
|
50
50
|
progressUpdateInterval = setInterval(() => {
|
|
51
51
|
if (globalProgress && spinner) {
|
|
52
52
|
updateSpinnerDisplay(globalProgress, spinner);
|
|
53
53
|
}
|
|
54
|
-
}, 80); // 80ms
|
|
54
|
+
}, 80); // Update every 80ms
|
|
55
55
|
}
|
|
56
|
-
//
|
|
56
|
+
// Update display immediately
|
|
57
57
|
updateSpinnerDisplay(progress, spinner);
|
|
58
58
|
}
|
|
59
59
|
function updateSpinnerDisplay(progress, spinner) {
|
|
60
|
-
//
|
|
60
|
+
// Compute overall progress
|
|
61
61
|
const totalObjects = progress.tables.total + progress.views.total + progress.rls.total +
|
|
62
62
|
progress.functions.total + progress.triggers.total + progress.cronJobs.total +
|
|
63
63
|
progress.customTypes.total;
|
|
64
64
|
const completedObjects = progress.tables.current + progress.views.current + progress.rls.current +
|
|
65
65
|
progress.functions.current + progress.triggers.current + progress.cronJobs.current +
|
|
66
66
|
progress.customTypes.current;
|
|
67
|
-
//
|
|
67
|
+
// Build progress bar
|
|
68
68
|
const createProgressBar = (current, total, width = 20) => {
|
|
69
69
|
if (total === 0)
|
|
70
70
|
return '░'.repeat(width);
|
|
71
71
|
const percentage = Math.min(current / total, 1);
|
|
72
72
|
const filled = Math.floor(percentage * width);
|
|
73
73
|
const empty = width - filled;
|
|
74
|
-
// 稲妻で単純に埋める(文字の入れ替えなし)
|
|
75
74
|
return '⚡'.repeat(filled) + '░'.repeat(empty);
|
|
76
75
|
};
|
|
77
|
-
//
|
|
76
|
+
// Show overall bar only (fit to console width)
|
|
78
77
|
const overallBar = createProgressBar(completedObjects, totalObjects, 20);
|
|
79
78
|
const overallPercent = totalObjects > 0 ? Math.floor((completedObjects / totalObjects) * 100) : 0;
|
|
80
|
-
//
|
|
79
|
+
// Green rotating spinner
|
|
81
80
|
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
82
81
|
const spinnerFrame = Math.floor((Date.now() / 80) % spinnerFrames.length);
|
|
83
82
|
const greenSpinner = `\x1b[32m${spinnerFrames[spinnerFrame]}\x1b[0m`;
|
|
84
|
-
//
|
|
83
|
+
// Dot animation
|
|
85
84
|
const dotFrames = ["", ".", "..", "..."];
|
|
86
85
|
const dotFrame = Math.floor((Date.now() / 400) % dotFrames.length);
|
|
87
|
-
// 解析中メッセージ
|
|
88
86
|
const statusMessage = "Extracting";
|
|
89
|
-
// 改行制御付きコンパクト表示(緑色スピナー付き)
|
|
90
87
|
const display = `\r${greenSpinner} [${overallBar}] ${overallPercent}% (${completedObjects}/${totalObjects}) ${statusMessage}${dotFrames[dotFrame]}`;
|
|
91
88
|
spinner.text = display;
|
|
92
89
|
}
|
|
93
|
-
//
|
|
90
|
+
// Stop progress display
|
|
94
91
|
function stopProgressDisplay() {
|
|
95
92
|
if (progressUpdateInterval) {
|
|
96
93
|
clearInterval(progressUpdateInterval);
|
|
@@ -99,7 +96,7 @@ function stopProgressDisplay() {
|
|
|
99
96
|
globalProgress = null;
|
|
100
97
|
}
|
|
101
98
|
/**
|
|
102
|
-
* RLS
|
|
99
|
+
* Fetch RLS policies
|
|
103
100
|
*/
|
|
104
101
|
async function fetchRlsPolicies(client, spinner, progress, schemas = ['public']) {
|
|
105
102
|
const policies = [];
|
|
@@ -128,7 +125,7 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
128
125
|
groupedPolicies[tableKey].push(row);
|
|
129
126
|
}
|
|
130
127
|
const tableKeys = Object.keys(groupedPolicies);
|
|
131
|
-
//
|
|
128
|
+
// Initialize progress
|
|
132
129
|
if (progress) {
|
|
133
130
|
progress.rls.total = tableKeys.length;
|
|
134
131
|
}
|
|
@@ -138,12 +135,12 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
138
135
|
const firstPolicy = tablePolicies[0];
|
|
139
136
|
const schemaName = firstPolicy.schemaname;
|
|
140
137
|
const tableName = firstPolicy.tablename;
|
|
141
|
-
//
|
|
138
|
+
// Update progress
|
|
142
139
|
if (progress && spinner) {
|
|
143
140
|
progress.rls.current = i + 1;
|
|
144
141
|
displayProgress(progress, spinner);
|
|
145
142
|
}
|
|
146
|
-
// RLS
|
|
143
|
+
// Add RLS policy description at top
|
|
147
144
|
let ddl = `-- RLS Policies for ${schemaName}.${tableName}\n`;
|
|
148
145
|
ddl += `-- Row Level Security policies to control data access at the row level\n\n`;
|
|
149
146
|
ddl += `ALTER TABLE ${schemaName}.${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
|
|
@@ -153,16 +150,16 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
153
150
|
ddl += ` AS ${policy.permissive || 'PERMISSIVE'}\n`;
|
|
154
151
|
ddl += ` FOR ${policy.cmd || 'ALL'}\n`;
|
|
155
152
|
if (policy.roles) {
|
|
156
|
-
// roles
|
|
153
|
+
// Handle roles as array or string
|
|
157
154
|
let roles;
|
|
158
155
|
if (Array.isArray(policy.roles)) {
|
|
159
156
|
roles = policy.roles.join(', ');
|
|
160
157
|
}
|
|
161
158
|
else {
|
|
162
|
-
// PostgreSQL
|
|
159
|
+
// Handle PostgreSQL array literal "{role1,role2}" or plain string
|
|
163
160
|
roles = String(policy.roles)
|
|
164
|
-
.replace(/[{}]/g, '') //
|
|
165
|
-
.replace(/"/g, ''); //
|
|
161
|
+
.replace(/[{}]/g, '') // Remove braces
|
|
162
|
+
.replace(/"/g, ''); // Remove double quotes
|
|
166
163
|
}
|
|
167
164
|
if (roles && roles.trim() !== '') {
|
|
168
165
|
ddl += ` TO ${roles}\n`;
|
|
@@ -180,6 +177,7 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
180
177
|
name: `${schemaName}_${tableName}_policies`,
|
|
181
178
|
type: 'rls',
|
|
182
179
|
category: `${schemaName}.${tableName}`,
|
|
180
|
+
schema: schemaName,
|
|
183
181
|
ddl,
|
|
184
182
|
timestamp: Math.floor(Date.now() / 1000)
|
|
185
183
|
});
|
|
@@ -192,7 +190,110 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
/**
|
|
195
|
-
*
|
|
193
|
+
* Fetch RLS enabled flag and policy count for all tables (pg_class.relrowsecurity + pg_policies)
|
|
194
|
+
*/
|
|
195
|
+
async function fetchTableRlsStatus(client, schemas = ['public']) {
|
|
196
|
+
if (schemas.length === 0)
|
|
197
|
+
return [];
|
|
198
|
+
const schemaPlaceholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
|
|
199
|
+
const result = await client.query(`
|
|
200
|
+
SELECT
|
|
201
|
+
n.nspname AS schema_name,
|
|
202
|
+
c.relname AS table_name,
|
|
203
|
+
COALESCE(c.relrowsecurity, false) AS rls_enabled
|
|
204
|
+
FROM pg_class c
|
|
205
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
206
|
+
WHERE c.relkind = 'r'
|
|
207
|
+
AND n.nspname IN (${schemaPlaceholders})
|
|
208
|
+
ORDER BY n.nspname, c.relname
|
|
209
|
+
`, schemas);
|
|
210
|
+
const policyCountMap = new Map();
|
|
211
|
+
const policyResult = await client.query(`
|
|
212
|
+
SELECT schemaname, tablename, COUNT(*) AS cnt
|
|
213
|
+
FROM pg_policies
|
|
214
|
+
WHERE schemaname IN (${schemaPlaceholders})
|
|
215
|
+
GROUP BY schemaname, tablename
|
|
216
|
+
`, schemas);
|
|
217
|
+
for (const row of policyResult.rows) {
|
|
218
|
+
policyCountMap.set(`${row.schemaname}.${row.tablename}`, parseInt(row.cnt, 10));
|
|
219
|
+
}
|
|
220
|
+
return result.rows.map((r) => {
|
|
221
|
+
const key = `${r.schema_name}.${r.table_name}`;
|
|
222
|
+
return {
|
|
223
|
+
schema: r.schema_name,
|
|
224
|
+
table: r.table_name,
|
|
225
|
+
rlsEnabled: !!r.rls_enabled,
|
|
226
|
+
policyCount: policyCountMap.get(key) ?? 0
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Fetch FK relations list (for llms.txt RELATIONS)
|
|
232
|
+
*/
|
|
233
|
+
async function fetchRelationList(client, schemas = ['public']) {
|
|
234
|
+
if (schemas.length === 0)
|
|
235
|
+
return [];
|
|
236
|
+
const schemaPlaceholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
|
|
237
|
+
const result = await client.query(`
|
|
238
|
+
SELECT
|
|
239
|
+
tc.table_schema AS from_schema,
|
|
240
|
+
tc.table_name AS from_table,
|
|
241
|
+
ccu.table_schema AS to_schema,
|
|
242
|
+
ccu.table_name AS to_table
|
|
243
|
+
FROM information_schema.table_constraints tc
|
|
244
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
245
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
246
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
247
|
+
AND tc.table_schema IN (${schemaPlaceholders})
|
|
248
|
+
ORDER BY tc.table_schema, tc.table_name
|
|
249
|
+
`, schemas);
|
|
250
|
+
return result.rows.map((r) => ({
|
|
251
|
+
from: `${r.from_schema}.${r.from_table}`,
|
|
252
|
+
to: `${r.to_schema}.${r.to_table}`
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Fetch all schemas in DB (for llms.txt ALL_SCHEMAS / unrextracted schemas)
|
|
257
|
+
*/
|
|
258
|
+
async function fetchAllSchemas(client) {
|
|
259
|
+
const result = await client.query(`
|
|
260
|
+
SELECT schema_name
|
|
261
|
+
FROM information_schema.schemata
|
|
262
|
+
WHERE schema_name NOT LIKE 'pg_%'
|
|
263
|
+
AND schema_name != 'information_schema'
|
|
264
|
+
AND schema_name != 'pg_catalog'
|
|
265
|
+
ORDER BY schema_name
|
|
266
|
+
`);
|
|
267
|
+
return result.rows.map((r) => r.schema_name);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Extract table refs from function DDL (heuristic, best-effort)
|
|
271
|
+
*/
|
|
272
|
+
function extractTableRefsFromFunctionDdl(ddl, defaultSchema = 'public') {
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
const add = (schema, table) => {
|
|
275
|
+
const key = `${schema}.${table}`;
|
|
276
|
+
if (!seen.has(key))
|
|
277
|
+
seen.add(key);
|
|
278
|
+
};
|
|
279
|
+
// schema.table form
|
|
280
|
+
const schemaTableRe = /(?:FROM|JOIN|INTO|UPDATE|DELETE\s+FROM|INSERT\s+INTO)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)/gi;
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = schemaTableRe.exec(ddl)) !== null) {
|
|
283
|
+
add(m[1], m[2]);
|
|
284
|
+
}
|
|
285
|
+
// Unqualified table name (after FROM / JOIN / INSERT INTO / UPDATE / DELETE FROM)
|
|
286
|
+
const unqualifiedRe = /(?:FROM|JOIN|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s|$|\)|,)/gi;
|
|
287
|
+
while ((m = unqualifiedRe.exec(ddl)) !== null) {
|
|
288
|
+
const name = m[1].toLowerCase();
|
|
289
|
+
if (!['select', 'where', 'on', 'and', 'or', 'inner', 'left', 'right', 'outer', 'cross', 'lateral'].includes(name)) {
|
|
290
|
+
add(defaultSchema, m[1]);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return Array.from(seen).sort();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Fetch functions
|
|
196
297
|
*/
|
|
197
298
|
async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
198
299
|
const functions = [];
|
|
@@ -211,20 +312,20 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
211
312
|
AND p.prokind IN ('f', 'p') -- functions and procedures
|
|
212
313
|
ORDER BY n.nspname, p.proname
|
|
213
314
|
`, schemas);
|
|
214
|
-
//
|
|
315
|
+
// Initialize progress
|
|
215
316
|
if (progress) {
|
|
216
317
|
progress.functions.total = result.rows.length;
|
|
217
318
|
}
|
|
218
319
|
for (let i = 0; i < result.rows.length; i++) {
|
|
219
320
|
const row = result.rows[i];
|
|
220
|
-
//
|
|
321
|
+
// Update progress
|
|
221
322
|
if (progress && spinner) {
|
|
222
323
|
progress.functions.current = i + 1;
|
|
223
324
|
displayProgress(progress, spinner);
|
|
224
325
|
}
|
|
225
|
-
//
|
|
326
|
+
// Build exact function signature (schema name and argument types)
|
|
226
327
|
const functionSignature = `${row.schema_name}.${row.name}(${row.identity_args || ''})`;
|
|
227
|
-
//
|
|
328
|
+
// Add function comment at top
|
|
228
329
|
let ddl = '';
|
|
229
330
|
if (!row.comment) {
|
|
230
331
|
ddl += `-- Function: ${functionSignature}\n`;
|
|
@@ -232,13 +333,13 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
232
333
|
else {
|
|
233
334
|
ddl += `-- ${row.comment}\n`;
|
|
234
335
|
}
|
|
235
|
-
//
|
|
336
|
+
// Add function definition (ensure semicolon)
|
|
236
337
|
let functionDef = row.definition;
|
|
237
338
|
if (!functionDef.trim().endsWith(';')) {
|
|
238
339
|
functionDef += ';';
|
|
239
340
|
}
|
|
240
341
|
ddl += functionDef + '\n\n';
|
|
241
|
-
// COMMENT ON
|
|
342
|
+
// Add COMMENT ON statement
|
|
242
343
|
if (!row.comment) {
|
|
243
344
|
ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
|
|
244
345
|
}
|
|
@@ -248,6 +349,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
248
349
|
functions.push({
|
|
249
350
|
name: row.name,
|
|
250
351
|
type: 'function',
|
|
352
|
+
schema: row.schema_name,
|
|
251
353
|
ddl,
|
|
252
354
|
comment: row.comment,
|
|
253
355
|
timestamp: Math.floor(Date.now() / 1000)
|
|
@@ -256,7 +358,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
256
358
|
return functions;
|
|
257
359
|
}
|
|
258
360
|
/**
|
|
259
|
-
*
|
|
361
|
+
* Fetch triggers
|
|
260
362
|
*/
|
|
261
363
|
async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
262
364
|
const triggers = [];
|
|
@@ -274,18 +376,18 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
274
376
|
AND NOT t.tgisinternal
|
|
275
377
|
ORDER BY n.nspname, c.relname, t.tgname
|
|
276
378
|
`, schemas);
|
|
277
|
-
//
|
|
379
|
+
// Initialize progress
|
|
278
380
|
if (progress) {
|
|
279
381
|
progress.triggers.total = result.rows.length;
|
|
280
382
|
}
|
|
281
383
|
for (let i = 0; i < result.rows.length; i++) {
|
|
282
384
|
const row = result.rows[i];
|
|
283
|
-
//
|
|
385
|
+
// Update progress
|
|
284
386
|
if (progress && spinner) {
|
|
285
387
|
progress.triggers.current = i + 1;
|
|
286
388
|
displayProgress(progress, spinner);
|
|
287
389
|
}
|
|
288
|
-
//
|
|
390
|
+
// Add trigger description at top
|
|
289
391
|
let ddl = `-- Trigger: ${row.trigger_name} on ${row.schema_name}.${row.table_name}\n`;
|
|
290
392
|
ddl += `-- Database trigger that automatically executes in response to certain events\n\n`;
|
|
291
393
|
ddl += row.definition + ';';
|
|
@@ -293,6 +395,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
293
395
|
name: `${row.schema_name}_${row.table_name}_${row.trigger_name}`,
|
|
294
396
|
type: 'trigger',
|
|
295
397
|
category: `${row.schema_name}.${row.table_name}`,
|
|
398
|
+
schema: row.schema_name,
|
|
296
399
|
ddl,
|
|
297
400
|
timestamp: Math.floor(Date.now() / 1000)
|
|
298
401
|
});
|
|
@@ -300,7 +403,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
300
403
|
return triggers;
|
|
301
404
|
}
|
|
302
405
|
/**
|
|
303
|
-
* Cron
|
|
406
|
+
* Fetch Cron jobs (pg_cron extension)
|
|
304
407
|
*/
|
|
305
408
|
async function fetchCronJobs(client, spinner, progress) {
|
|
306
409
|
const cronJobs = [];
|
|
@@ -315,18 +418,18 @@ async function fetchCronJobs(client, spinner, progress) {
|
|
|
315
418
|
FROM cron.job
|
|
316
419
|
ORDER BY jobid
|
|
317
420
|
`);
|
|
318
|
-
//
|
|
421
|
+
// Initialize progress
|
|
319
422
|
if (progress) {
|
|
320
423
|
progress.cronJobs.total = result.rows.length;
|
|
321
424
|
}
|
|
322
425
|
for (let i = 0; i < result.rows.length; i++) {
|
|
323
426
|
const row = result.rows[i];
|
|
324
|
-
//
|
|
427
|
+
// Update progress
|
|
325
428
|
if (progress && spinner) {
|
|
326
429
|
progress.cronJobs.current = i + 1;
|
|
327
430
|
displayProgress(progress, spinner);
|
|
328
431
|
}
|
|
329
|
-
// Cron
|
|
432
|
+
// Add Cron job description at top
|
|
330
433
|
let ddl = `-- Cron Job: ${row.jobname || `job_${row.jobid}`}\n`;
|
|
331
434
|
ddl += `-- Scheduled job that runs automatically at specified intervals\n`;
|
|
332
435
|
ddl += `-- Schedule: ${row.schedule}\n`;
|
|
@@ -341,12 +444,12 @@ async function fetchCronJobs(client, spinner, progress) {
|
|
|
341
444
|
}
|
|
342
445
|
}
|
|
343
446
|
catch (error) {
|
|
344
|
-
// pg_cron
|
|
447
|
+
// Skip when pg_cron extension is not present
|
|
345
448
|
}
|
|
346
449
|
return cronJobs;
|
|
347
450
|
}
|
|
348
451
|
/**
|
|
349
|
-
*
|
|
452
|
+
* Fetch custom types
|
|
350
453
|
*/
|
|
351
454
|
async function fetchCustomTypes(client, spinner, progress, schemas = ['public']) {
|
|
352
455
|
const types = [];
|
|
@@ -368,41 +471,41 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
368
471
|
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
369
472
|
WHERE n.nspname IN (${schemaPlaceholders})
|
|
370
473
|
AND t.typtype IN ('e', 'c', 'd')
|
|
371
|
-
AND t.typisdefined = true --
|
|
372
|
-
AND NOT t.typarray = 0 --
|
|
474
|
+
AND t.typisdefined = true -- defined types only
|
|
475
|
+
AND NOT t.typarray = 0 -- exclude array base types
|
|
373
476
|
AND NOT EXISTS (
|
|
374
|
-
--
|
|
477
|
+
-- exclude same-name as table, view, index, sequence, composite
|
|
375
478
|
SELECT 1 FROM pg_class c
|
|
376
479
|
WHERE c.relname = t.typname
|
|
377
480
|
AND c.relnamespace = n.oid
|
|
378
481
|
)
|
|
379
482
|
AND NOT EXISTS (
|
|
380
|
-
--
|
|
483
|
+
-- exclude same-name as function/procedure
|
|
381
484
|
SELECT 1 FROM pg_proc p
|
|
382
485
|
WHERE p.proname = t.typname
|
|
383
486
|
AND p.pronamespace = n.oid
|
|
384
487
|
)
|
|
385
|
-
AND t.typname NOT LIKE 'pg_%' -- PostgreSQL
|
|
386
|
-
AND t.typname NOT LIKE '_%' --
|
|
387
|
-
AND t.typname NOT LIKE '%_old' --
|
|
388
|
-
AND t.typname NOT LIKE '%_bak' --
|
|
389
|
-
AND t.typname NOT LIKE 'tmp_%' --
|
|
488
|
+
AND t.typname NOT LIKE 'pg_%' -- exclude PostgreSQL built-in types
|
|
489
|
+
AND t.typname NOT LIKE '_%' -- exclude array types (leading underscore)
|
|
490
|
+
AND t.typname NOT LIKE '%_old' -- exclude deprecated types
|
|
491
|
+
AND t.typname NOT LIKE '%_bak' -- exclude backup types
|
|
492
|
+
AND t.typname NOT LIKE 'tmp_%' -- exclude temporary types
|
|
390
493
|
ORDER BY n.nspname, t.typname
|
|
391
494
|
`, schemas);
|
|
392
|
-
//
|
|
495
|
+
// Initialize progress
|
|
393
496
|
if (progress) {
|
|
394
497
|
progress.customTypes.total = result.rows.length;
|
|
395
498
|
}
|
|
396
499
|
for (let i = 0; i < result.rows.length; i++) {
|
|
397
500
|
const row = result.rows[i];
|
|
398
|
-
//
|
|
501
|
+
// Update progress
|
|
399
502
|
if (progress && spinner) {
|
|
400
503
|
progress.customTypes.current = i + 1;
|
|
401
504
|
displayProgress(progress, spinner);
|
|
402
505
|
}
|
|
403
506
|
let ddl = '';
|
|
404
507
|
if (row.type_category === 'enum') {
|
|
405
|
-
// ENUM
|
|
508
|
+
// Fetch ENUM type details
|
|
406
509
|
const enumResult = await client.query(`
|
|
407
510
|
SELECT enumlabel
|
|
408
511
|
FROM pg_enum
|
|
@@ -414,14 +517,14 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
414
517
|
)
|
|
415
518
|
ORDER BY enumsortorder
|
|
416
519
|
`, [row.type_name, row.schema_name]);
|
|
417
|
-
// ENUM
|
|
520
|
+
// Generate DDL only when ENUM values exist
|
|
418
521
|
if (enumResult.rows.length > 0) {
|
|
419
522
|
const labels = enumResult.rows.map(r => `'${r.enumlabel}'`).join(', ');
|
|
420
523
|
ddl = `CREATE TYPE ${row.type_name} AS ENUM (${labels});`;
|
|
421
524
|
}
|
|
422
525
|
}
|
|
423
526
|
else if (row.type_category === 'composite') {
|
|
424
|
-
// COMPOSITE
|
|
527
|
+
// Fetch COMPOSITE type details
|
|
425
528
|
const compositeResult = await client.query(`
|
|
426
529
|
SELECT
|
|
427
530
|
a.attname as column_name,
|
|
@@ -434,10 +537,10 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
434
537
|
)
|
|
435
538
|
)
|
|
436
539
|
AND a.attnum > 0
|
|
437
|
-
AND NOT a.attisdropped --
|
|
540
|
+
AND NOT a.attisdropped -- exclude dropped columns
|
|
438
541
|
ORDER BY a.attnum
|
|
439
542
|
`, [row.type_name, row.schema_name]);
|
|
440
|
-
//
|
|
543
|
+
// Generate DDL only when composite type has attributes
|
|
441
544
|
if (compositeResult.rows.length > 0) {
|
|
442
545
|
const columns = compositeResult.rows
|
|
443
546
|
.map(r => ` ${r.column_name} ${r.column_type}`)
|
|
@@ -446,7 +549,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
446
549
|
}
|
|
447
550
|
}
|
|
448
551
|
else if (row.type_category === 'domain') {
|
|
449
|
-
// DOMAIN
|
|
552
|
+
// Fetch DOMAIN type details
|
|
450
553
|
const domainResult = await client.query(`
|
|
451
554
|
SELECT
|
|
452
555
|
pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
|
|
@@ -476,7 +579,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
476
579
|
}
|
|
477
580
|
}
|
|
478
581
|
if (ddl) {
|
|
479
|
-
//
|
|
582
|
+
// Add type comment at top
|
|
480
583
|
let finalDdl = '';
|
|
481
584
|
if (!row.comment) {
|
|
482
585
|
finalDdl += `-- Type: ${row.type_name}\n`;
|
|
@@ -484,9 +587,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
484
587
|
else {
|
|
485
588
|
finalDdl += `-- ${row.comment}\n`;
|
|
486
589
|
}
|
|
487
|
-
//
|
|
590
|
+
// Add type definition
|
|
488
591
|
finalDdl += ddl + '\n\n';
|
|
489
|
-
// COMMENT ON
|
|
592
|
+
// Add COMMENT ON statement
|
|
490
593
|
if (!row.comment) {
|
|
491
594
|
finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
|
|
492
595
|
}
|
|
@@ -494,8 +597,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
494
597
|
finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
|
|
495
598
|
}
|
|
496
599
|
types.push({
|
|
497
|
-
name:
|
|
600
|
+
name: row.type_name,
|
|
498
601
|
type: 'type',
|
|
602
|
+
schema: row.schema_name,
|
|
499
603
|
ddl: finalDdl,
|
|
500
604
|
comment: row.comment,
|
|
501
605
|
timestamp: Math.floor(Date.now() / 1000)
|
|
@@ -505,19 +609,19 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
505
609
|
return types;
|
|
506
610
|
}
|
|
507
611
|
/**
|
|
508
|
-
*
|
|
612
|
+
* Fetch table definitions from database
|
|
509
613
|
*/
|
|
510
614
|
async function fetchTableDefinitions(client, spinner, progress, schemas = ['public']) {
|
|
511
615
|
const definitions = [];
|
|
512
616
|
const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
|
|
513
|
-
//
|
|
617
|
+
// Fetch table list
|
|
514
618
|
const tablesResult = await client.query(`
|
|
515
619
|
SELECT tablename, schemaname, 'table' as type
|
|
516
620
|
FROM pg_tables
|
|
517
621
|
WHERE schemaname IN (${schemaPlaceholders})
|
|
518
622
|
ORDER BY schemaname, tablename
|
|
519
623
|
`, schemas);
|
|
520
|
-
//
|
|
624
|
+
// Fetch view list
|
|
521
625
|
const viewsResult = await client.query(`
|
|
522
626
|
SELECT viewname as tablename, schemaname, 'view' as type
|
|
523
627
|
FROM pg_views
|
|
@@ -527,22 +631,22 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
527
631
|
const allObjects = [...tablesResult.rows, ...viewsResult.rows];
|
|
528
632
|
const tableCount = tablesResult.rows.length;
|
|
529
633
|
const viewCount = viewsResult.rows.length;
|
|
530
|
-
//
|
|
634
|
+
// Initialize progress
|
|
531
635
|
if (progress) {
|
|
532
636
|
progress.tables.total = tableCount;
|
|
533
637
|
progress.views.total = viewCount;
|
|
534
638
|
}
|
|
535
|
-
//
|
|
536
|
-
//
|
|
639
|
+
// Process tables/views with limited concurrency (cap connection count)
|
|
640
|
+
// Max configurable via env (default 20, max 50)
|
|
537
641
|
const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
|
|
538
642
|
const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
|
|
539
|
-
//
|
|
643
|
+
// Use env value (capped at minimum 5)
|
|
540
644
|
const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
|
|
541
|
-
//
|
|
645
|
+
// Debug log (development only)
|
|
542
646
|
if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
|
|
543
647
|
console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
|
|
544
648
|
}
|
|
545
|
-
//
|
|
649
|
+
// Promise factory for table/view processing
|
|
546
650
|
const processObject = async (obj, index) => {
|
|
547
651
|
const isTable = obj.type === 'table';
|
|
548
652
|
const name = obj.tablename;
|
|
@@ -552,9 +656,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
552
656
|
let comment = '';
|
|
553
657
|
let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
|
554
658
|
if (type === 'table') {
|
|
555
|
-
//
|
|
659
|
+
// Table case
|
|
556
660
|
try {
|
|
557
|
-
//
|
|
661
|
+
// Get table last updated time
|
|
558
662
|
const tableStatsResult = await client.query(`
|
|
559
663
|
SELECT
|
|
560
664
|
EXTRACT(EPOCH FROM GREATEST(
|
|
@@ -571,11 +675,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
571
675
|
}
|
|
572
676
|
}
|
|
573
677
|
catch (error) {
|
|
574
|
-
//
|
|
678
|
+
// On error use default timestamp
|
|
575
679
|
}
|
|
576
|
-
// CREATE TABLE
|
|
680
|
+
// Generate CREATE TABLE statement
|
|
577
681
|
ddl = await generateCreateTableDDL(client, name, schemaName);
|
|
578
|
-
//
|
|
682
|
+
// Get table comment
|
|
579
683
|
try {
|
|
580
684
|
const tableCommentResult = await client.query(`
|
|
581
685
|
SELECT obj_description(c.oid) as table_comment
|
|
@@ -588,13 +692,13 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
588
692
|
}
|
|
589
693
|
}
|
|
590
694
|
catch (error) {
|
|
591
|
-
//
|
|
695
|
+
// On error no comment
|
|
592
696
|
}
|
|
593
697
|
}
|
|
594
698
|
else {
|
|
595
|
-
//
|
|
699
|
+
// View case
|
|
596
700
|
try {
|
|
597
|
-
//
|
|
701
|
+
// Get view definition and security_invoker setting
|
|
598
702
|
const viewResult = await client.query(`
|
|
599
703
|
SELECT
|
|
600
704
|
pv.definition,
|
|
@@ -610,14 +714,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
610
714
|
`, [schemaName, name]);
|
|
611
715
|
if (viewResult.rows.length > 0) {
|
|
612
716
|
const view = viewResult.rows[0];
|
|
613
|
-
//
|
|
717
|
+
// Get view comment
|
|
614
718
|
const viewCommentResult = await client.query(`
|
|
615
719
|
SELECT obj_description(c.oid) as view_comment
|
|
616
720
|
FROM pg_class c
|
|
617
721
|
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
618
722
|
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
|
|
619
723
|
`, [name, schemaName]);
|
|
620
|
-
//
|
|
724
|
+
// Add view comment at top
|
|
621
725
|
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
622
726
|
comment = viewCommentResult.rows[0].view_comment;
|
|
623
727
|
ddl = `-- ${comment}\n`;
|
|
@@ -625,9 +729,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
625
729
|
else {
|
|
626
730
|
ddl = `-- View: ${name}\n`;
|
|
627
731
|
}
|
|
628
|
-
//
|
|
629
|
-
let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
|
|
630
|
-
// security_invoker
|
|
732
|
+
// Add view definition
|
|
733
|
+
let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
|
|
734
|
+
// Check security_invoker setting
|
|
631
735
|
if (view.reloptions) {
|
|
632
736
|
for (const option of view.reloptions) {
|
|
633
737
|
if (option.startsWith('security_invoker=')) {
|
|
@@ -643,14 +747,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
643
747
|
}
|
|
644
748
|
}
|
|
645
749
|
ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
|
|
646
|
-
// COMMENT ON
|
|
750
|
+
// Add COMMENT ON statement
|
|
647
751
|
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
648
752
|
ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
|
|
649
753
|
}
|
|
650
754
|
else {
|
|
651
755
|
ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
|
|
652
756
|
}
|
|
653
|
-
//
|
|
757
|
+
// Get view creation time (if available)
|
|
654
758
|
try {
|
|
655
759
|
const viewStatsResult = await client.query(`
|
|
656
760
|
SELECT EXTRACT(EPOCH FROM GREATEST(
|
|
@@ -666,41 +770,42 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
666
770
|
}
|
|
667
771
|
}
|
|
668
772
|
catch (error) {
|
|
669
|
-
//
|
|
773
|
+
// On error use default timestamp
|
|
670
774
|
}
|
|
671
775
|
}
|
|
672
776
|
}
|
|
673
777
|
catch (error) {
|
|
674
|
-
//
|
|
778
|
+
// On error no comment
|
|
675
779
|
}
|
|
676
780
|
}
|
|
677
781
|
return {
|
|
678
|
-
name
|
|
782
|
+
name,
|
|
679
783
|
type,
|
|
784
|
+
schema: schemaName,
|
|
680
785
|
ddl,
|
|
681
786
|
timestamp,
|
|
682
787
|
comment: comment || undefined,
|
|
683
788
|
isTable
|
|
684
789
|
};
|
|
685
790
|
};
|
|
686
|
-
//
|
|
791
|
+
// Simple batch concurrency (reliable progress updates)
|
|
687
792
|
const processedResults = [];
|
|
688
793
|
for (let i = 0; i < allObjects.length; i += CONCURRENT_LIMIT) {
|
|
689
794
|
const batch = allObjects.slice(i, i + CONCURRENT_LIMIT);
|
|
690
|
-
//
|
|
795
|
+
// Process batch in parallel
|
|
691
796
|
const batchPromises = batch.map(async (obj, batchIndex) => {
|
|
692
797
|
try {
|
|
693
798
|
const globalIndex = i + batchIndex;
|
|
694
|
-
//
|
|
799
|
+
// Debug: start
|
|
695
800
|
if (process.env.SUPATOOL_DEBUG) {
|
|
696
801
|
console.log(`Starting ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
|
|
697
802
|
}
|
|
698
803
|
const result = await processObject(obj, globalIndex);
|
|
699
|
-
//
|
|
804
|
+
// Debug: done
|
|
700
805
|
if (process.env.SUPATOOL_DEBUG) {
|
|
701
806
|
console.log(`Completed ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
|
|
702
807
|
}
|
|
703
|
-
//
|
|
808
|
+
// Update progress immediately on each completion
|
|
704
809
|
if (result && progress && spinner) {
|
|
705
810
|
if (result.isTable) {
|
|
706
811
|
progress.tables.current = Math.min(progress.tables.current + 1, progress.tables.total);
|
|
@@ -717,11 +822,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
717
822
|
return null;
|
|
718
823
|
}
|
|
719
824
|
});
|
|
720
|
-
//
|
|
825
|
+
// Wait for batch to complete
|
|
721
826
|
const batchResults = await Promise.all(batchPromises);
|
|
722
827
|
processedResults.push(...batchResults);
|
|
723
828
|
}
|
|
724
|
-
//
|
|
829
|
+
// Add to definitions excluding nulls
|
|
725
830
|
for (const result of processedResults) {
|
|
726
831
|
if (result) {
|
|
727
832
|
const { isTable, ...definition } = result;
|
|
@@ -731,12 +836,12 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
731
836
|
return definitions;
|
|
732
837
|
}
|
|
733
838
|
/**
|
|
734
|
-
* CREATE TABLE DDL
|
|
839
|
+
* Generate CREATE TABLE DDL (concurrent version)
|
|
735
840
|
*/
|
|
736
841
|
async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
|
|
737
|
-
//
|
|
842
|
+
// Run all queries in parallel
|
|
738
843
|
const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
|
|
739
|
-
//
|
|
844
|
+
// Get column info
|
|
740
845
|
client.query(`
|
|
741
846
|
SELECT
|
|
742
847
|
c.column_name,
|
|
@@ -754,7 +859,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
754
859
|
AND c.table_name = $2
|
|
755
860
|
ORDER BY c.ordinal_position
|
|
756
861
|
`, [schemaName, tableName]),
|
|
757
|
-
//
|
|
862
|
+
// Get primary key info
|
|
758
863
|
client.query(`
|
|
759
864
|
SELECT column_name
|
|
760
865
|
FROM information_schema.table_constraints tc
|
|
@@ -765,14 +870,14 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
765
870
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
766
871
|
ORDER BY kcu.ordinal_position
|
|
767
872
|
`, [schemaName, tableName]),
|
|
768
|
-
//
|
|
873
|
+
// Get table comment
|
|
769
874
|
client.query(`
|
|
770
875
|
SELECT obj_description(c.oid) as table_comment
|
|
771
876
|
FROM pg_class c
|
|
772
877
|
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
773
878
|
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
|
|
774
879
|
`, [tableName, schemaName]),
|
|
775
|
-
//
|
|
880
|
+
// Get column comments
|
|
776
881
|
client.query(`
|
|
777
882
|
SELECT
|
|
778
883
|
c.column_name,
|
|
@@ -786,7 +891,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
786
891
|
AND pgn.nspname = $1
|
|
787
892
|
ORDER BY c.ordinal_position
|
|
788
893
|
`, [schemaName, tableName]),
|
|
789
|
-
// UNIQUE
|
|
894
|
+
// Get UNIQUE constraints
|
|
790
895
|
client.query(`
|
|
791
896
|
SELECT
|
|
792
897
|
tc.constraint_name,
|
|
@@ -800,7 +905,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
800
905
|
GROUP BY tc.constraint_name
|
|
801
906
|
ORDER BY tc.constraint_name
|
|
802
907
|
`, [schemaName, tableName]),
|
|
803
|
-
// FOREIGN KEY
|
|
908
|
+
// Get FOREIGN KEY constraints
|
|
804
909
|
client.query(`
|
|
805
910
|
SELECT
|
|
806
911
|
tc.constraint_name,
|
|
@@ -826,51 +931,51 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
826
931
|
columnComments.set(row.column_name, row.column_comment);
|
|
827
932
|
}
|
|
828
933
|
});
|
|
829
|
-
//
|
|
934
|
+
// Add table comment at top (with schema name)
|
|
830
935
|
let ddl = '';
|
|
831
|
-
//
|
|
936
|
+
// Add table comment at top
|
|
832
937
|
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
|
833
938
|
ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
|
|
834
939
|
}
|
|
835
940
|
else {
|
|
836
|
-
ddl += `-- Table: ${tableName}\n`;
|
|
941
|
+
ddl += `-- Table: ${schemaName}.${tableName}\n`;
|
|
837
942
|
}
|
|
838
|
-
// CREATE TABLE
|
|
839
|
-
ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
|
|
943
|
+
// Generate CREATE TABLE (schema-qualified)
|
|
944
|
+
ddl += `CREATE TABLE IF NOT EXISTS ${schemaName}.${tableName} (\n`;
|
|
840
945
|
const columnDefs = [];
|
|
841
946
|
for (const col of columnsResult.rows) {
|
|
842
947
|
const rawType = col.full_type ||
|
|
843
948
|
((col.data_type === 'USER-DEFINED' && col.udt_name) ? col.udt_name : col.data_type);
|
|
844
949
|
let colDef = ` ${col.column_name} ${rawType}`;
|
|
845
|
-
//
|
|
950
|
+
// Length spec
|
|
846
951
|
if (col.character_maximum_length) {
|
|
847
952
|
colDef += `(${col.character_maximum_length})`;
|
|
848
953
|
}
|
|
849
|
-
// NOT NULL
|
|
954
|
+
// NOT NULL constraint
|
|
850
955
|
if (col.is_nullable === 'NO') {
|
|
851
956
|
colDef += ' NOT NULL';
|
|
852
957
|
}
|
|
853
|
-
//
|
|
958
|
+
// Default value
|
|
854
959
|
if (col.column_default) {
|
|
855
960
|
colDef += ` DEFAULT ${col.column_default}`;
|
|
856
961
|
}
|
|
857
962
|
columnDefs.push(colDef);
|
|
858
963
|
}
|
|
859
964
|
ddl += columnDefs.join(',\n');
|
|
860
|
-
//
|
|
965
|
+
// Primary key constraint
|
|
861
966
|
if (primaryKeyResult.rows.length > 0) {
|
|
862
967
|
const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
|
|
863
968
|
ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
|
|
864
969
|
}
|
|
865
|
-
// UNIQUE
|
|
970
|
+
// Add UNIQUE constraints inside CREATE TABLE
|
|
866
971
|
for (const unique of uniqueConstraintResult.rows) {
|
|
867
972
|
ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
|
|
868
973
|
}
|
|
869
|
-
// FOREIGN KEY
|
|
974
|
+
// Add FOREIGN KEY constraints inside CREATE TABLE
|
|
870
975
|
for (const fk of foreignKeyResult.rows) {
|
|
871
976
|
ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
|
|
872
977
|
}
|
|
873
|
-
// CHECK
|
|
978
|
+
// Add CHECK constraints inside CREATE TABLE (must be last)
|
|
874
979
|
const checkConstraintResult = await client.query(`
|
|
875
980
|
SELECT
|
|
876
981
|
con.conname as constraint_name,
|
|
@@ -887,16 +992,16 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
887
992
|
ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
|
|
888
993
|
}
|
|
889
994
|
ddl += '\n);\n\n';
|
|
890
|
-
// COMMENT ON
|
|
995
|
+
// Add COMMENT ON statements
|
|
891
996
|
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
|
892
997
|
ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
|
|
893
998
|
}
|
|
894
999
|
else {
|
|
895
1000
|
ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
|
|
896
1001
|
}
|
|
897
|
-
//
|
|
1002
|
+
// Add column comments (with schema name)
|
|
898
1003
|
if (columnComments.size > 0) {
|
|
899
|
-
ddl += '\n--
|
|
1004
|
+
ddl += '\n-- Column comments\n';
|
|
900
1005
|
for (const [columnName, comment] of columnComments) {
|
|
901
1006
|
ddl += `COMMENT ON COLUMN ${schemaName}.${tableName}.${columnName} IS '${comment}';\n`;
|
|
902
1007
|
}
|
|
@@ -904,271 +1009,386 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
904
1009
|
return ddl;
|
|
905
1010
|
}
|
|
906
1011
|
/**
|
|
907
|
-
*
|
|
1012
|
+
* Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
|
|
908
1013
|
*/
|
|
909
|
-
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true) {
|
|
1014
|
+
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = []) {
|
|
910
1015
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
911
1016
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
912
|
-
|
|
1017
|
+
const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
1018
|
+
const npmUrl = 'https://www.npmjs.com/package/supatool';
|
|
1019
|
+
const headerComment = version
|
|
1020
|
+
? `-- Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n`
|
|
1021
|
+
: `-- Generated by supatool, ${outputDate} | ${npmUrl}\n`;
|
|
1022
|
+
const tables = definitions.filter(d => d.type === 'table');
|
|
1023
|
+
const views = definitions.filter(d => d.type === 'view');
|
|
1024
|
+
const rlsList = definitions.filter(d => d.type === 'rls');
|
|
1025
|
+
const triggersList = definitions.filter(d => d.type === 'trigger');
|
|
1026
|
+
const functions = definitions.filter(d => d.type === 'function');
|
|
1027
|
+
const cronJobs = definitions.filter(d => d.type === 'cron');
|
|
1028
|
+
const customTypes = definitions.filter(d => d.type === 'type');
|
|
1029
|
+
// schema.table -> RLS DDL
|
|
1030
|
+
const rlsByCategory = new Map();
|
|
1031
|
+
for (const r of rlsList) {
|
|
1032
|
+
if (r.category)
|
|
1033
|
+
rlsByCategory.set(r.category, r.ddl);
|
|
1034
|
+
}
|
|
1035
|
+
// schema.table -> trigger DDL array
|
|
1036
|
+
const triggersByCategory = new Map();
|
|
1037
|
+
for (const t of triggersList) {
|
|
1038
|
+
if (!t.category)
|
|
1039
|
+
continue;
|
|
1040
|
+
const list = triggersByCategory.get(t.category) ?? [];
|
|
1041
|
+
list.push(t.ddl);
|
|
1042
|
+
triggersByCategory.set(t.category, list);
|
|
1043
|
+
}
|
|
1044
|
+
// Build merged DDL (table/view + RLS + triggers)
|
|
1045
|
+
const mergeRlsAndTriggers = (def) => {
|
|
1046
|
+
const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
|
|
1047
|
+
let ddl = def.ddl.trimEnd();
|
|
1048
|
+
const rlsDdl = rlsByCategory.get(cat);
|
|
1049
|
+
if (rlsDdl) {
|
|
1050
|
+
ddl += '\n\n' + rlsDdl.trim();
|
|
1051
|
+
}
|
|
1052
|
+
const trgList = triggersByCategory.get(cat);
|
|
1053
|
+
if (trgList && trgList.length > 0) {
|
|
1054
|
+
ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
|
|
1055
|
+
}
|
|
1056
|
+
return ddl.endsWith('\n') ? ddl : ddl + '\n';
|
|
1057
|
+
};
|
|
1058
|
+
const mergedTables = tables.map(t => ({
|
|
1059
|
+
...t,
|
|
1060
|
+
ddl: mergeRlsAndTriggers(t)
|
|
1061
|
+
}));
|
|
1062
|
+
const mergedViews = views.map(v => ({
|
|
1063
|
+
...v,
|
|
1064
|
+
ddl: mergeRlsAndTriggers(v)
|
|
1065
|
+
}));
|
|
1066
|
+
const toWrite = [
|
|
1067
|
+
...mergedTables,
|
|
1068
|
+
...mergedViews,
|
|
1069
|
+
...functions,
|
|
1070
|
+
...cronJobs,
|
|
1071
|
+
...customTypes
|
|
1072
|
+
];
|
|
1073
|
+
const multiSchema = schemas.length > 1;
|
|
1074
|
+
const typeDirNames = {
|
|
1075
|
+
table: 'tables',
|
|
1076
|
+
view: 'views',
|
|
1077
|
+
function: 'rpc',
|
|
1078
|
+
cron: 'cron',
|
|
1079
|
+
type: 'types'
|
|
1080
|
+
};
|
|
913
1081
|
if (!fs.existsSync(outputDir)) {
|
|
914
1082
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
915
1083
|
}
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
} : {
|
|
926
|
-
// --no-separate の場合は全てルートに
|
|
927
|
-
table: outputDir,
|
|
928
|
-
view: outputDir,
|
|
929
|
-
rls: outputDir,
|
|
930
|
-
function: outputDir,
|
|
931
|
-
trigger: outputDir,
|
|
932
|
-
cron: outputDir,
|
|
933
|
-
type: outputDir
|
|
934
|
-
};
|
|
935
|
-
// 必要なディレクトリを事前作成
|
|
936
|
-
const requiredDirs = new Set(Object.values(typeDirectories));
|
|
937
|
-
for (const dir of requiredDirs) {
|
|
938
|
-
if (!fs.existsSync(dir)) {
|
|
939
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
1084
|
+
const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
1085
|
+
for (const def of toWrite) {
|
|
1086
|
+
const typeDir = typeDirNames[def.type];
|
|
1087
|
+
const baseTypeDir = separateDirectories ? typeDir : '.';
|
|
1088
|
+
const targetDir = multiSchema && def.schema
|
|
1089
|
+
? path.join(outputDir, def.schema, baseTypeDir)
|
|
1090
|
+
: path.join(outputDir, baseTypeDir);
|
|
1091
|
+
if (!fs.existsSync(targetDir)) {
|
|
1092
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
940
1093
|
}
|
|
941
|
-
}
|
|
942
|
-
// 並行ファイル書き込み
|
|
943
|
-
const writePromises = definitions.map(async (def) => {
|
|
944
|
-
const targetDir = typeDirectories[def.type];
|
|
945
|
-
// ファイル名を決定(TypeとTriggerを区別しやすくする)
|
|
946
1094
|
let fileName;
|
|
947
1095
|
if (def.type === 'function') {
|
|
948
1096
|
fileName = `fn_${def.name}.sql`;
|
|
949
1097
|
}
|
|
950
|
-
else if (def.type === 'trigger') {
|
|
951
|
-
fileName = `trg_${def.name}.sql`;
|
|
952
|
-
}
|
|
953
1098
|
else {
|
|
954
1099
|
fileName = `${def.name}.sql`;
|
|
955
1100
|
}
|
|
956
1101
|
const filePath = path.join(targetDir, fileName);
|
|
957
|
-
// 最後に改行を追加
|
|
958
1102
|
const ddlWithNewline = def.ddl.endsWith('\n') ? def.ddl : def.ddl + '\n';
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
});
|
|
963
|
-
// 全ファイル書き込みの完了を待機
|
|
964
|
-
await Promise.all(writePromises);
|
|
965
|
-
// インデックスファイルを生成
|
|
966
|
-
await generateIndexFile(definitions, outputDir, separateDirectories);
|
|
1103
|
+
await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
|
|
1104
|
+
}
|
|
1105
|
+
await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version, tableRlsStatus);
|
|
967
1106
|
}
|
|
968
1107
|
/**
|
|
969
|
-
*
|
|
970
|
-
* AIが構造を理解しやすいように1行ずつリストアップ
|
|
1108
|
+
* Generate index file for DB objects (RLS/triggers already merged into table/view)
|
|
971
1109
|
*/
|
|
972
|
-
async function generateIndexFile(definitions, outputDir, separateDirectories = true) {
|
|
1110
|
+
async function generateIndexFile(definitions, outputDir, separateDirectories = true, multiSchema = false, relations = [], rpcTables = [], allSchemas = [], extractedSchemas = [], version, tableRlsStatus = []) {
|
|
973
1111
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
974
1112
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
975
|
-
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1113
|
+
const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
1114
|
+
const npmUrl = 'https://www.npmjs.com/package/supatool';
|
|
1115
|
+
const headerLine = version
|
|
1116
|
+
? `Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n\n`
|
|
1117
|
+
: `Generated by supatool, ${outputDate} | ${npmUrl}\n\n`;
|
|
1118
|
+
const readmeHeader = version
|
|
1119
|
+
? `Generated by [supatool](${npmUrl}) v${version}, ${outputDate}\n\n`
|
|
1120
|
+
: `Generated by [supatool](${npmUrl}), ${outputDate}\n\n`;
|
|
1121
|
+
const typeDirNames = {
|
|
1122
|
+
table: 'tables',
|
|
1123
|
+
view: 'views',
|
|
1124
|
+
function: 'rpc',
|
|
1125
|
+
cron: 'cron',
|
|
1126
|
+
type: 'types'
|
|
984
1127
|
};
|
|
985
1128
|
const typeLabels = {
|
|
986
1129
|
table: 'Tables',
|
|
987
1130
|
view: 'Views',
|
|
988
|
-
rls: 'RLS Policies',
|
|
989
1131
|
function: 'Functions',
|
|
990
|
-
trigger: 'Triggers',
|
|
991
1132
|
cron: 'Cron Jobs',
|
|
992
1133
|
type: 'Custom Types'
|
|
993
1134
|
};
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1135
|
+
const groupedDefs = {
|
|
1136
|
+
table: definitions.filter(def => def.type === 'table'),
|
|
1137
|
+
view: definitions.filter(def => def.type === 'view'),
|
|
1138
|
+
function: definitions.filter(def => def.type === 'function'),
|
|
1139
|
+
cron: definitions.filter(def => def.type === 'cron'),
|
|
1140
|
+
type: definitions.filter(def => def.type === 'type')
|
|
1141
|
+
};
|
|
1142
|
+
// schema.table -> RLS status (for Tables docs and warnings)
|
|
1143
|
+
const rlsMap = new Map();
|
|
1144
|
+
for (const s of tableRlsStatus) {
|
|
1145
|
+
rlsMap.set(`${s.schema}.${s.table}`, s);
|
|
1146
|
+
}
|
|
1147
|
+
const formatRlsNote = (schema, name) => {
|
|
1148
|
+
const s = rlsMap.get(`${schema}.${name}`);
|
|
1149
|
+
if (!s)
|
|
1150
|
+
return '';
|
|
1151
|
+
if (!s.rlsEnabled)
|
|
1152
|
+
return ' **⚠️ RLS disabled**';
|
|
1153
|
+
if (s.policyCount === 0)
|
|
1154
|
+
return ' (RLS: enabled, policies: 0)';
|
|
1155
|
+
return ` (RLS: enabled, policies: ${s.policyCount})`;
|
|
1156
|
+
};
|
|
1157
|
+
// Build relative path per file (schema/type/file when multiSchema)
|
|
1158
|
+
const getRelPath = (def) => {
|
|
1159
|
+
const typeDir = separateDirectories ? (typeDirNames[def.type] ?? def.type) : '.';
|
|
1160
|
+
const fileName = def.type === 'function' ? `fn_${def.name}.sql` : `${def.name}.sql`;
|
|
1161
|
+
if (multiSchema && def.schema) {
|
|
1162
|
+
return `${def.schema}/${typeDir}/${fileName}`;
|
|
1001
1163
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
else if (def.type === 'trigger') {
|
|
1020
|
-
fileName = `trg_${def.name}.sql`;
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
fileName = `${def.name}.sql`;
|
|
1024
|
-
}
|
|
1025
|
-
const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
|
|
1026
|
-
const commentText = def.comment ? ` - ${def.comment}` : '';
|
|
1027
|
-
indexContent += `- [${def.name}](${filePath})${commentText}\n`;
|
|
1028
|
-
});
|
|
1029
|
-
indexContent += '\n';
|
|
1030
|
-
});
|
|
1031
|
-
// ディレクトリ構造(フォルダのみ)
|
|
1032
|
-
indexContent += '## Directory Structure\n\n';
|
|
1033
|
-
indexContent += '```\n';
|
|
1034
|
-
indexContent += 'schemas/\n';
|
|
1035
|
-
indexContent += '├── index.md\n';
|
|
1036
|
-
indexContent += '├── llms.txt\n';
|
|
1037
|
-
if (separateDirectories) {
|
|
1038
|
-
Object.entries(groupedDefs).forEach(([type, defs]) => {
|
|
1039
|
-
if (defs.length === 0)
|
|
1040
|
-
return;
|
|
1041
|
-
const folderName = type === 'trigger' ? 'rpc' : type === 'table' ? 'tables' : type;
|
|
1042
|
-
indexContent += `└── ${folderName}/\n`;
|
|
1043
|
-
});
|
|
1164
|
+
return separateDirectories ? `${typeDir}/${fileName}` : fileName;
|
|
1165
|
+
};
|
|
1166
|
+
// === Human-readable README.md (description + link to llms.txt) ===
|
|
1167
|
+
let readmeContent = readmeHeader;
|
|
1168
|
+
readmeContent += '# Schema (extract output)\n\n';
|
|
1169
|
+
readmeContent += 'This folder contains DDL exported by `supatool extract`.\n\n';
|
|
1170
|
+
readmeContent += '- **tables/** – Table definitions (with RLS and triggers in the same file)\n';
|
|
1171
|
+
readmeContent += '- **views/** – View definitions\n';
|
|
1172
|
+
readmeContent += '- **rpc/** – Functions\n';
|
|
1173
|
+
readmeContent += '- **cron/** – Cron jobs\n';
|
|
1174
|
+
readmeContent += '- **types/** – Custom types\n\n';
|
|
1175
|
+
if (multiSchema) {
|
|
1176
|
+
readmeContent += 'When multiple schemas are extracted, each schema has its own subfolder (e.g. `public/tables/`, `agent/views/`).\n\n';
|
|
1177
|
+
}
|
|
1178
|
+
readmeContent += 'Full catalog and relations: [llms.txt](llms.txt)\n';
|
|
1179
|
+
if (tableRlsStatus.some(s => !s.rlsEnabled)) {
|
|
1180
|
+
readmeContent += '\n⚠️ Tables with RLS disabled: [rls_warnings.md](rls_warnings.md)\n';
|
|
1044
1181
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1182
|
+
// === llms.txt ===
|
|
1183
|
+
let llmsContent = headerLine;
|
|
1184
|
+
llmsContent += 'Database Schema - Complete Objects Catalog\n';
|
|
1185
|
+
llmsContent += '(Tables/Views include RLS and Triggers in the same file)\n\n';
|
|
1049
1186
|
llmsContent += 'SUMMARY\n';
|
|
1050
1187
|
Object.entries(groupedDefs).forEach(([type, defs]) => {
|
|
1051
|
-
if (defs.length > 0) {
|
|
1188
|
+
if (defs.length > 0 && typeLabels[type]) {
|
|
1052
1189
|
llmsContent += `${typeLabels[type]}: ${defs.length}\n`;
|
|
1053
1190
|
}
|
|
1054
1191
|
});
|
|
1055
|
-
llmsContent += '\n';
|
|
1056
|
-
// Flat list for AI processing (single format)
|
|
1057
|
-
llmsContent += 'OBJECTS\n';
|
|
1192
|
+
llmsContent += '\nOBJECTS\n';
|
|
1058
1193
|
definitions.forEach(def => {
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
if (def.type === 'function') {
|
|
1065
|
-
fileName = `fn_${def.name}.sql`;
|
|
1066
|
-
}
|
|
1067
|
-
else if (def.type === 'trigger') {
|
|
1068
|
-
fileName = `trg_${def.name}.sql`;
|
|
1069
|
-
}
|
|
1070
|
-
else {
|
|
1071
|
-
fileName = `${def.name}.sql`;
|
|
1072
|
-
}
|
|
1073
|
-
const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
|
|
1074
|
-
const commentText = def.comment ? `:${def.comment}` : '';
|
|
1075
|
-
llmsContent += `${def.type}:${def.name}:${filePath}${commentText}\n`;
|
|
1194
|
+
const filePath = getRelPath(def);
|
|
1195
|
+
const commentSuffix = def.comment ? ` # ${def.comment}` : '';
|
|
1196
|
+
const displayName = def.schema ? `${def.schema}.${def.name}` : def.name;
|
|
1197
|
+
const rlsSuffix = def.type === 'table' && def.schema ? formatRlsNote(def.schema, def.name) : '';
|
|
1198
|
+
llmsContent += `${def.type}:${displayName}:${filePath}${commentSuffix}${rlsSuffix}\n`;
|
|
1076
1199
|
});
|
|
1077
|
-
|
|
1078
|
-
|
|
1200
|
+
if (relations.length > 0) {
|
|
1201
|
+
llmsContent += '\nRELATIONS\n';
|
|
1202
|
+
relations.forEach(r => {
|
|
1203
|
+
llmsContent += `${r.from} -> ${r.to}\n`;
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (rpcTables.length > 0) {
|
|
1207
|
+
llmsContent += '\nRPC_TABLES\n';
|
|
1208
|
+
rpcTables.forEach(rt => {
|
|
1209
|
+
llmsContent += `${rt.rpc}: ${rt.tables.join(', ')}\n`;
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
if (allSchemas.length > 0) {
|
|
1213
|
+
const extractedSet = new Set(extractedSchemas);
|
|
1214
|
+
const extractedList = allSchemas.filter(s => extractedSet.has(s));
|
|
1215
|
+
const notExtractedList = allSchemas.filter(s => !extractedSet.has(s));
|
|
1216
|
+
llmsContent += '\nALL_SCHEMAS\n';
|
|
1217
|
+
llmsContent += 'EXTRACTED\n';
|
|
1218
|
+
extractedList.forEach(schemaName => {
|
|
1219
|
+
llmsContent += `${schemaName}\n`;
|
|
1220
|
+
});
|
|
1221
|
+
llmsContent += '\nNOT_EXTRACTED\n';
|
|
1222
|
+
notExtractedList.forEach(schemaName => {
|
|
1223
|
+
llmsContent += `${schemaName}\n`;
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
const readmePath = path.join(outputDir, 'README.md');
|
|
1079
1227
|
const llmsPath = path.join(outputDir, 'llms.txt');
|
|
1080
|
-
fs.writeFileSync(
|
|
1228
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
1081
1229
|
fs.writeFileSync(llmsPath, llmsContent);
|
|
1230
|
+
// schema_index.json (same data for agents that parse JSON)
|
|
1231
|
+
const schemaIndex = {
|
|
1232
|
+
objects: definitions.map(def => {
|
|
1233
|
+
const base = {
|
|
1234
|
+
type: def.type,
|
|
1235
|
+
name: def.schema ? `${def.schema}.${def.name}` : def.name,
|
|
1236
|
+
path: getRelPath(def),
|
|
1237
|
+
...(def.comment && { comment: def.comment })
|
|
1238
|
+
};
|
|
1239
|
+
if (def.type === 'table' && def.schema) {
|
|
1240
|
+
const s = rlsMap.get(`${def.schema}.${def.name}`);
|
|
1241
|
+
if (s) {
|
|
1242
|
+
base.rls = s.rlsEnabled ? (s.policyCount === 0 ? 'enabled_no_policies' : `enabled_${s.policyCount}_policies`) : 'disabled';
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return base;
|
|
1246
|
+
}),
|
|
1247
|
+
relations: relations.map(r => ({ from: r.from, to: r.to })),
|
|
1248
|
+
rpc_tables: rpcTables.map(rt => ({ rpc: rt.rpc, tables: rt.tables })),
|
|
1249
|
+
all_schemas: allSchemas.length > 0
|
|
1250
|
+
? {
|
|
1251
|
+
extracted: allSchemas.filter(s => extractedSchemas.includes(s)),
|
|
1252
|
+
not_extracted: allSchemas.filter(s => !extractedSchemas.includes(s))
|
|
1253
|
+
}
|
|
1254
|
+
: undefined
|
|
1255
|
+
};
|
|
1256
|
+
fs.writeFileSync(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2), 'utf8');
|
|
1257
|
+
// schema_summary.md (one-file overview for AI) — include RLS status per table
|
|
1258
|
+
let summaryMd = '# Schema summary\n\n';
|
|
1259
|
+
const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
|
|
1260
|
+
if (tableDefs.length > 0) {
|
|
1261
|
+
summaryMd += '## Tables / Views\n';
|
|
1262
|
+
tableDefs.forEach(d => {
|
|
1263
|
+
const name = d.schema ? `${d.schema}.${d.name}` : d.name;
|
|
1264
|
+
const rlsNote = d.type === 'table' && d.schema ? formatRlsNote(d.schema, d.name) : '';
|
|
1265
|
+
summaryMd += d.comment ? `- ${name}${rlsNote} (# ${d.comment})\n` : `- ${name}${rlsNote}\n`;
|
|
1266
|
+
});
|
|
1267
|
+
summaryMd += '\n';
|
|
1268
|
+
}
|
|
1269
|
+
if (relations.length > 0) {
|
|
1270
|
+
summaryMd += '## Relations\n';
|
|
1271
|
+
relations.forEach(r => {
|
|
1272
|
+
summaryMd += `- ${r.from} -> ${r.to}\n`;
|
|
1273
|
+
});
|
|
1274
|
+
summaryMd += '\n';
|
|
1275
|
+
}
|
|
1276
|
+
if (rpcTables.length > 0) {
|
|
1277
|
+
summaryMd += '## RPC → Tables\n';
|
|
1278
|
+
rpcTables.forEach(rt => {
|
|
1279
|
+
summaryMd += `- ${rt.rpc}: ${rt.tables.join(', ')}\n`;
|
|
1280
|
+
});
|
|
1281
|
+
summaryMd += '\n';
|
|
1282
|
+
}
|
|
1283
|
+
if (allSchemas.length > 0) {
|
|
1284
|
+
const extractedSet = new Set(extractedSchemas);
|
|
1285
|
+
summaryMd += '## Schemas\n';
|
|
1286
|
+
summaryMd += `- Extracted: ${allSchemas.filter(s => extractedSet.has(s)).join(', ') || '(none)'}\n`;
|
|
1287
|
+
summaryMd += `- Not extracted: ${allSchemas.filter(s => !extractedSet.has(s)).join(', ') || '(none)'}\n`;
|
|
1288
|
+
}
|
|
1289
|
+
fs.writeFileSync(path.join(outputDir, 'schema_summary.md'), summaryMd, 'utf8');
|
|
1290
|
+
// RLS disabled tables warning doc (tables only; RLS enabled with 0 policies is not warned)
|
|
1291
|
+
const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
|
|
1292
|
+
if (rlsNotEnabled.length > 0) {
|
|
1293
|
+
let warnMd = '# Tables with RLS disabled (warning)\n\n';
|
|
1294
|
+
warnMd += 'The following tables do not have Row Level Security enabled.\n';
|
|
1295
|
+
warnMd += 'Enabling RLS is recommended for production security.\n\n';
|
|
1296
|
+
warnMd += '| Schema | Table |\n|--------|-------|\n';
|
|
1297
|
+
rlsNotEnabled.forEach(s => {
|
|
1298
|
+
warnMd += `| ${s.schema} | ${s.table} |\n`;
|
|
1299
|
+
});
|
|
1300
|
+
fs.writeFileSync(path.join(outputDir, 'rls_warnings.md'), warnMd, 'utf8');
|
|
1301
|
+
}
|
|
1082
1302
|
}
|
|
1083
1303
|
/**
|
|
1084
|
-
*
|
|
1304
|
+
* Classify and output definitions
|
|
1085
1305
|
*/
|
|
1086
1306
|
async function extractDefinitions(options) {
|
|
1087
|
-
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'] } = options;
|
|
1088
|
-
// Node.js
|
|
1307
|
+
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
|
|
1308
|
+
// Disable Node.js SSL certificate verification
|
|
1089
1309
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1090
|
-
//
|
|
1310
|
+
// Connection string validation
|
|
1091
1311
|
if (!connectionString) {
|
|
1092
|
-
throw new Error('
|
|
1093
|
-
'1. --connection
|
|
1094
|
-
'2. SUPABASE_CONNECTION_STRING
|
|
1095
|
-
'3. DATABASE_URL
|
|
1096
|
-
'4. supatool.config.json
|
|
1312
|
+
throw new Error('Connection string is not configured. Please set it using one of:\n' +
|
|
1313
|
+
'1. --connection option\n' +
|
|
1314
|
+
'2. SUPABASE_CONNECTION_STRING environment variable\n' +
|
|
1315
|
+
'3. DATABASE_URL environment variable\n' +
|
|
1316
|
+
'4. supatool.config.json configuration file');
|
|
1097
1317
|
}
|
|
1098
|
-
//
|
|
1318
|
+
// Connection string format validation
|
|
1099
1319
|
if (!connectionString.startsWith('postgresql://') && !connectionString.startsWith('postgres://')) {
|
|
1100
|
-
throw new Error(
|
|
1101
|
-
'
|
|
1320
|
+
throw new Error(`Invalid connection string format: ${connectionString}\n` +
|
|
1321
|
+
'Correct format: postgresql://username:password@host:port/database');
|
|
1102
1322
|
}
|
|
1103
|
-
//
|
|
1323
|
+
// URL encode password part
|
|
1104
1324
|
let encodedConnectionString = connectionString;
|
|
1105
|
-
console.log('🔍
|
|
1325
|
+
console.log('🔍 Original connection string:', connectionString);
|
|
1106
1326
|
try {
|
|
1107
|
-
//
|
|
1327
|
+
// Special handling when password contains @
|
|
1108
1328
|
if (connectionString.includes('@') && connectionString.split('@').length > 2) {
|
|
1109
|
-
console.log('⚠️
|
|
1110
|
-
//
|
|
1329
|
+
console.log('⚠️ Password contains @, executing special handling');
|
|
1330
|
+
// Use last @ as delimiter
|
|
1111
1331
|
const parts = connectionString.split('@');
|
|
1112
|
-
const lastPart = parts.pop(); //
|
|
1113
|
-
const firstParts = parts.join('@'); //
|
|
1114
|
-
console.log('
|
|
1115
|
-
console.log('
|
|
1116
|
-
console.log('
|
|
1117
|
-
//
|
|
1332
|
+
const lastPart = parts.pop(); // Last part (host:port/database)
|
|
1333
|
+
const firstParts = parts.join('@'); // First part (postgresql://user:password)
|
|
1334
|
+
console.log(' Split result:');
|
|
1335
|
+
console.log(' First part:', firstParts);
|
|
1336
|
+
console.log(' Last part:', lastPart);
|
|
1337
|
+
// Encode password part
|
|
1118
1338
|
const colonIndex = firstParts.lastIndexOf(':');
|
|
1119
1339
|
if (colonIndex > 0) {
|
|
1120
1340
|
const protocolAndUser = firstParts.substring(0, colonIndex);
|
|
1121
1341
|
const password = firstParts.substring(colonIndex + 1);
|
|
1122
1342
|
const encodedPassword = encodeURIComponent(password);
|
|
1123
1343
|
encodedConnectionString = `${protocolAndUser}:${encodedPassword}@${lastPart}`;
|
|
1124
|
-
console.log('
|
|
1125
|
-
console.log('
|
|
1126
|
-
console.log('
|
|
1127
|
-
console.log('
|
|
1128
|
-
console.log('
|
|
1344
|
+
console.log(' Encode result:');
|
|
1345
|
+
console.log(' Protocol+User:', protocolAndUser);
|
|
1346
|
+
console.log(' Original password:', password);
|
|
1347
|
+
console.log(' Encoded password:', encodedPassword);
|
|
1348
|
+
console.log(' Final connection string:', encodedConnectionString);
|
|
1129
1349
|
}
|
|
1130
1350
|
}
|
|
1131
1351
|
else {
|
|
1132
|
-
console.log('✅
|
|
1133
|
-
//
|
|
1352
|
+
console.log('✅ Executing normal URL parsing');
|
|
1353
|
+
// Normal URL parsing
|
|
1134
1354
|
const url = new URL(connectionString);
|
|
1135
|
-
//
|
|
1355
|
+
// Handle username containing dots
|
|
1136
1356
|
if (url.username && url.username.includes('.')) {
|
|
1137
|
-
console.log(
|
|
1357
|
+
console.log(`Username (with dots): ${url.username}`);
|
|
1138
1358
|
}
|
|
1139
1359
|
if (url.password) {
|
|
1140
|
-
//
|
|
1360
|
+
// Encode only password part
|
|
1141
1361
|
const encodedPassword = encodeURIComponent(url.password);
|
|
1142
1362
|
url.password = encodedPassword;
|
|
1143
1363
|
encodedConnectionString = url.toString();
|
|
1144
|
-
console.log('
|
|
1364
|
+
console.log(' Password encoded:', encodedPassword);
|
|
1145
1365
|
}
|
|
1146
1366
|
}
|
|
1147
|
-
// Supabase
|
|
1367
|
+
// Add SSL settings for Supabase connection
|
|
1148
1368
|
if (!encodedConnectionString.includes('sslmode=')) {
|
|
1149
1369
|
const separator = encodedConnectionString.includes('?') ? '&' : '?';
|
|
1150
1370
|
encodedConnectionString += `${separator}sslmode=require`;
|
|
1151
|
-
console.log(' SSL
|
|
1371
|
+
console.log(' SSL setting added:', encodedConnectionString);
|
|
1152
1372
|
}
|
|
1153
|
-
//
|
|
1373
|
+
// Display debug info (password hidden)
|
|
1154
1374
|
const debugUrl = new URL(encodedConnectionString);
|
|
1155
1375
|
const maskedPassword = debugUrl.password ? '*'.repeat(debugUrl.password.length) : '';
|
|
1156
1376
|
debugUrl.password = maskedPassword;
|
|
1157
|
-
console.log('🔍
|
|
1158
|
-
console.log(`
|
|
1159
|
-
console.log(`
|
|
1160
|
-
console.log(`
|
|
1161
|
-
console.log(`
|
|
1377
|
+
console.log('🔍 Connection info:');
|
|
1378
|
+
console.log(` Host: ${debugUrl.hostname}`);
|
|
1379
|
+
console.log(` Port: ${debugUrl.port}`);
|
|
1380
|
+
console.log(` Database: ${debugUrl.pathname.slice(1)}`);
|
|
1381
|
+
console.log(` User: ${debugUrl.username}`);
|
|
1162
1382
|
console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
|
|
1163
1383
|
}
|
|
1164
1384
|
catch (error) {
|
|
1165
|
-
// URL
|
|
1166
|
-
console.warn('
|
|
1167
|
-
console.warn('
|
|
1385
|
+
// Use original string if URL parsing fails
|
|
1386
|
+
console.warn('Failed to parse connection string URL. May contain special characters.');
|
|
1387
|
+
console.warn('Error details:', error instanceof Error ? error.message : String(error));
|
|
1168
1388
|
}
|
|
1169
1389
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1170
1390
|
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
|
|
1171
|
-
//
|
|
1391
|
+
// Overwrite confirmation
|
|
1172
1392
|
if (!force && fs.existsSync(outputDir)) {
|
|
1173
1393
|
const files = fs.readdirSync(outputDir);
|
|
1174
1394
|
if (files.length > 0) {
|
|
@@ -1186,7 +1406,7 @@ async function extractDefinitions(options) {
|
|
|
1186
1406
|
}
|
|
1187
1407
|
}
|
|
1188
1408
|
}
|
|
1189
|
-
//
|
|
1409
|
+
// Dynamic import for spinner
|
|
1190
1410
|
const { default: ora } = await Promise.resolve().then(() => __importStar(require('ora')));
|
|
1191
1411
|
const spinner = ora('Connecting to database...').start();
|
|
1192
1412
|
const client = new pg_1.Client({
|
|
@@ -1197,14 +1417,14 @@ async function extractDefinitions(options) {
|
|
|
1197
1417
|
}
|
|
1198
1418
|
});
|
|
1199
1419
|
try {
|
|
1200
|
-
//
|
|
1201
|
-
console.log('🔧
|
|
1420
|
+
// Debug before connect
|
|
1421
|
+
console.log('🔧 Connection settings:');
|
|
1202
1422
|
console.log(` SSL: rejectUnauthorized=false`);
|
|
1203
|
-
console.log(`
|
|
1423
|
+
console.log(` Connection string length: ${encodedConnectionString.length}`);
|
|
1204
1424
|
await client.connect();
|
|
1205
1425
|
spinner.text = 'Connected to database';
|
|
1206
1426
|
let allDefinitions = [];
|
|
1207
|
-
//
|
|
1427
|
+
// Initialize progress tracker
|
|
1208
1428
|
const progress = {
|
|
1209
1429
|
tables: { current: 0, total: 0 },
|
|
1210
1430
|
views: { current: 0, total: 0 },
|
|
@@ -1215,14 +1435,14 @@ async function extractDefinitions(options) {
|
|
|
1215
1435
|
customTypes: { current: 0, total: 0 }
|
|
1216
1436
|
};
|
|
1217
1437
|
if (all) {
|
|
1218
|
-
//
|
|
1438
|
+
// Get total count for each object type first
|
|
1219
1439
|
spinner.text = 'Counting database objects...';
|
|
1220
|
-
//
|
|
1440
|
+
// Get total tables/views count
|
|
1221
1441
|
const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
|
|
1222
1442
|
const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
|
|
1223
1443
|
progress.tables.total = parseInt(tablesCountResult.rows[0].count);
|
|
1224
1444
|
progress.views.total = parseInt(viewsCountResult.rows[0].count);
|
|
1225
|
-
// RLS
|
|
1445
|
+
// Get total RLS policy count (per table)
|
|
1226
1446
|
try {
|
|
1227
1447
|
const rlsCountResult = await client.query(`
|
|
1228
1448
|
SELECT COUNT(DISTINCT tablename) as count
|
|
@@ -1234,7 +1454,7 @@ async function extractDefinitions(options) {
|
|
|
1234
1454
|
catch (error) {
|
|
1235
1455
|
progress.rls.total = 0;
|
|
1236
1456
|
}
|
|
1237
|
-
//
|
|
1457
|
+
// Get total functions count
|
|
1238
1458
|
const functionsCountResult = await client.query(`
|
|
1239
1459
|
SELECT COUNT(*) as count
|
|
1240
1460
|
FROM pg_proc p
|
|
@@ -1242,7 +1462,7 @@ async function extractDefinitions(options) {
|
|
|
1242
1462
|
WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p')
|
|
1243
1463
|
`);
|
|
1244
1464
|
progress.functions.total = parseInt(functionsCountResult.rows[0].count);
|
|
1245
|
-
//
|
|
1465
|
+
// Get total triggers count
|
|
1246
1466
|
const triggersCountResult = await client.query(`
|
|
1247
1467
|
SELECT COUNT(*) as count
|
|
1248
1468
|
FROM pg_trigger t
|
|
@@ -1251,7 +1471,7 @@ async function extractDefinitions(options) {
|
|
|
1251
1471
|
WHERE n.nspname = 'public' AND NOT t.tgisinternal
|
|
1252
1472
|
`);
|
|
1253
1473
|
progress.triggers.total = parseInt(triggersCountResult.rows[0].count);
|
|
1254
|
-
// Cron
|
|
1474
|
+
// Get total Cron jobs count
|
|
1255
1475
|
try {
|
|
1256
1476
|
const cronCountResult = await client.query('SELECT COUNT(*) as count FROM cron.job');
|
|
1257
1477
|
progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
|
|
@@ -1259,7 +1479,7 @@ async function extractDefinitions(options) {
|
|
|
1259
1479
|
catch (error) {
|
|
1260
1480
|
progress.cronJobs.total = 0;
|
|
1261
1481
|
}
|
|
1262
|
-
//
|
|
1482
|
+
// Get total custom types count
|
|
1263
1483
|
const typesCountResult = await client.query(`
|
|
1264
1484
|
SELECT COUNT(*) as count
|
|
1265
1485
|
FROM pg_type t
|
|
@@ -1276,7 +1496,7 @@ async function extractDefinitions(options) {
|
|
|
1276
1496
|
AND t.typname NOT LIKE '_%'
|
|
1277
1497
|
`);
|
|
1278
1498
|
progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
|
|
1279
|
-
// --all
|
|
1499
|
+
// When --all: fetch all objects (sequential)
|
|
1280
1500
|
const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
|
|
1281
1501
|
const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
|
|
1282
1502
|
const functions = await fetchFunctions(client, spinner, progress, schemas);
|
|
@@ -1293,8 +1513,8 @@ async function extractDefinitions(options) {
|
|
|
1293
1513
|
];
|
|
1294
1514
|
}
|
|
1295
1515
|
else {
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1516
|
+
// Legacy path (tables/views only)
|
|
1517
|
+
// Get total tables/views count
|
|
1298
1518
|
const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
|
|
1299
1519
|
const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
|
|
1300
1520
|
progress.tables.total = parseInt(tablesCountResult.rows[0].count);
|
|
@@ -1310,15 +1530,63 @@ async function extractDefinitions(options) {
|
|
|
1310
1530
|
allDefinitions = definitions;
|
|
1311
1531
|
}
|
|
1312
1532
|
}
|
|
1313
|
-
//
|
|
1533
|
+
// Pattern matching
|
|
1314
1534
|
if (tablePattern !== '*') {
|
|
1315
1535
|
const regex = new RegExp(tablePattern.replace(/\*/g, '.*'));
|
|
1316
1536
|
allDefinitions = allDefinitions.filter(def => regex.test(def.name));
|
|
1317
1537
|
}
|
|
1318
|
-
//
|
|
1538
|
+
// Fetch for RELATIONS / RPC_TABLES (llms.txt upgrade)
|
|
1539
|
+
let relations = [];
|
|
1540
|
+
let rpcTables = [];
|
|
1541
|
+
let allSchemas = [];
|
|
1542
|
+
try {
|
|
1543
|
+
allSchemas = await fetchAllSchemas(client);
|
|
1544
|
+
relations = await fetchRelationList(client, schemas);
|
|
1545
|
+
const funcDefs = allDefinitions.filter(d => d.type === 'function');
|
|
1546
|
+
for (const f of funcDefs) {
|
|
1547
|
+
const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
|
|
1548
|
+
if (tables.length > 0) {
|
|
1549
|
+
rpcTables.push({
|
|
1550
|
+
rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
|
|
1551
|
+
tables
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
catch (err) {
|
|
1557
|
+
if (process.env.SUPATOOL_DEBUG) {
|
|
1558
|
+
console.warn('RELATIONS/RPC_TABLES extraction skipped:', err);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
// RLS status (for Tables docs, rls_warnings.md, and extract-time warning)
|
|
1562
|
+
let tableRlsStatus = [];
|
|
1563
|
+
try {
|
|
1564
|
+
const tableDefs = allDefinitions.filter(d => d.type === 'table');
|
|
1565
|
+
if (tableDefs.length > 0) {
|
|
1566
|
+
tableRlsStatus = await fetchTableRlsStatus(client, schemas);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
catch (err) {
|
|
1570
|
+
if (process.env.SUPATOOL_DEBUG) {
|
|
1571
|
+
console.warn('RLS status fetch skipped:', err);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
// When force: remove output dir then write (so removed tables don't leave files)
|
|
1575
|
+
if (force && fs.existsSync(outputDir)) {
|
|
1576
|
+
fs.rmSync(outputDir, { recursive: true });
|
|
1577
|
+
}
|
|
1578
|
+
// Save definitions (table+RLS+triggers merged, schema folders)
|
|
1319
1579
|
spinner.text = 'Saving definitions to files...';
|
|
1320
|
-
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories);
|
|
1321
|
-
//
|
|
1580
|
+
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus);
|
|
1581
|
+
// Warn at extract time when any table has RLS disabled
|
|
1582
|
+
const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
|
|
1583
|
+
if (rlsNotEnabled.length > 0) {
|
|
1584
|
+
console.warn('');
|
|
1585
|
+
console.warn('⚠️ Tables with RLS disabled: ' + rlsNotEnabled.map(s => `${s.schema}.${s.table}`).join(', '));
|
|
1586
|
+
console.warn(' Details: ' + outputDir + '/rls_warnings.md');
|
|
1587
|
+
console.warn('');
|
|
1588
|
+
}
|
|
1589
|
+
// Show stats
|
|
1322
1590
|
const counts = {
|
|
1323
1591
|
table: allDefinitions.filter(def => def.type === 'table').length,
|
|
1324
1592
|
view: allDefinitions.filter(def => def.type === 'view').length,
|
|
@@ -1328,7 +1596,7 @@ async function extractDefinitions(options) {
|
|
|
1328
1596
|
cron: allDefinitions.filter(def => def.type === 'cron').length,
|
|
1329
1597
|
type: allDefinitions.filter(def => def.type === 'type').length
|
|
1330
1598
|
};
|
|
1331
|
-
//
|
|
1599
|
+
// Stop progress display
|
|
1332
1600
|
stopProgressDisplay();
|
|
1333
1601
|
spinner.succeed(`Extraction completed: ${outputDir}`);
|
|
1334
1602
|
if (counts.table > 0)
|
|
@@ -1348,7 +1616,7 @@ async function extractDefinitions(options) {
|
|
|
1348
1616
|
console.log('');
|
|
1349
1617
|
}
|
|
1350
1618
|
catch (error) {
|
|
1351
|
-
//
|
|
1619
|
+
// Stop progress display (on error)
|
|
1352
1620
|
stopProgressDisplay();
|
|
1353
1621
|
spinner.fail('Extraction failed');
|
|
1354
1622
|
console.error('Error:', error);
|
|
@@ -1359,7 +1627,7 @@ async function extractDefinitions(options) {
|
|
|
1359
1627
|
await client.end();
|
|
1360
1628
|
}
|
|
1361
1629
|
catch (closeError) {
|
|
1362
|
-
//
|
|
1630
|
+
// Ignore DB connection close errors (e.g. already disconnected)
|
|
1363
1631
|
}
|
|
1364
1632
|
}
|
|
1365
1633
|
}
|