supatool 0.3.7 → 0.4.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 +52 -285
- 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 +489 -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,72 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
192
190
|
}
|
|
193
191
|
}
|
|
194
192
|
/**
|
|
195
|
-
*
|
|
193
|
+
* Fetch FK relations list (for llms.txt RELATIONS)
|
|
194
|
+
*/
|
|
195
|
+
async function fetchRelationList(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
|
+
tc.table_schema AS from_schema,
|
|
202
|
+
tc.table_name AS from_table,
|
|
203
|
+
ccu.table_schema AS to_schema,
|
|
204
|
+
ccu.table_name AS to_table
|
|
205
|
+
FROM information_schema.table_constraints tc
|
|
206
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
207
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
208
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
209
|
+
AND tc.table_schema IN (${schemaPlaceholders})
|
|
210
|
+
ORDER BY tc.table_schema, tc.table_name
|
|
211
|
+
`, schemas);
|
|
212
|
+
return result.rows.map((r) => ({
|
|
213
|
+
from: `${r.from_schema}.${r.from_table}`,
|
|
214
|
+
to: `${r.to_schema}.${r.to_table}`
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Fetch all schemas in DB (for llms.txt ALL_SCHEMAS / unrextracted schemas)
|
|
219
|
+
*/
|
|
220
|
+
async function fetchAllSchemas(client) {
|
|
221
|
+
const result = await client.query(`
|
|
222
|
+
SELECT schema_name
|
|
223
|
+
FROM information_schema.schemata
|
|
224
|
+
WHERE schema_name NOT LIKE 'pg_%'
|
|
225
|
+
AND schema_name != 'information_schema'
|
|
226
|
+
AND schema_name != 'pg_catalog'
|
|
227
|
+
ORDER BY schema_name
|
|
228
|
+
`);
|
|
229
|
+
return result.rows.map((r) => r.schema_name);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Extract table refs from function DDL (heuristic, best-effort)
|
|
233
|
+
*/
|
|
234
|
+
function extractTableRefsFromFunctionDdl(ddl, defaultSchema = 'public') {
|
|
235
|
+
const seen = new Set();
|
|
236
|
+
const add = (schema, table) => {
|
|
237
|
+
const key = `${schema}.${table}`;
|
|
238
|
+
if (!seen.has(key))
|
|
239
|
+
seen.add(key);
|
|
240
|
+
};
|
|
241
|
+
// schema.table form
|
|
242
|
+
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;
|
|
243
|
+
let m;
|
|
244
|
+
while ((m = schemaTableRe.exec(ddl)) !== null) {
|
|
245
|
+
add(m[1], m[2]);
|
|
246
|
+
}
|
|
247
|
+
// Unqualified table name (after FROM / JOIN / INSERT INTO / UPDATE / DELETE FROM)
|
|
248
|
+
const unqualifiedRe = /(?:FROM|JOIN|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s|$|\)|,)/gi;
|
|
249
|
+
while ((m = unqualifiedRe.exec(ddl)) !== null) {
|
|
250
|
+
const name = m[1].toLowerCase();
|
|
251
|
+
if (!['select', 'where', 'on', 'and', 'or', 'inner', 'left', 'right', 'outer', 'cross', 'lateral'].includes(name)) {
|
|
252
|
+
add(defaultSchema, m[1]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return Array.from(seen).sort();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Fetch functions
|
|
196
259
|
*/
|
|
197
260
|
async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
198
261
|
const functions = [];
|
|
@@ -211,20 +274,20 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
211
274
|
AND p.prokind IN ('f', 'p') -- functions and procedures
|
|
212
275
|
ORDER BY n.nspname, p.proname
|
|
213
276
|
`, schemas);
|
|
214
|
-
//
|
|
277
|
+
// Initialize progress
|
|
215
278
|
if (progress) {
|
|
216
279
|
progress.functions.total = result.rows.length;
|
|
217
280
|
}
|
|
218
281
|
for (let i = 0; i < result.rows.length; i++) {
|
|
219
282
|
const row = result.rows[i];
|
|
220
|
-
//
|
|
283
|
+
// Update progress
|
|
221
284
|
if (progress && spinner) {
|
|
222
285
|
progress.functions.current = i + 1;
|
|
223
286
|
displayProgress(progress, spinner);
|
|
224
287
|
}
|
|
225
|
-
//
|
|
288
|
+
// Build exact function signature (schema name and argument types)
|
|
226
289
|
const functionSignature = `${row.schema_name}.${row.name}(${row.identity_args || ''})`;
|
|
227
|
-
//
|
|
290
|
+
// Add function comment at top
|
|
228
291
|
let ddl = '';
|
|
229
292
|
if (!row.comment) {
|
|
230
293
|
ddl += `-- Function: ${functionSignature}\n`;
|
|
@@ -232,13 +295,13 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
232
295
|
else {
|
|
233
296
|
ddl += `-- ${row.comment}\n`;
|
|
234
297
|
}
|
|
235
|
-
//
|
|
298
|
+
// Add function definition (ensure semicolon)
|
|
236
299
|
let functionDef = row.definition;
|
|
237
300
|
if (!functionDef.trim().endsWith(';')) {
|
|
238
301
|
functionDef += ';';
|
|
239
302
|
}
|
|
240
303
|
ddl += functionDef + '\n\n';
|
|
241
|
-
// COMMENT ON
|
|
304
|
+
// Add COMMENT ON statement
|
|
242
305
|
if (!row.comment) {
|
|
243
306
|
ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
|
|
244
307
|
}
|
|
@@ -248,6 +311,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
248
311
|
functions.push({
|
|
249
312
|
name: row.name,
|
|
250
313
|
type: 'function',
|
|
314
|
+
schema: row.schema_name,
|
|
251
315
|
ddl,
|
|
252
316
|
comment: row.comment,
|
|
253
317
|
timestamp: Math.floor(Date.now() / 1000)
|
|
@@ -256,7 +320,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
|
|
|
256
320
|
return functions;
|
|
257
321
|
}
|
|
258
322
|
/**
|
|
259
|
-
*
|
|
323
|
+
* Fetch triggers
|
|
260
324
|
*/
|
|
261
325
|
async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
262
326
|
const triggers = [];
|
|
@@ -274,18 +338,18 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
274
338
|
AND NOT t.tgisinternal
|
|
275
339
|
ORDER BY n.nspname, c.relname, t.tgname
|
|
276
340
|
`, schemas);
|
|
277
|
-
//
|
|
341
|
+
// Initialize progress
|
|
278
342
|
if (progress) {
|
|
279
343
|
progress.triggers.total = result.rows.length;
|
|
280
344
|
}
|
|
281
345
|
for (let i = 0; i < result.rows.length; i++) {
|
|
282
346
|
const row = result.rows[i];
|
|
283
|
-
//
|
|
347
|
+
// Update progress
|
|
284
348
|
if (progress && spinner) {
|
|
285
349
|
progress.triggers.current = i + 1;
|
|
286
350
|
displayProgress(progress, spinner);
|
|
287
351
|
}
|
|
288
|
-
//
|
|
352
|
+
// Add trigger description at top
|
|
289
353
|
let ddl = `-- Trigger: ${row.trigger_name} on ${row.schema_name}.${row.table_name}\n`;
|
|
290
354
|
ddl += `-- Database trigger that automatically executes in response to certain events\n\n`;
|
|
291
355
|
ddl += row.definition + ';';
|
|
@@ -293,6 +357,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
293
357
|
name: `${row.schema_name}_${row.table_name}_${row.trigger_name}`,
|
|
294
358
|
type: 'trigger',
|
|
295
359
|
category: `${row.schema_name}.${row.table_name}`,
|
|
360
|
+
schema: row.schema_name,
|
|
296
361
|
ddl,
|
|
297
362
|
timestamp: Math.floor(Date.now() / 1000)
|
|
298
363
|
});
|
|
@@ -300,7 +365,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
|
|
|
300
365
|
return triggers;
|
|
301
366
|
}
|
|
302
367
|
/**
|
|
303
|
-
* Cron
|
|
368
|
+
* Fetch Cron jobs (pg_cron extension)
|
|
304
369
|
*/
|
|
305
370
|
async function fetchCronJobs(client, spinner, progress) {
|
|
306
371
|
const cronJobs = [];
|
|
@@ -315,18 +380,18 @@ async function fetchCronJobs(client, spinner, progress) {
|
|
|
315
380
|
FROM cron.job
|
|
316
381
|
ORDER BY jobid
|
|
317
382
|
`);
|
|
318
|
-
//
|
|
383
|
+
// Initialize progress
|
|
319
384
|
if (progress) {
|
|
320
385
|
progress.cronJobs.total = result.rows.length;
|
|
321
386
|
}
|
|
322
387
|
for (let i = 0; i < result.rows.length; i++) {
|
|
323
388
|
const row = result.rows[i];
|
|
324
|
-
//
|
|
389
|
+
// Update progress
|
|
325
390
|
if (progress && spinner) {
|
|
326
391
|
progress.cronJobs.current = i + 1;
|
|
327
392
|
displayProgress(progress, spinner);
|
|
328
393
|
}
|
|
329
|
-
// Cron
|
|
394
|
+
// Add Cron job description at top
|
|
330
395
|
let ddl = `-- Cron Job: ${row.jobname || `job_${row.jobid}`}\n`;
|
|
331
396
|
ddl += `-- Scheduled job that runs automatically at specified intervals\n`;
|
|
332
397
|
ddl += `-- Schedule: ${row.schedule}\n`;
|
|
@@ -341,12 +406,12 @@ async function fetchCronJobs(client, spinner, progress) {
|
|
|
341
406
|
}
|
|
342
407
|
}
|
|
343
408
|
catch (error) {
|
|
344
|
-
// pg_cron
|
|
409
|
+
// Skip when pg_cron extension is not present
|
|
345
410
|
}
|
|
346
411
|
return cronJobs;
|
|
347
412
|
}
|
|
348
413
|
/**
|
|
349
|
-
*
|
|
414
|
+
* Fetch custom types
|
|
350
415
|
*/
|
|
351
416
|
async function fetchCustomTypes(client, spinner, progress, schemas = ['public']) {
|
|
352
417
|
const types = [];
|
|
@@ -368,41 +433,41 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
368
433
|
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
369
434
|
WHERE n.nspname IN (${schemaPlaceholders})
|
|
370
435
|
AND t.typtype IN ('e', 'c', 'd')
|
|
371
|
-
AND t.typisdefined = true --
|
|
372
|
-
AND NOT t.typarray = 0 --
|
|
436
|
+
AND t.typisdefined = true -- defined types only
|
|
437
|
+
AND NOT t.typarray = 0 -- exclude array base types
|
|
373
438
|
AND NOT EXISTS (
|
|
374
|
-
--
|
|
439
|
+
-- exclude same-name as table, view, index, sequence, composite
|
|
375
440
|
SELECT 1 FROM pg_class c
|
|
376
441
|
WHERE c.relname = t.typname
|
|
377
442
|
AND c.relnamespace = n.oid
|
|
378
443
|
)
|
|
379
444
|
AND NOT EXISTS (
|
|
380
|
-
--
|
|
445
|
+
-- exclude same-name as function/procedure
|
|
381
446
|
SELECT 1 FROM pg_proc p
|
|
382
447
|
WHERE p.proname = t.typname
|
|
383
448
|
AND p.pronamespace = n.oid
|
|
384
449
|
)
|
|
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_%' --
|
|
450
|
+
AND t.typname NOT LIKE 'pg_%' -- exclude PostgreSQL built-in types
|
|
451
|
+
AND t.typname NOT LIKE '_%' -- exclude array types (leading underscore)
|
|
452
|
+
AND t.typname NOT LIKE '%_old' -- exclude deprecated types
|
|
453
|
+
AND t.typname NOT LIKE '%_bak' -- exclude backup types
|
|
454
|
+
AND t.typname NOT LIKE 'tmp_%' -- exclude temporary types
|
|
390
455
|
ORDER BY n.nspname, t.typname
|
|
391
456
|
`, schemas);
|
|
392
|
-
//
|
|
457
|
+
// Initialize progress
|
|
393
458
|
if (progress) {
|
|
394
459
|
progress.customTypes.total = result.rows.length;
|
|
395
460
|
}
|
|
396
461
|
for (let i = 0; i < result.rows.length; i++) {
|
|
397
462
|
const row = result.rows[i];
|
|
398
|
-
//
|
|
463
|
+
// Update progress
|
|
399
464
|
if (progress && spinner) {
|
|
400
465
|
progress.customTypes.current = i + 1;
|
|
401
466
|
displayProgress(progress, spinner);
|
|
402
467
|
}
|
|
403
468
|
let ddl = '';
|
|
404
469
|
if (row.type_category === 'enum') {
|
|
405
|
-
// ENUM
|
|
470
|
+
// Fetch ENUM type details
|
|
406
471
|
const enumResult = await client.query(`
|
|
407
472
|
SELECT enumlabel
|
|
408
473
|
FROM pg_enum
|
|
@@ -414,14 +479,14 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
414
479
|
)
|
|
415
480
|
ORDER BY enumsortorder
|
|
416
481
|
`, [row.type_name, row.schema_name]);
|
|
417
|
-
// ENUM
|
|
482
|
+
// Generate DDL only when ENUM values exist
|
|
418
483
|
if (enumResult.rows.length > 0) {
|
|
419
484
|
const labels = enumResult.rows.map(r => `'${r.enumlabel}'`).join(', ');
|
|
420
485
|
ddl = `CREATE TYPE ${row.type_name} AS ENUM (${labels});`;
|
|
421
486
|
}
|
|
422
487
|
}
|
|
423
488
|
else if (row.type_category === 'composite') {
|
|
424
|
-
// COMPOSITE
|
|
489
|
+
// Fetch COMPOSITE type details
|
|
425
490
|
const compositeResult = await client.query(`
|
|
426
491
|
SELECT
|
|
427
492
|
a.attname as column_name,
|
|
@@ -434,10 +499,10 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
434
499
|
)
|
|
435
500
|
)
|
|
436
501
|
AND a.attnum > 0
|
|
437
|
-
AND NOT a.attisdropped --
|
|
502
|
+
AND NOT a.attisdropped -- exclude dropped columns
|
|
438
503
|
ORDER BY a.attnum
|
|
439
504
|
`, [row.type_name, row.schema_name]);
|
|
440
|
-
//
|
|
505
|
+
// Generate DDL only when composite type has attributes
|
|
441
506
|
if (compositeResult.rows.length > 0) {
|
|
442
507
|
const columns = compositeResult.rows
|
|
443
508
|
.map(r => ` ${r.column_name} ${r.column_type}`)
|
|
@@ -446,7 +511,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
446
511
|
}
|
|
447
512
|
}
|
|
448
513
|
else if (row.type_category === 'domain') {
|
|
449
|
-
// DOMAIN
|
|
514
|
+
// Fetch DOMAIN type details
|
|
450
515
|
const domainResult = await client.query(`
|
|
451
516
|
SELECT
|
|
452
517
|
pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
|
|
@@ -476,7 +541,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
476
541
|
}
|
|
477
542
|
}
|
|
478
543
|
if (ddl) {
|
|
479
|
-
//
|
|
544
|
+
// Add type comment at top
|
|
480
545
|
let finalDdl = '';
|
|
481
546
|
if (!row.comment) {
|
|
482
547
|
finalDdl += `-- Type: ${row.type_name}\n`;
|
|
@@ -484,9 +549,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
484
549
|
else {
|
|
485
550
|
finalDdl += `-- ${row.comment}\n`;
|
|
486
551
|
}
|
|
487
|
-
//
|
|
552
|
+
// Add type definition
|
|
488
553
|
finalDdl += ddl + '\n\n';
|
|
489
|
-
// COMMENT ON
|
|
554
|
+
// Add COMMENT ON statement
|
|
490
555
|
if (!row.comment) {
|
|
491
556
|
finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
|
|
492
557
|
}
|
|
@@ -494,8 +559,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
494
559
|
finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
|
|
495
560
|
}
|
|
496
561
|
types.push({
|
|
497
|
-
name:
|
|
562
|
+
name: row.type_name,
|
|
498
563
|
type: 'type',
|
|
564
|
+
schema: row.schema_name,
|
|
499
565
|
ddl: finalDdl,
|
|
500
566
|
comment: row.comment,
|
|
501
567
|
timestamp: Math.floor(Date.now() / 1000)
|
|
@@ -505,19 +571,19 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
|
|
|
505
571
|
return types;
|
|
506
572
|
}
|
|
507
573
|
/**
|
|
508
|
-
*
|
|
574
|
+
* Fetch table definitions from database
|
|
509
575
|
*/
|
|
510
576
|
async function fetchTableDefinitions(client, spinner, progress, schemas = ['public']) {
|
|
511
577
|
const definitions = [];
|
|
512
578
|
const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
|
|
513
|
-
//
|
|
579
|
+
// Fetch table list
|
|
514
580
|
const tablesResult = await client.query(`
|
|
515
581
|
SELECT tablename, schemaname, 'table' as type
|
|
516
582
|
FROM pg_tables
|
|
517
583
|
WHERE schemaname IN (${schemaPlaceholders})
|
|
518
584
|
ORDER BY schemaname, tablename
|
|
519
585
|
`, schemas);
|
|
520
|
-
//
|
|
586
|
+
// Fetch view list
|
|
521
587
|
const viewsResult = await client.query(`
|
|
522
588
|
SELECT viewname as tablename, schemaname, 'view' as type
|
|
523
589
|
FROM pg_views
|
|
@@ -527,22 +593,22 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
527
593
|
const allObjects = [...tablesResult.rows, ...viewsResult.rows];
|
|
528
594
|
const tableCount = tablesResult.rows.length;
|
|
529
595
|
const viewCount = viewsResult.rows.length;
|
|
530
|
-
//
|
|
596
|
+
// Initialize progress
|
|
531
597
|
if (progress) {
|
|
532
598
|
progress.tables.total = tableCount;
|
|
533
599
|
progress.views.total = viewCount;
|
|
534
600
|
}
|
|
535
|
-
//
|
|
536
|
-
//
|
|
601
|
+
// Process tables/views with limited concurrency (cap connection count)
|
|
602
|
+
// Max configurable via env (default 20, max 50)
|
|
537
603
|
const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
|
|
538
604
|
const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
|
|
539
|
-
//
|
|
605
|
+
// Use env value (capped at minimum 5)
|
|
540
606
|
const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
|
|
541
|
-
//
|
|
607
|
+
// Debug log (development only)
|
|
542
608
|
if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
|
|
543
609
|
console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
|
|
544
610
|
}
|
|
545
|
-
//
|
|
611
|
+
// Promise factory for table/view processing
|
|
546
612
|
const processObject = async (obj, index) => {
|
|
547
613
|
const isTable = obj.type === 'table';
|
|
548
614
|
const name = obj.tablename;
|
|
@@ -552,9 +618,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
552
618
|
let comment = '';
|
|
553
619
|
let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
|
554
620
|
if (type === 'table') {
|
|
555
|
-
//
|
|
621
|
+
// Table case
|
|
556
622
|
try {
|
|
557
|
-
//
|
|
623
|
+
// Get table last updated time
|
|
558
624
|
const tableStatsResult = await client.query(`
|
|
559
625
|
SELECT
|
|
560
626
|
EXTRACT(EPOCH FROM GREATEST(
|
|
@@ -571,11 +637,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
571
637
|
}
|
|
572
638
|
}
|
|
573
639
|
catch (error) {
|
|
574
|
-
//
|
|
640
|
+
// On error use default timestamp
|
|
575
641
|
}
|
|
576
|
-
// CREATE TABLE
|
|
642
|
+
// Generate CREATE TABLE statement
|
|
577
643
|
ddl = await generateCreateTableDDL(client, name, schemaName);
|
|
578
|
-
//
|
|
644
|
+
// Get table comment
|
|
579
645
|
try {
|
|
580
646
|
const tableCommentResult = await client.query(`
|
|
581
647
|
SELECT obj_description(c.oid) as table_comment
|
|
@@ -588,13 +654,13 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
588
654
|
}
|
|
589
655
|
}
|
|
590
656
|
catch (error) {
|
|
591
|
-
//
|
|
657
|
+
// On error no comment
|
|
592
658
|
}
|
|
593
659
|
}
|
|
594
660
|
else {
|
|
595
|
-
//
|
|
661
|
+
// View case
|
|
596
662
|
try {
|
|
597
|
-
//
|
|
663
|
+
// Get view definition and security_invoker setting
|
|
598
664
|
const viewResult = await client.query(`
|
|
599
665
|
SELECT
|
|
600
666
|
pv.definition,
|
|
@@ -610,14 +676,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
610
676
|
`, [schemaName, name]);
|
|
611
677
|
if (viewResult.rows.length > 0) {
|
|
612
678
|
const view = viewResult.rows[0];
|
|
613
|
-
//
|
|
679
|
+
// Get view comment
|
|
614
680
|
const viewCommentResult = await client.query(`
|
|
615
681
|
SELECT obj_description(c.oid) as view_comment
|
|
616
682
|
FROM pg_class c
|
|
617
683
|
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
618
684
|
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
|
|
619
685
|
`, [name, schemaName]);
|
|
620
|
-
//
|
|
686
|
+
// Add view comment at top
|
|
621
687
|
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
622
688
|
comment = viewCommentResult.rows[0].view_comment;
|
|
623
689
|
ddl = `-- ${comment}\n`;
|
|
@@ -625,9 +691,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
625
691
|
else {
|
|
626
692
|
ddl = `-- View: ${name}\n`;
|
|
627
693
|
}
|
|
628
|
-
//
|
|
629
|
-
let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
|
|
630
|
-
// security_invoker
|
|
694
|
+
// Add view definition
|
|
695
|
+
let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
|
|
696
|
+
// Check security_invoker setting
|
|
631
697
|
if (view.reloptions) {
|
|
632
698
|
for (const option of view.reloptions) {
|
|
633
699
|
if (option.startsWith('security_invoker=')) {
|
|
@@ -643,14 +709,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
643
709
|
}
|
|
644
710
|
}
|
|
645
711
|
ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
|
|
646
|
-
// COMMENT ON
|
|
712
|
+
// Add COMMENT ON statement
|
|
647
713
|
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
648
714
|
ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
|
|
649
715
|
}
|
|
650
716
|
else {
|
|
651
717
|
ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
|
|
652
718
|
}
|
|
653
|
-
//
|
|
719
|
+
// Get view creation time (if available)
|
|
654
720
|
try {
|
|
655
721
|
const viewStatsResult = await client.query(`
|
|
656
722
|
SELECT EXTRACT(EPOCH FROM GREATEST(
|
|
@@ -666,41 +732,42 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
666
732
|
}
|
|
667
733
|
}
|
|
668
734
|
catch (error) {
|
|
669
|
-
//
|
|
735
|
+
// On error use default timestamp
|
|
670
736
|
}
|
|
671
737
|
}
|
|
672
738
|
}
|
|
673
739
|
catch (error) {
|
|
674
|
-
//
|
|
740
|
+
// On error no comment
|
|
675
741
|
}
|
|
676
742
|
}
|
|
677
743
|
return {
|
|
678
|
-
name
|
|
744
|
+
name,
|
|
679
745
|
type,
|
|
746
|
+
schema: schemaName,
|
|
680
747
|
ddl,
|
|
681
748
|
timestamp,
|
|
682
749
|
comment: comment || undefined,
|
|
683
750
|
isTable
|
|
684
751
|
};
|
|
685
752
|
};
|
|
686
|
-
//
|
|
753
|
+
// Simple batch concurrency (reliable progress updates)
|
|
687
754
|
const processedResults = [];
|
|
688
755
|
for (let i = 0; i < allObjects.length; i += CONCURRENT_LIMIT) {
|
|
689
756
|
const batch = allObjects.slice(i, i + CONCURRENT_LIMIT);
|
|
690
|
-
//
|
|
757
|
+
// Process batch in parallel
|
|
691
758
|
const batchPromises = batch.map(async (obj, batchIndex) => {
|
|
692
759
|
try {
|
|
693
760
|
const globalIndex = i + batchIndex;
|
|
694
|
-
//
|
|
761
|
+
// Debug: start
|
|
695
762
|
if (process.env.SUPATOOL_DEBUG) {
|
|
696
763
|
console.log(`Starting ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
|
|
697
764
|
}
|
|
698
765
|
const result = await processObject(obj, globalIndex);
|
|
699
|
-
//
|
|
766
|
+
// Debug: done
|
|
700
767
|
if (process.env.SUPATOOL_DEBUG) {
|
|
701
768
|
console.log(`Completed ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
|
|
702
769
|
}
|
|
703
|
-
//
|
|
770
|
+
// Update progress immediately on each completion
|
|
704
771
|
if (result && progress && spinner) {
|
|
705
772
|
if (result.isTable) {
|
|
706
773
|
progress.tables.current = Math.min(progress.tables.current + 1, progress.tables.total);
|
|
@@ -717,11 +784,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
717
784
|
return null;
|
|
718
785
|
}
|
|
719
786
|
});
|
|
720
|
-
//
|
|
787
|
+
// Wait for batch to complete
|
|
721
788
|
const batchResults = await Promise.all(batchPromises);
|
|
722
789
|
processedResults.push(...batchResults);
|
|
723
790
|
}
|
|
724
|
-
//
|
|
791
|
+
// Add to definitions excluding nulls
|
|
725
792
|
for (const result of processedResults) {
|
|
726
793
|
if (result) {
|
|
727
794
|
const { isTable, ...definition } = result;
|
|
@@ -731,12 +798,12 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
731
798
|
return definitions;
|
|
732
799
|
}
|
|
733
800
|
/**
|
|
734
|
-
* CREATE TABLE DDL
|
|
801
|
+
* Generate CREATE TABLE DDL (concurrent version)
|
|
735
802
|
*/
|
|
736
803
|
async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
|
|
737
|
-
//
|
|
804
|
+
// Run all queries in parallel
|
|
738
805
|
const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
|
|
739
|
-
//
|
|
806
|
+
// Get column info
|
|
740
807
|
client.query(`
|
|
741
808
|
SELECT
|
|
742
809
|
c.column_name,
|
|
@@ -754,7 +821,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
754
821
|
AND c.table_name = $2
|
|
755
822
|
ORDER BY c.ordinal_position
|
|
756
823
|
`, [schemaName, tableName]),
|
|
757
|
-
//
|
|
824
|
+
// Get primary key info
|
|
758
825
|
client.query(`
|
|
759
826
|
SELECT column_name
|
|
760
827
|
FROM information_schema.table_constraints tc
|
|
@@ -765,14 +832,14 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
765
832
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
766
833
|
ORDER BY kcu.ordinal_position
|
|
767
834
|
`, [schemaName, tableName]),
|
|
768
|
-
//
|
|
835
|
+
// Get table comment
|
|
769
836
|
client.query(`
|
|
770
837
|
SELECT obj_description(c.oid) as table_comment
|
|
771
838
|
FROM pg_class c
|
|
772
839
|
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
773
840
|
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
|
|
774
841
|
`, [tableName, schemaName]),
|
|
775
|
-
//
|
|
842
|
+
// Get column comments
|
|
776
843
|
client.query(`
|
|
777
844
|
SELECT
|
|
778
845
|
c.column_name,
|
|
@@ -786,7 +853,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
786
853
|
AND pgn.nspname = $1
|
|
787
854
|
ORDER BY c.ordinal_position
|
|
788
855
|
`, [schemaName, tableName]),
|
|
789
|
-
// UNIQUE
|
|
856
|
+
// Get UNIQUE constraints
|
|
790
857
|
client.query(`
|
|
791
858
|
SELECT
|
|
792
859
|
tc.constraint_name,
|
|
@@ -800,7 +867,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
800
867
|
GROUP BY tc.constraint_name
|
|
801
868
|
ORDER BY tc.constraint_name
|
|
802
869
|
`, [schemaName, tableName]),
|
|
803
|
-
// FOREIGN KEY
|
|
870
|
+
// Get FOREIGN KEY constraints
|
|
804
871
|
client.query(`
|
|
805
872
|
SELECT
|
|
806
873
|
tc.constraint_name,
|
|
@@ -826,51 +893,51 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
826
893
|
columnComments.set(row.column_name, row.column_comment);
|
|
827
894
|
}
|
|
828
895
|
});
|
|
829
|
-
//
|
|
896
|
+
// Add table comment at top (with schema name)
|
|
830
897
|
let ddl = '';
|
|
831
|
-
//
|
|
898
|
+
// Add table comment at top
|
|
832
899
|
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
|
833
900
|
ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
|
|
834
901
|
}
|
|
835
902
|
else {
|
|
836
|
-
ddl += `-- Table: ${tableName}\n`;
|
|
903
|
+
ddl += `-- Table: ${schemaName}.${tableName}\n`;
|
|
837
904
|
}
|
|
838
|
-
// CREATE TABLE
|
|
839
|
-
ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
|
|
905
|
+
// Generate CREATE TABLE (schema-qualified)
|
|
906
|
+
ddl += `CREATE TABLE IF NOT EXISTS ${schemaName}.${tableName} (\n`;
|
|
840
907
|
const columnDefs = [];
|
|
841
908
|
for (const col of columnsResult.rows) {
|
|
842
909
|
const rawType = col.full_type ||
|
|
843
910
|
((col.data_type === 'USER-DEFINED' && col.udt_name) ? col.udt_name : col.data_type);
|
|
844
911
|
let colDef = ` ${col.column_name} ${rawType}`;
|
|
845
|
-
//
|
|
912
|
+
// Length spec
|
|
846
913
|
if (col.character_maximum_length) {
|
|
847
914
|
colDef += `(${col.character_maximum_length})`;
|
|
848
915
|
}
|
|
849
|
-
// NOT NULL
|
|
916
|
+
// NOT NULL constraint
|
|
850
917
|
if (col.is_nullable === 'NO') {
|
|
851
918
|
colDef += ' NOT NULL';
|
|
852
919
|
}
|
|
853
|
-
//
|
|
920
|
+
// Default value
|
|
854
921
|
if (col.column_default) {
|
|
855
922
|
colDef += ` DEFAULT ${col.column_default}`;
|
|
856
923
|
}
|
|
857
924
|
columnDefs.push(colDef);
|
|
858
925
|
}
|
|
859
926
|
ddl += columnDefs.join(',\n');
|
|
860
|
-
//
|
|
927
|
+
// Primary key constraint
|
|
861
928
|
if (primaryKeyResult.rows.length > 0) {
|
|
862
929
|
const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
|
|
863
930
|
ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
|
|
864
931
|
}
|
|
865
|
-
// UNIQUE
|
|
932
|
+
// Add UNIQUE constraints inside CREATE TABLE
|
|
866
933
|
for (const unique of uniqueConstraintResult.rows) {
|
|
867
934
|
ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
|
|
868
935
|
}
|
|
869
|
-
// FOREIGN KEY
|
|
936
|
+
// Add FOREIGN KEY constraints inside CREATE TABLE
|
|
870
937
|
for (const fk of foreignKeyResult.rows) {
|
|
871
938
|
ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
|
|
872
939
|
}
|
|
873
|
-
// CHECK
|
|
940
|
+
// Add CHECK constraints inside CREATE TABLE (must be last)
|
|
874
941
|
const checkConstraintResult = await client.query(`
|
|
875
942
|
SELECT
|
|
876
943
|
con.conname as constraint_name,
|
|
@@ -887,16 +954,16 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
887
954
|
ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
|
|
888
955
|
}
|
|
889
956
|
ddl += '\n);\n\n';
|
|
890
|
-
// COMMENT ON
|
|
957
|
+
// Add COMMENT ON statements
|
|
891
958
|
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
|
892
959
|
ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
|
|
893
960
|
}
|
|
894
961
|
else {
|
|
895
962
|
ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
|
|
896
963
|
}
|
|
897
|
-
//
|
|
964
|
+
// Add column comments (with schema name)
|
|
898
965
|
if (columnComments.size > 0) {
|
|
899
|
-
ddl += '\n--
|
|
966
|
+
ddl += '\n-- Column comments\n';
|
|
900
967
|
for (const [columnName, comment] of columnComments) {
|
|
901
968
|
ddl += `COMMENT ON COLUMN ${schemaName}.${tableName}.${columnName} IS '${comment}';\n`;
|
|
902
969
|
}
|
|
@@ -904,271 +971,345 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
904
971
|
return ddl;
|
|
905
972
|
}
|
|
906
973
|
/**
|
|
907
|
-
*
|
|
974
|
+
* Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
|
|
908
975
|
*/
|
|
909
|
-
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true) {
|
|
976
|
+
async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version) {
|
|
910
977
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
911
978
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
912
|
-
|
|
979
|
+
const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
980
|
+
const npmUrl = 'https://www.npmjs.com/package/supatool';
|
|
981
|
+
const headerComment = version
|
|
982
|
+
? `-- Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n`
|
|
983
|
+
: `-- Generated by supatool, ${outputDate} | ${npmUrl}\n`;
|
|
984
|
+
const tables = definitions.filter(d => d.type === 'table');
|
|
985
|
+
const views = definitions.filter(d => d.type === 'view');
|
|
986
|
+
const rlsList = definitions.filter(d => d.type === 'rls');
|
|
987
|
+
const triggersList = definitions.filter(d => d.type === 'trigger');
|
|
988
|
+
const functions = definitions.filter(d => d.type === 'function');
|
|
989
|
+
const cronJobs = definitions.filter(d => d.type === 'cron');
|
|
990
|
+
const customTypes = definitions.filter(d => d.type === 'type');
|
|
991
|
+
// schema.table -> RLS DDL
|
|
992
|
+
const rlsByCategory = new Map();
|
|
993
|
+
for (const r of rlsList) {
|
|
994
|
+
if (r.category)
|
|
995
|
+
rlsByCategory.set(r.category, r.ddl);
|
|
996
|
+
}
|
|
997
|
+
// schema.table -> trigger DDL array
|
|
998
|
+
const triggersByCategory = new Map();
|
|
999
|
+
for (const t of triggersList) {
|
|
1000
|
+
if (!t.category)
|
|
1001
|
+
continue;
|
|
1002
|
+
const list = triggersByCategory.get(t.category) ?? [];
|
|
1003
|
+
list.push(t.ddl);
|
|
1004
|
+
triggersByCategory.set(t.category, list);
|
|
1005
|
+
}
|
|
1006
|
+
// Build merged DDL (table/view + RLS + triggers)
|
|
1007
|
+
const mergeRlsAndTriggers = (def) => {
|
|
1008
|
+
const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
|
|
1009
|
+
let ddl = def.ddl.trimEnd();
|
|
1010
|
+
const rlsDdl = rlsByCategory.get(cat);
|
|
1011
|
+
if (rlsDdl) {
|
|
1012
|
+
ddl += '\n\n' + rlsDdl.trim();
|
|
1013
|
+
}
|
|
1014
|
+
const trgList = triggersByCategory.get(cat);
|
|
1015
|
+
if (trgList && trgList.length > 0) {
|
|
1016
|
+
ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
|
|
1017
|
+
}
|
|
1018
|
+
return ddl.endsWith('\n') ? ddl : ddl + '\n';
|
|
1019
|
+
};
|
|
1020
|
+
const mergedTables = tables.map(t => ({
|
|
1021
|
+
...t,
|
|
1022
|
+
ddl: mergeRlsAndTriggers(t)
|
|
1023
|
+
}));
|
|
1024
|
+
const mergedViews = views.map(v => ({
|
|
1025
|
+
...v,
|
|
1026
|
+
ddl: mergeRlsAndTriggers(v)
|
|
1027
|
+
}));
|
|
1028
|
+
const toWrite = [
|
|
1029
|
+
...mergedTables,
|
|
1030
|
+
...mergedViews,
|
|
1031
|
+
...functions,
|
|
1032
|
+
...cronJobs,
|
|
1033
|
+
...customTypes
|
|
1034
|
+
];
|
|
1035
|
+
const multiSchema = schemas.length > 1;
|
|
1036
|
+
const typeDirNames = {
|
|
1037
|
+
table: 'tables',
|
|
1038
|
+
view: 'views',
|
|
1039
|
+
function: 'rpc',
|
|
1040
|
+
cron: 'cron',
|
|
1041
|
+
type: 'types'
|
|
1042
|
+
};
|
|
913
1043
|
if (!fs.existsSync(outputDir)) {
|
|
914
1044
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
915
1045
|
}
|
|
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 });
|
|
1046
|
+
const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
1047
|
+
for (const def of toWrite) {
|
|
1048
|
+
const typeDir = typeDirNames[def.type];
|
|
1049
|
+
const baseTypeDir = separateDirectories ? typeDir : '.';
|
|
1050
|
+
const targetDir = multiSchema && def.schema
|
|
1051
|
+
? path.join(outputDir, def.schema, baseTypeDir)
|
|
1052
|
+
: path.join(outputDir, baseTypeDir);
|
|
1053
|
+
if (!fs.existsSync(targetDir)) {
|
|
1054
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
940
1055
|
}
|
|
941
|
-
}
|
|
942
|
-
// 並行ファイル書き込み
|
|
943
|
-
const writePromises = definitions.map(async (def) => {
|
|
944
|
-
const targetDir = typeDirectories[def.type];
|
|
945
|
-
// ファイル名を決定(TypeとTriggerを区別しやすくする)
|
|
946
1056
|
let fileName;
|
|
947
1057
|
if (def.type === 'function') {
|
|
948
1058
|
fileName = `fn_${def.name}.sql`;
|
|
949
1059
|
}
|
|
950
|
-
else if (def.type === 'trigger') {
|
|
951
|
-
fileName = `trg_${def.name}.sql`;
|
|
952
|
-
}
|
|
953
1060
|
else {
|
|
954
1061
|
fileName = `${def.name}.sql`;
|
|
955
1062
|
}
|
|
956
1063
|
const filePath = path.join(targetDir, fileName);
|
|
957
|
-
// 最後に改行を追加
|
|
958
1064
|
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);
|
|
1065
|
+
await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
|
|
1066
|
+
}
|
|
1067
|
+
await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version);
|
|
967
1068
|
}
|
|
968
1069
|
/**
|
|
969
|
-
*
|
|
970
|
-
* AIが構造を理解しやすいように1行ずつリストアップ
|
|
1070
|
+
* Generate index file for DB objects (RLS/triggers already merged into table/view)
|
|
971
1071
|
*/
|
|
972
|
-
async function generateIndexFile(definitions, outputDir, separateDirectories = true) {
|
|
1072
|
+
async function generateIndexFile(definitions, outputDir, separateDirectories = true, multiSchema = false, relations = [], rpcTables = [], allSchemas = [], extractedSchemas = [], version) {
|
|
973
1073
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
974
1074
|
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
975
|
-
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1075
|
+
const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
1076
|
+
const npmUrl = 'https://www.npmjs.com/package/supatool';
|
|
1077
|
+
const headerLine = version
|
|
1078
|
+
? `Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n\n`
|
|
1079
|
+
: `Generated by supatool, ${outputDate} | ${npmUrl}\n\n`;
|
|
1080
|
+
const readmeHeader = version
|
|
1081
|
+
? `Generated by [supatool](${npmUrl}) v${version}, ${outputDate}\n\n`
|
|
1082
|
+
: `Generated by [supatool](${npmUrl}), ${outputDate}\n\n`;
|
|
1083
|
+
const typeDirNames = {
|
|
1084
|
+
table: 'tables',
|
|
1085
|
+
view: 'views',
|
|
1086
|
+
function: 'rpc',
|
|
1087
|
+
cron: 'cron',
|
|
1088
|
+
type: 'types'
|
|
984
1089
|
};
|
|
985
1090
|
const typeLabels = {
|
|
986
1091
|
table: 'Tables',
|
|
987
1092
|
view: 'Views',
|
|
988
|
-
rls: 'RLS Policies',
|
|
989
1093
|
function: 'Functions',
|
|
990
|
-
trigger: 'Triggers',
|
|
991
1094
|
cron: 'Cron Jobs',
|
|
992
1095
|
type: 'Custom Types'
|
|
993
1096
|
};
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1097
|
+
const groupedDefs = {
|
|
1098
|
+
table: definitions.filter(def => def.type === 'table'),
|
|
1099
|
+
view: definitions.filter(def => def.type === 'view'),
|
|
1100
|
+
function: definitions.filter(def => def.type === 'function'),
|
|
1101
|
+
cron: definitions.filter(def => def.type === 'cron'),
|
|
1102
|
+
type: definitions.filter(def => def.type === 'type')
|
|
1103
|
+
};
|
|
1104
|
+
// Build relative path per file (schema/type/file when multiSchema)
|
|
1105
|
+
const getRelPath = (def) => {
|
|
1106
|
+
const typeDir = separateDirectories ? (typeDirNames[def.type] ?? def.type) : '.';
|
|
1107
|
+
const fileName = def.type === 'function' ? `fn_${def.name}.sql` : `${def.name}.sql`;
|
|
1108
|
+
if (multiSchema && def.schema) {
|
|
1109
|
+
return `${def.schema}/${typeDir}/${fileName}`;
|
|
1001
1110
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
let fileName;
|
|
1016
|
-
if (def.type === 'function') {
|
|
1017
|
-
fileName = `fn_${def.name}.sql`;
|
|
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
|
-
});
|
|
1111
|
+
return separateDirectories ? `${typeDir}/${fileName}` : fileName;
|
|
1112
|
+
};
|
|
1113
|
+
// === Human-readable README.md (description + link to llms.txt) ===
|
|
1114
|
+
let readmeContent = readmeHeader;
|
|
1115
|
+
readmeContent += '# Schema (extract output)\n\n';
|
|
1116
|
+
readmeContent += 'This folder contains DDL exported by `supatool extract`.\n\n';
|
|
1117
|
+
readmeContent += '- **tables/** – Table definitions (with RLS and triggers in the same file)\n';
|
|
1118
|
+
readmeContent += '- **views/** – View definitions\n';
|
|
1119
|
+
readmeContent += '- **rpc/** – Functions\n';
|
|
1120
|
+
readmeContent += '- **cron/** – Cron jobs\n';
|
|
1121
|
+
readmeContent += '- **types/** – Custom types\n\n';
|
|
1122
|
+
if (multiSchema) {
|
|
1123
|
+
readmeContent += 'When multiple schemas are extracted, each schema has its own subfolder (e.g. `public/tables/`, `agent/views/`).\n\n';
|
|
1044
1124
|
}
|
|
1045
|
-
|
|
1046
|
-
// ===
|
|
1047
|
-
let llmsContent =
|
|
1048
|
-
|
|
1125
|
+
readmeContent += 'Full catalog and relations: [llms.txt](llms.txt)\n';
|
|
1126
|
+
// === llms.txt ===
|
|
1127
|
+
let llmsContent = headerLine;
|
|
1128
|
+
llmsContent += 'Database Schema - Complete Objects Catalog\n';
|
|
1129
|
+
llmsContent += '(Tables/Views include RLS and Triggers in the same file)\n\n';
|
|
1049
1130
|
llmsContent += 'SUMMARY\n';
|
|
1050
1131
|
Object.entries(groupedDefs).forEach(([type, defs]) => {
|
|
1051
|
-
if (defs.length > 0) {
|
|
1132
|
+
if (defs.length > 0 && typeLabels[type]) {
|
|
1052
1133
|
llmsContent += `${typeLabels[type]}: ${defs.length}\n`;
|
|
1053
1134
|
}
|
|
1054
1135
|
});
|
|
1055
|
-
llmsContent += '\n';
|
|
1056
|
-
// Flat list for AI processing (single format)
|
|
1057
|
-
llmsContent += 'OBJECTS\n';
|
|
1136
|
+
llmsContent += '\nOBJECTS\n';
|
|
1058
1137
|
definitions.forEach(def => {
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
let fileName;
|
|
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`;
|
|
1138
|
+
const filePath = getRelPath(def);
|
|
1139
|
+
const commentSuffix = def.comment ? ` # ${def.comment}` : '';
|
|
1140
|
+
const displayName = def.schema ? `${def.schema}.${def.name}` : def.name;
|
|
1141
|
+
llmsContent += `${def.type}:${displayName}:${filePath}${commentSuffix}\n`;
|
|
1076
1142
|
});
|
|
1077
|
-
|
|
1078
|
-
|
|
1143
|
+
if (relations.length > 0) {
|
|
1144
|
+
llmsContent += '\nRELATIONS\n';
|
|
1145
|
+
relations.forEach(r => {
|
|
1146
|
+
llmsContent += `${r.from} -> ${r.to}\n`;
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
if (rpcTables.length > 0) {
|
|
1150
|
+
llmsContent += '\nRPC_TABLES\n';
|
|
1151
|
+
rpcTables.forEach(rt => {
|
|
1152
|
+
llmsContent += `${rt.rpc}: ${rt.tables.join(', ')}\n`;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (allSchemas.length > 0) {
|
|
1156
|
+
const extractedSet = new Set(extractedSchemas);
|
|
1157
|
+
const extractedList = allSchemas.filter(s => extractedSet.has(s));
|
|
1158
|
+
const notExtractedList = allSchemas.filter(s => !extractedSet.has(s));
|
|
1159
|
+
llmsContent += '\nALL_SCHEMAS\n';
|
|
1160
|
+
llmsContent += 'EXTRACTED\n';
|
|
1161
|
+
extractedList.forEach(schemaName => {
|
|
1162
|
+
llmsContent += `${schemaName}\n`;
|
|
1163
|
+
});
|
|
1164
|
+
llmsContent += '\nNOT_EXTRACTED\n';
|
|
1165
|
+
notExtractedList.forEach(schemaName => {
|
|
1166
|
+
llmsContent += `${schemaName}\n`;
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
const readmePath = path.join(outputDir, 'README.md');
|
|
1079
1170
|
const llmsPath = path.join(outputDir, 'llms.txt');
|
|
1080
|
-
fs.writeFileSync(
|
|
1171
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
1081
1172
|
fs.writeFileSync(llmsPath, llmsContent);
|
|
1173
|
+
// schema_index.json (same data for agents that parse JSON)
|
|
1174
|
+
const schemaIndex = {
|
|
1175
|
+
objects: definitions.map(def => ({
|
|
1176
|
+
type: def.type,
|
|
1177
|
+
name: def.schema ? `${def.schema}.${def.name}` : def.name,
|
|
1178
|
+
path: getRelPath(def),
|
|
1179
|
+
...(def.comment && { comment: def.comment })
|
|
1180
|
+
})),
|
|
1181
|
+
relations: relations.map(r => ({ from: r.from, to: r.to })),
|
|
1182
|
+
rpc_tables: rpcTables.map(rt => ({ rpc: rt.rpc, tables: rt.tables })),
|
|
1183
|
+
all_schemas: allSchemas.length > 0
|
|
1184
|
+
? {
|
|
1185
|
+
extracted: allSchemas.filter(s => extractedSchemas.includes(s)),
|
|
1186
|
+
not_extracted: allSchemas.filter(s => !extractedSchemas.includes(s))
|
|
1187
|
+
}
|
|
1188
|
+
: undefined
|
|
1189
|
+
};
|
|
1190
|
+
fs.writeFileSync(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2), 'utf8');
|
|
1191
|
+
// schema_summary.md (one-file overview for AI)
|
|
1192
|
+
let summaryMd = '# Schema summary\n\n';
|
|
1193
|
+
const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
|
|
1194
|
+
if (tableDefs.length > 0) {
|
|
1195
|
+
summaryMd += '## Tables / Views\n';
|
|
1196
|
+
tableDefs.forEach(d => {
|
|
1197
|
+
const name = d.schema ? `${d.schema}.${d.name}` : d.name;
|
|
1198
|
+
summaryMd += d.comment ? `- ${name} (# ${d.comment})\n` : `- ${name}\n`;
|
|
1199
|
+
});
|
|
1200
|
+
summaryMd += '\n';
|
|
1201
|
+
}
|
|
1202
|
+
if (relations.length > 0) {
|
|
1203
|
+
summaryMd += '## Relations\n';
|
|
1204
|
+
relations.forEach(r => {
|
|
1205
|
+
summaryMd += `- ${r.from} -> ${r.to}\n`;
|
|
1206
|
+
});
|
|
1207
|
+
summaryMd += '\n';
|
|
1208
|
+
}
|
|
1209
|
+
if (rpcTables.length > 0) {
|
|
1210
|
+
summaryMd += '## RPC → Tables\n';
|
|
1211
|
+
rpcTables.forEach(rt => {
|
|
1212
|
+
summaryMd += `- ${rt.rpc}: ${rt.tables.join(', ')}\n`;
|
|
1213
|
+
});
|
|
1214
|
+
summaryMd += '\n';
|
|
1215
|
+
}
|
|
1216
|
+
if (allSchemas.length > 0) {
|
|
1217
|
+
const extractedSet = new Set(extractedSchemas);
|
|
1218
|
+
summaryMd += '## Schemas\n';
|
|
1219
|
+
summaryMd += `- Extracted: ${allSchemas.filter(s => extractedSet.has(s)).join(', ') || '(none)'}\n`;
|
|
1220
|
+
summaryMd += `- Not extracted: ${allSchemas.filter(s => !extractedSet.has(s)).join(', ') || '(none)'}\n`;
|
|
1221
|
+
}
|
|
1222
|
+
fs.writeFileSync(path.join(outputDir, 'schema_summary.md'), summaryMd, 'utf8');
|
|
1082
1223
|
}
|
|
1083
1224
|
/**
|
|
1084
|
-
*
|
|
1225
|
+
* Classify and output definitions
|
|
1085
1226
|
*/
|
|
1086
1227
|
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
|
|
1228
|
+
const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
|
|
1229
|
+
// Disable Node.js SSL certificate verification
|
|
1089
1230
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
1090
|
-
//
|
|
1231
|
+
// Connection string validation
|
|
1091
1232
|
if (!connectionString) {
|
|
1092
|
-
throw new Error('
|
|
1093
|
-
'1. --connection
|
|
1094
|
-
'2. SUPABASE_CONNECTION_STRING
|
|
1095
|
-
'3. DATABASE_URL
|
|
1096
|
-
'4. supatool.config.json
|
|
1233
|
+
throw new Error('Connection string is not configured. Please set it using one of:\n' +
|
|
1234
|
+
'1. --connection option\n' +
|
|
1235
|
+
'2. SUPABASE_CONNECTION_STRING environment variable\n' +
|
|
1236
|
+
'3. DATABASE_URL environment variable\n' +
|
|
1237
|
+
'4. supatool.config.json configuration file');
|
|
1097
1238
|
}
|
|
1098
|
-
//
|
|
1239
|
+
// Connection string format validation
|
|
1099
1240
|
if (!connectionString.startsWith('postgresql://') && !connectionString.startsWith('postgres://')) {
|
|
1100
|
-
throw new Error(
|
|
1101
|
-
'
|
|
1241
|
+
throw new Error(`Invalid connection string format: ${connectionString}\n` +
|
|
1242
|
+
'Correct format: postgresql://username:password@host:port/database');
|
|
1102
1243
|
}
|
|
1103
|
-
//
|
|
1244
|
+
// URL encode password part
|
|
1104
1245
|
let encodedConnectionString = connectionString;
|
|
1105
|
-
console.log('🔍
|
|
1246
|
+
console.log('🔍 Original connection string:', connectionString);
|
|
1106
1247
|
try {
|
|
1107
|
-
//
|
|
1248
|
+
// Special handling when password contains @
|
|
1108
1249
|
if (connectionString.includes('@') && connectionString.split('@').length > 2) {
|
|
1109
|
-
console.log('⚠️
|
|
1110
|
-
//
|
|
1250
|
+
console.log('⚠️ Password contains @, executing special handling');
|
|
1251
|
+
// Use last @ as delimiter
|
|
1111
1252
|
const parts = connectionString.split('@');
|
|
1112
|
-
const lastPart = parts.pop(); //
|
|
1113
|
-
const firstParts = parts.join('@'); //
|
|
1114
|
-
console.log('
|
|
1115
|
-
console.log('
|
|
1116
|
-
console.log('
|
|
1117
|
-
//
|
|
1253
|
+
const lastPart = parts.pop(); // Last part (host:port/database)
|
|
1254
|
+
const firstParts = parts.join('@'); // First part (postgresql://user:password)
|
|
1255
|
+
console.log(' Split result:');
|
|
1256
|
+
console.log(' First part:', firstParts);
|
|
1257
|
+
console.log(' Last part:', lastPart);
|
|
1258
|
+
// Encode password part
|
|
1118
1259
|
const colonIndex = firstParts.lastIndexOf(':');
|
|
1119
1260
|
if (colonIndex > 0) {
|
|
1120
1261
|
const protocolAndUser = firstParts.substring(0, colonIndex);
|
|
1121
1262
|
const password = firstParts.substring(colonIndex + 1);
|
|
1122
1263
|
const encodedPassword = encodeURIComponent(password);
|
|
1123
1264
|
encodedConnectionString = `${protocolAndUser}:${encodedPassword}@${lastPart}`;
|
|
1124
|
-
console.log('
|
|
1125
|
-
console.log('
|
|
1126
|
-
console.log('
|
|
1127
|
-
console.log('
|
|
1128
|
-
console.log('
|
|
1265
|
+
console.log(' Encode result:');
|
|
1266
|
+
console.log(' Protocol+User:', protocolAndUser);
|
|
1267
|
+
console.log(' Original password:', password);
|
|
1268
|
+
console.log(' Encoded password:', encodedPassword);
|
|
1269
|
+
console.log(' Final connection string:', encodedConnectionString);
|
|
1129
1270
|
}
|
|
1130
1271
|
}
|
|
1131
1272
|
else {
|
|
1132
|
-
console.log('✅
|
|
1133
|
-
//
|
|
1273
|
+
console.log('✅ Executing normal URL parsing');
|
|
1274
|
+
// Normal URL parsing
|
|
1134
1275
|
const url = new URL(connectionString);
|
|
1135
|
-
//
|
|
1276
|
+
// Handle username containing dots
|
|
1136
1277
|
if (url.username && url.username.includes('.')) {
|
|
1137
|
-
console.log(
|
|
1278
|
+
console.log(`Username (with dots): ${url.username}`);
|
|
1138
1279
|
}
|
|
1139
1280
|
if (url.password) {
|
|
1140
|
-
//
|
|
1281
|
+
// Encode only password part
|
|
1141
1282
|
const encodedPassword = encodeURIComponent(url.password);
|
|
1142
1283
|
url.password = encodedPassword;
|
|
1143
1284
|
encodedConnectionString = url.toString();
|
|
1144
|
-
console.log('
|
|
1285
|
+
console.log(' Password encoded:', encodedPassword);
|
|
1145
1286
|
}
|
|
1146
1287
|
}
|
|
1147
|
-
// Supabase
|
|
1288
|
+
// Add SSL settings for Supabase connection
|
|
1148
1289
|
if (!encodedConnectionString.includes('sslmode=')) {
|
|
1149
1290
|
const separator = encodedConnectionString.includes('?') ? '&' : '?';
|
|
1150
1291
|
encodedConnectionString += `${separator}sslmode=require`;
|
|
1151
|
-
console.log(' SSL
|
|
1292
|
+
console.log(' SSL setting added:', encodedConnectionString);
|
|
1152
1293
|
}
|
|
1153
|
-
//
|
|
1294
|
+
// Display debug info (password hidden)
|
|
1154
1295
|
const debugUrl = new URL(encodedConnectionString);
|
|
1155
1296
|
const maskedPassword = debugUrl.password ? '*'.repeat(debugUrl.password.length) : '';
|
|
1156
1297
|
debugUrl.password = maskedPassword;
|
|
1157
|
-
console.log('🔍
|
|
1158
|
-
console.log(`
|
|
1159
|
-
console.log(`
|
|
1160
|
-
console.log(`
|
|
1161
|
-
console.log(`
|
|
1298
|
+
console.log('🔍 Connection info:');
|
|
1299
|
+
console.log(` Host: ${debugUrl.hostname}`);
|
|
1300
|
+
console.log(` Port: ${debugUrl.port}`);
|
|
1301
|
+
console.log(` Database: ${debugUrl.pathname.slice(1)}`);
|
|
1302
|
+
console.log(` User: ${debugUrl.username}`);
|
|
1162
1303
|
console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
|
|
1163
1304
|
}
|
|
1164
1305
|
catch (error) {
|
|
1165
|
-
// URL
|
|
1166
|
-
console.warn('
|
|
1167
|
-
console.warn('
|
|
1306
|
+
// Use original string if URL parsing fails
|
|
1307
|
+
console.warn('Failed to parse connection string URL. May contain special characters.');
|
|
1308
|
+
console.warn('Error details:', error instanceof Error ? error.message : String(error));
|
|
1168
1309
|
}
|
|
1169
1310
|
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1170
1311
|
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
|
|
1171
|
-
//
|
|
1312
|
+
// Overwrite confirmation
|
|
1172
1313
|
if (!force && fs.existsSync(outputDir)) {
|
|
1173
1314
|
const files = fs.readdirSync(outputDir);
|
|
1174
1315
|
if (files.length > 0) {
|
|
@@ -1186,7 +1327,7 @@ async function extractDefinitions(options) {
|
|
|
1186
1327
|
}
|
|
1187
1328
|
}
|
|
1188
1329
|
}
|
|
1189
|
-
//
|
|
1330
|
+
// Dynamic import for spinner
|
|
1190
1331
|
const { default: ora } = await Promise.resolve().then(() => __importStar(require('ora')));
|
|
1191
1332
|
const spinner = ora('Connecting to database...').start();
|
|
1192
1333
|
const client = new pg_1.Client({
|
|
@@ -1197,14 +1338,14 @@ async function extractDefinitions(options) {
|
|
|
1197
1338
|
}
|
|
1198
1339
|
});
|
|
1199
1340
|
try {
|
|
1200
|
-
//
|
|
1201
|
-
console.log('🔧
|
|
1341
|
+
// Debug before connect
|
|
1342
|
+
console.log('🔧 Connection settings:');
|
|
1202
1343
|
console.log(` SSL: rejectUnauthorized=false`);
|
|
1203
|
-
console.log(`
|
|
1344
|
+
console.log(` Connection string length: ${encodedConnectionString.length}`);
|
|
1204
1345
|
await client.connect();
|
|
1205
1346
|
spinner.text = 'Connected to database';
|
|
1206
1347
|
let allDefinitions = [];
|
|
1207
|
-
//
|
|
1348
|
+
// Initialize progress tracker
|
|
1208
1349
|
const progress = {
|
|
1209
1350
|
tables: { current: 0, total: 0 },
|
|
1210
1351
|
views: { current: 0, total: 0 },
|
|
@@ -1215,14 +1356,14 @@ async function extractDefinitions(options) {
|
|
|
1215
1356
|
customTypes: { current: 0, total: 0 }
|
|
1216
1357
|
};
|
|
1217
1358
|
if (all) {
|
|
1218
|
-
//
|
|
1359
|
+
// Get total count for each object type first
|
|
1219
1360
|
spinner.text = 'Counting database objects...';
|
|
1220
|
-
//
|
|
1361
|
+
// Get total tables/views count
|
|
1221
1362
|
const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
|
|
1222
1363
|
const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
|
|
1223
1364
|
progress.tables.total = parseInt(tablesCountResult.rows[0].count);
|
|
1224
1365
|
progress.views.total = parseInt(viewsCountResult.rows[0].count);
|
|
1225
|
-
// RLS
|
|
1366
|
+
// Get total RLS policy count (per table)
|
|
1226
1367
|
try {
|
|
1227
1368
|
const rlsCountResult = await client.query(`
|
|
1228
1369
|
SELECT COUNT(DISTINCT tablename) as count
|
|
@@ -1234,7 +1375,7 @@ async function extractDefinitions(options) {
|
|
|
1234
1375
|
catch (error) {
|
|
1235
1376
|
progress.rls.total = 0;
|
|
1236
1377
|
}
|
|
1237
|
-
//
|
|
1378
|
+
// Get total functions count
|
|
1238
1379
|
const functionsCountResult = await client.query(`
|
|
1239
1380
|
SELECT COUNT(*) as count
|
|
1240
1381
|
FROM pg_proc p
|
|
@@ -1242,7 +1383,7 @@ async function extractDefinitions(options) {
|
|
|
1242
1383
|
WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p')
|
|
1243
1384
|
`);
|
|
1244
1385
|
progress.functions.total = parseInt(functionsCountResult.rows[0].count);
|
|
1245
|
-
//
|
|
1386
|
+
// Get total triggers count
|
|
1246
1387
|
const triggersCountResult = await client.query(`
|
|
1247
1388
|
SELECT COUNT(*) as count
|
|
1248
1389
|
FROM pg_trigger t
|
|
@@ -1251,7 +1392,7 @@ async function extractDefinitions(options) {
|
|
|
1251
1392
|
WHERE n.nspname = 'public' AND NOT t.tgisinternal
|
|
1252
1393
|
`);
|
|
1253
1394
|
progress.triggers.total = parseInt(triggersCountResult.rows[0].count);
|
|
1254
|
-
// Cron
|
|
1395
|
+
// Get total Cron jobs count
|
|
1255
1396
|
try {
|
|
1256
1397
|
const cronCountResult = await client.query('SELECT COUNT(*) as count FROM cron.job');
|
|
1257
1398
|
progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
|
|
@@ -1259,7 +1400,7 @@ async function extractDefinitions(options) {
|
|
|
1259
1400
|
catch (error) {
|
|
1260
1401
|
progress.cronJobs.total = 0;
|
|
1261
1402
|
}
|
|
1262
|
-
//
|
|
1403
|
+
// Get total custom types count
|
|
1263
1404
|
const typesCountResult = await client.query(`
|
|
1264
1405
|
SELECT COUNT(*) as count
|
|
1265
1406
|
FROM pg_type t
|
|
@@ -1276,7 +1417,7 @@ async function extractDefinitions(options) {
|
|
|
1276
1417
|
AND t.typname NOT LIKE '_%'
|
|
1277
1418
|
`);
|
|
1278
1419
|
progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
|
|
1279
|
-
// --all
|
|
1420
|
+
// When --all: fetch all objects (sequential)
|
|
1280
1421
|
const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
|
|
1281
1422
|
const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
|
|
1282
1423
|
const functions = await fetchFunctions(client, spinner, progress, schemas);
|
|
@@ -1293,8 +1434,8 @@ async function extractDefinitions(options) {
|
|
|
1293
1434
|
];
|
|
1294
1435
|
}
|
|
1295
1436
|
else {
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1437
|
+
// Legacy path (tables/views only)
|
|
1438
|
+
// Get total tables/views count
|
|
1298
1439
|
const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
|
|
1299
1440
|
const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
|
|
1300
1441
|
progress.tables.total = parseInt(tablesCountResult.rows[0].count);
|
|
@@ -1310,15 +1451,42 @@ async function extractDefinitions(options) {
|
|
|
1310
1451
|
allDefinitions = definitions;
|
|
1311
1452
|
}
|
|
1312
1453
|
}
|
|
1313
|
-
//
|
|
1454
|
+
// Pattern matching
|
|
1314
1455
|
if (tablePattern !== '*') {
|
|
1315
1456
|
const regex = new RegExp(tablePattern.replace(/\*/g, '.*'));
|
|
1316
1457
|
allDefinitions = allDefinitions.filter(def => regex.test(def.name));
|
|
1317
1458
|
}
|
|
1318
|
-
//
|
|
1459
|
+
// Fetch for RELATIONS / RPC_TABLES (llms.txt upgrade)
|
|
1460
|
+
let relations = [];
|
|
1461
|
+
let rpcTables = [];
|
|
1462
|
+
let allSchemas = [];
|
|
1463
|
+
try {
|
|
1464
|
+
allSchemas = await fetchAllSchemas(client);
|
|
1465
|
+
relations = await fetchRelationList(client, schemas);
|
|
1466
|
+
const funcDefs = allDefinitions.filter(d => d.type === 'function');
|
|
1467
|
+
for (const f of funcDefs) {
|
|
1468
|
+
const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
|
|
1469
|
+
if (tables.length > 0) {
|
|
1470
|
+
rpcTables.push({
|
|
1471
|
+
rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
|
|
1472
|
+
tables
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
catch (err) {
|
|
1478
|
+
if (process.env.SUPATOOL_DEBUG) {
|
|
1479
|
+
console.warn('RELATIONS/RPC_TABLES extraction skipped:', err);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
// When force: remove output dir then write (so removed tables don't leave files)
|
|
1483
|
+
if (force && fs.existsSync(outputDir)) {
|
|
1484
|
+
fs.rmSync(outputDir, { recursive: true });
|
|
1485
|
+
}
|
|
1486
|
+
// Save definitions (table+RLS+triggers merged, schema folders)
|
|
1319
1487
|
spinner.text = 'Saving definitions to files...';
|
|
1320
|
-
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories);
|
|
1321
|
-
//
|
|
1488
|
+
await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version);
|
|
1489
|
+
// Show stats
|
|
1322
1490
|
const counts = {
|
|
1323
1491
|
table: allDefinitions.filter(def => def.type === 'table').length,
|
|
1324
1492
|
view: allDefinitions.filter(def => def.type === 'view').length,
|
|
@@ -1328,7 +1496,7 @@ async function extractDefinitions(options) {
|
|
|
1328
1496
|
cron: allDefinitions.filter(def => def.type === 'cron').length,
|
|
1329
1497
|
type: allDefinitions.filter(def => def.type === 'type').length
|
|
1330
1498
|
};
|
|
1331
|
-
//
|
|
1499
|
+
// Stop progress display
|
|
1332
1500
|
stopProgressDisplay();
|
|
1333
1501
|
spinner.succeed(`Extraction completed: ${outputDir}`);
|
|
1334
1502
|
if (counts.table > 0)
|
|
@@ -1348,7 +1516,7 @@ async function extractDefinitions(options) {
|
|
|
1348
1516
|
console.log('');
|
|
1349
1517
|
}
|
|
1350
1518
|
catch (error) {
|
|
1351
|
-
//
|
|
1519
|
+
// Stop progress display (on error)
|
|
1352
1520
|
stopProgressDisplay();
|
|
1353
1521
|
spinner.fail('Extraction failed');
|
|
1354
1522
|
console.error('Error:', error);
|
|
@@ -1359,7 +1527,7 @@ async function extractDefinitions(options) {
|
|
|
1359
1527
|
await client.end();
|
|
1360
1528
|
}
|
|
1361
1529
|
catch (closeError) {
|
|
1362
|
-
//
|
|
1530
|
+
// Ignore DB connection close errors (e.g. already disconnected)
|
|
1363
1531
|
}
|
|
1364
1532
|
}
|
|
1365
1533
|
}
|