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
|
@@ -40,63 +40,63 @@ const util_1 = require("util");
|
|
|
40
40
|
const definitionExtractor_1 = require("./definitionExtractor");
|
|
41
41
|
const dnsLookup = (0, util_1.promisify)(dns.lookup);
|
|
42
42
|
/**
|
|
43
|
-
* DDL
|
|
43
|
+
* Normalize DDL string (unify whitespace, newlines, tabs)
|
|
44
44
|
*/
|
|
45
45
|
function normalizeDDL(ddl) {
|
|
46
46
|
return ddl
|
|
47
|
-
.replace(/\s+/g, ' ') //
|
|
48
|
-
.replace(/;\s+/g, ';\n') //
|
|
49
|
-
.trim(); //
|
|
47
|
+
.replace(/\s+/g, ' ') // collapse consecutive whitespace to single space
|
|
48
|
+
.replace(/;\s+/g, ';\n') // newline after semicolon
|
|
49
|
+
.trim(); // trim leading/trailing
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* Fetch schema from remote Supabase
|
|
53
53
|
*/
|
|
54
|
-
async function fetchRemoteSchemas(connectionString) {
|
|
54
|
+
async function fetchRemoteSchemas(connectionString, targetTableNames) {
|
|
55
55
|
const schemas = {};
|
|
56
|
-
// console.log('
|
|
56
|
+
// console.log('Connecting to database...');
|
|
57
57
|
try {
|
|
58
|
-
//
|
|
58
|
+
// Basic connection string check
|
|
59
59
|
if (!connectionString || !connectionString.startsWith('postgresql://')) {
|
|
60
|
-
throw new Error('
|
|
60
|
+
throw new Error('Invalid connection string. Please specify a valid postgresql:// format.');
|
|
61
61
|
}
|
|
62
|
-
// URL
|
|
62
|
+
// Parse URL and display connection info
|
|
63
63
|
const url = new URL(connectionString);
|
|
64
|
-
console.log(
|
|
65
|
-
console.log(
|
|
66
|
-
//
|
|
64
|
+
console.log(`Target: ${url.hostname}:${url.port}`);
|
|
65
|
+
console.log(`User: ${url.username}`);
|
|
66
|
+
// Handle special characters in password
|
|
67
67
|
const decodedPassword = decodeURIComponent(url.password || '');
|
|
68
|
-
//
|
|
68
|
+
// Rebuild connection string with proper password encoding
|
|
69
69
|
const encodedPassword = encodeURIComponent(decodedPassword);
|
|
70
|
-
//
|
|
70
|
+
// Explicit session pooler mode
|
|
71
71
|
let reconstructedConnectionString = `postgresql://${url.username}:${encodedPassword}@${url.hostname}:${url.port}${url.pathname}`;
|
|
72
|
-
//
|
|
72
|
+
// Add session pooling params
|
|
73
73
|
const searchParams = new URLSearchParams(url.search);
|
|
74
74
|
searchParams.set('sslmode', 'require');
|
|
75
75
|
searchParams.set('application_name', 'supatool');
|
|
76
76
|
reconstructedConnectionString += `?${searchParams.toString()}`;
|
|
77
|
-
// IPv4
|
|
77
|
+
// Prefer IPv4
|
|
78
78
|
dns.setDefaultResultOrder('ipv4first');
|
|
79
|
-
// DNS
|
|
79
|
+
// DNS resolution test
|
|
80
80
|
try {
|
|
81
81
|
const addresses = await dns.promises.lookup(url.hostname, { all: true });
|
|
82
82
|
}
|
|
83
83
|
catch (dnsError) {
|
|
84
|
-
console.error('❌ DNS
|
|
84
|
+
console.error('❌ DNS resolution failed');
|
|
85
85
|
throw dnsError;
|
|
86
86
|
}
|
|
87
|
-
// Session pooler
|
|
87
|
+
// Session pooler config
|
|
88
88
|
const clientConfig = {
|
|
89
89
|
connectionString: reconstructedConnectionString,
|
|
90
90
|
ssl: {
|
|
91
91
|
rejectUnauthorized: false
|
|
92
92
|
},
|
|
93
|
-
// Session pooler
|
|
93
|
+
// Session pooler extra options
|
|
94
94
|
statement_timeout: 30000,
|
|
95
95
|
query_timeout: 30000,
|
|
96
96
|
connectionTimeoutMillis: 10000,
|
|
97
97
|
idleTimeoutMillis: 10000
|
|
98
98
|
};
|
|
99
|
-
console.log('
|
|
99
|
+
console.log('Connecting via session pooler...');
|
|
100
100
|
let client;
|
|
101
101
|
try {
|
|
102
102
|
client = new pg_1.Client(clientConfig);
|
|
@@ -107,8 +107,8 @@ async function fetchRemoteSchemas(connectionString) {
|
|
|
107
107
|
sslError.message.includes('SCRAM') ||
|
|
108
108
|
sslError.message.includes('certificate') ||
|
|
109
109
|
sslError.message.includes('SELF_SIGNED_CERT'))) {
|
|
110
|
-
console.log('SSL
|
|
111
|
-
// SSL
|
|
110
|
+
console.log('SSL connection failed, retrying with SSL disabled...');
|
|
111
|
+
// Retry with SSL disabled
|
|
112
112
|
const noSslConnectionString = reconstructedConnectionString.replace('sslmode=require', 'sslmode=disable');
|
|
113
113
|
const noSslConfig = {
|
|
114
114
|
...clientConfig,
|
|
@@ -118,12 +118,12 @@ async function fetchRemoteSchemas(connectionString) {
|
|
|
118
118
|
try {
|
|
119
119
|
client = new pg_1.Client(noSslConfig);
|
|
120
120
|
await client.connect();
|
|
121
|
-
console.log('SSL
|
|
121
|
+
console.log('Connection successful with SSL disabled');
|
|
122
122
|
}
|
|
123
123
|
catch (noSslError) {
|
|
124
124
|
if (noSslError instanceof Error && (noSslError.message.includes('SASL') || noSslError.message.includes('SCRAM'))) {
|
|
125
|
-
console.log('
|
|
126
|
-
//
|
|
125
|
+
console.log('SASL error continues with session pooler. Retrying with direct connection...');
|
|
126
|
+
// Retry with direct connection (port 5432)
|
|
127
127
|
const directConnectionString = noSslConnectionString.replace('pooler.supabase.com:5432', 'supabase.co:5432');
|
|
128
128
|
const directConfig = {
|
|
129
129
|
...noSslConfig,
|
|
@@ -131,7 +131,7 @@ async function fetchRemoteSchemas(connectionString) {
|
|
|
131
131
|
};
|
|
132
132
|
client = new pg_1.Client(directConfig);
|
|
133
133
|
await client.connect();
|
|
134
|
-
console.log('Direct connection
|
|
134
|
+
console.log('Direct connection successful');
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
137
|
throw noSslError;
|
|
@@ -142,29 +142,45 @@ async function fetchRemoteSchemas(connectionString) {
|
|
|
142
142
|
throw sslError;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
-
//
|
|
145
|
+
// Connection test query
|
|
146
146
|
const testResult = await client.query('SELECT version()');
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
// Fetch table list
|
|
148
|
+
let tablesResult;
|
|
149
|
+
if (targetTableNames && targetTableNames.length > 0) {
|
|
150
|
+
// When targeting specific tables only
|
|
151
|
+
const placeholders = targetTableNames.map((_, index) => `$${index + 1}`).join(',');
|
|
152
|
+
tablesResult = await client.query(`
|
|
153
|
+
SELECT tablename
|
|
154
|
+
FROM pg_tables
|
|
155
|
+
WHERE schemaname = 'public'
|
|
156
|
+
AND tablename IN (${placeholders})
|
|
157
|
+
ORDER BY tablename
|
|
158
|
+
`, targetTableNames);
|
|
159
|
+
console.log(`Remote tables (filtered): ${tablesResult.rows.length}/${targetTableNames.length}`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// All tables
|
|
163
|
+
tablesResult = await client.query(`
|
|
164
|
+
SELECT tablename
|
|
165
|
+
FROM pg_tables
|
|
166
|
+
WHERE schemaname = 'public'
|
|
167
|
+
ORDER BY tablename
|
|
168
|
+
`);
|
|
169
|
+
console.log(`Remote tables: ${tablesResult.rows.length}`);
|
|
170
|
+
}
|
|
171
|
+
// For loading animation
|
|
156
172
|
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
157
173
|
let spinnerIndex = 0;
|
|
158
174
|
const startTime = Date.now();
|
|
159
175
|
const spinnerInterval = setInterval(() => {
|
|
160
176
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
161
|
-
process.stdout.write(`\r${spinner[spinnerIndex]}
|
|
177
|
+
process.stdout.write(`\r${spinner[spinnerIndex]} Fetching schemas... ${elapsed}s`);
|
|
162
178
|
spinnerIndex = (spinnerIndex + 1) % spinner.length;
|
|
163
179
|
}, 100);
|
|
164
|
-
//
|
|
180
|
+
// Fetch schema info for each table
|
|
165
181
|
for (const row of tablesResult.rows) {
|
|
166
182
|
const tableName = row.tablename;
|
|
167
|
-
//
|
|
183
|
+
// Get table last updated time (reuse existing logic)
|
|
168
184
|
let timestamp = Math.floor(Date.now() / 1000);
|
|
169
185
|
try {
|
|
170
186
|
const tableStatsResult = await client.query(`
|
|
@@ -188,14 +204,14 @@ async function fetchRemoteSchemas(connectionString) {
|
|
|
188
204
|
catch {
|
|
189
205
|
timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
|
190
206
|
}
|
|
191
|
-
// DDL
|
|
207
|
+
// DDL generation unified with definitionExtractor
|
|
192
208
|
const ddl = await (0, definitionExtractor_1.generateCreateTableDDL)(client, tableName, 'public');
|
|
193
209
|
schemas[tableName] = {
|
|
194
210
|
ddl,
|
|
195
211
|
timestamp
|
|
196
212
|
};
|
|
197
213
|
}
|
|
198
|
-
//
|
|
214
|
+
// Stop loading animation
|
|
199
215
|
clearInterval(spinnerInterval);
|
|
200
216
|
const totalTime = Math.floor((Date.now() - startTime) / 1000);
|
|
201
217
|
process.stdout.write(`\rSchema fetch completed (${totalTime}s) \n`);
|
|
@@ -38,22 +38,22 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const diff_1 = require("diff");
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* Generate ALTER TABLE statements from DDL diff
|
|
42
42
|
*/
|
|
43
43
|
function generateAlterStatements(tableName, fromDdl, toDdl) {
|
|
44
44
|
const statements = [];
|
|
45
45
|
const diff = (0, diff_1.diffLines)(fromDdl, toDdl);
|
|
46
|
-
//
|
|
47
|
-
//
|
|
46
|
+
// Debug output removed - simple display
|
|
47
|
+
// For column rename detection
|
|
48
48
|
const removedColumns = [];
|
|
49
49
|
const addedColumns = [];
|
|
50
50
|
for (const part of diff) {
|
|
51
51
|
if (part.added) {
|
|
52
|
-
//
|
|
52
|
+
// Convert added lines to ALTER TABLE
|
|
53
53
|
const lines = part.value.split('\n').filter(line => line.trim());
|
|
54
54
|
for (const line of lines) {
|
|
55
55
|
const trimmed = line.trim();
|
|
56
|
-
//
|
|
56
|
+
// Detect column definition addition
|
|
57
57
|
if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
|
|
58
58
|
const columnDef = trimmed.replace(/,$/, '').trim();
|
|
59
59
|
const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
|
|
@@ -66,11 +66,11 @@ function generateAlterStatements(tableName, fromDdl, toDdl) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
else if (part.removed) {
|
|
69
|
-
//
|
|
69
|
+
// Convert removed lines to ALTER TABLE
|
|
70
70
|
const lines = part.value.split('\n').filter(line => line.trim());
|
|
71
71
|
for (const line of lines) {
|
|
72
72
|
const trimmed = line.trim();
|
|
73
|
-
//
|
|
73
|
+
// Detect column definition removal
|
|
74
74
|
if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
|
|
75
75
|
const columnDef = trimmed.replace(/,$/, '').trim();
|
|
76
76
|
const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
|
|
@@ -83,47 +83,47 @@ function generateAlterStatements(tableName, fromDdl, toDdl) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
//
|
|
86
|
+
// Column rename detection (same type, different name)
|
|
87
87
|
const renames = [];
|
|
88
88
|
for (const removed of removedColumns) {
|
|
89
89
|
const matching = addedColumns.find(added => added.definition === removed.definition);
|
|
90
90
|
if (matching) {
|
|
91
91
|
renames.push({ from: removed.name, to: matching.name });
|
|
92
|
-
//
|
|
92
|
+
// Mark as processed
|
|
93
93
|
removedColumns.splice(removedColumns.indexOf(removed), 1);
|
|
94
94
|
addedColumns.splice(addedColumns.indexOf(matching), 1);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
//
|
|
98
|
-
// RENAME COLUMN
|
|
97
|
+
// Debug log removed - simple display
|
|
98
|
+
// Generate RENAME COLUMN statements
|
|
99
99
|
for (const rename of renames) {
|
|
100
100
|
statements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
|
|
101
101
|
}
|
|
102
|
-
//
|
|
102
|
+
// Remaining removed columns
|
|
103
103
|
for (const removed of removedColumns) {
|
|
104
104
|
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${removed.name};`);
|
|
105
105
|
}
|
|
106
|
-
//
|
|
106
|
+
// Remaining added columns
|
|
107
107
|
for (const added of addedColumns) {
|
|
108
108
|
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${added.name} ${added.definition};`);
|
|
109
109
|
}
|
|
110
110
|
return statements;
|
|
111
111
|
}
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
113
|
+
* Generate migration file
|
|
114
114
|
*/
|
|
115
115
|
async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.') {
|
|
116
116
|
const migrationDir = path.join(projectDir, 'supabase', 'migrations');
|
|
117
|
-
// migrations
|
|
117
|
+
// Create migrations directory
|
|
118
118
|
if (!fs.existsSync(migrationDir)) {
|
|
119
119
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
120
120
|
}
|
|
121
|
-
// ALTER TABLE
|
|
121
|
+
// Generate ALTER TABLE statements
|
|
122
122
|
const alterStatements = generateAlterStatements(tableName, fromDdl, toDdl);
|
|
123
123
|
if (alterStatements.length === 0) {
|
|
124
124
|
return await generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir);
|
|
125
125
|
}
|
|
126
|
-
//
|
|
126
|
+
// Generate filename from timestamp
|
|
127
127
|
const now = new Date();
|
|
128
128
|
const timestamp = now.toISOString()
|
|
129
129
|
.replace(/[-:]/g, '')
|
|
@@ -131,37 +131,37 @@ async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.'
|
|
|
131
131
|
.replace('T', '');
|
|
132
132
|
const filename = `${timestamp}_update_${tableName}.sql`;
|
|
133
133
|
const filepath = path.join(migrationDir, filename);
|
|
134
|
-
//
|
|
134
|
+
// Build migration file content
|
|
135
135
|
const content = `-- Migration generated by supatool
|
|
136
136
|
-- Table: ${tableName}
|
|
137
137
|
-- Generated at: ${now.toISOString()}
|
|
138
138
|
|
|
139
139
|
${alterStatements.join('\n')}
|
|
140
140
|
`;
|
|
141
|
-
//
|
|
141
|
+
// Write file
|
|
142
142
|
fs.writeFileSync(filepath, content, 'utf-8');
|
|
143
|
-
console.log(
|
|
143
|
+
console.log(`Migration file generated: ${filename}`);
|
|
144
144
|
return filepath;
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* DDL
|
|
147
|
+
* Extract column definitions from DDL
|
|
148
148
|
*/
|
|
149
149
|
function extractColumns(ddl) {
|
|
150
150
|
const columns = [];
|
|
151
|
-
// CREATE TABLE
|
|
151
|
+
// Extract CREATE TABLE part
|
|
152
152
|
const createTableMatch = ddl.match(/CREATE TABLE[^(]*\((.*)\);?/is);
|
|
153
153
|
if (!createTableMatch) {
|
|
154
154
|
return columns;
|
|
155
155
|
}
|
|
156
156
|
const tableContent = createTableMatch[1];
|
|
157
|
-
//
|
|
157
|
+
// Split column definitions and CONSTRAINTs
|
|
158
158
|
const parts = tableContent.split(',').map(part => part.trim());
|
|
159
159
|
for (const part of parts) {
|
|
160
|
-
// CONSTRAINT
|
|
160
|
+
// Exclude CONSTRAINT, PRIMARY KEY
|
|
161
161
|
if (part.match(/^(PRIMARY|CONSTRAINT|UNIQUE|FOREIGN|CHECK)/i)) {
|
|
162
162
|
continue;
|
|
163
163
|
}
|
|
164
|
-
//
|
|
164
|
+
// Split column name and data type
|
|
165
165
|
const columnMatch = part.trim().match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$/);
|
|
166
166
|
if (columnMatch) {
|
|
167
167
|
const columnName = columnMatch[1];
|
|
@@ -172,29 +172,29 @@ function extractColumns(ddl) {
|
|
|
172
172
|
return columns;
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
*
|
|
175
|
+
* Analyze column changes from diff
|
|
176
176
|
*/
|
|
177
177
|
function analyzeDiffForTemplate(fromDdl, toDdl) {
|
|
178
|
-
//
|
|
178
|
+
// Extract columns from each DDL
|
|
179
179
|
const fromColumns = extractColumns(fromDdl);
|
|
180
180
|
const toColumns = extractColumns(toDdl);
|
|
181
|
-
//
|
|
181
|
+
// Removed columns (in FROM but not in TO)
|
|
182
182
|
const removedColumns = fromColumns.filter(fromCol => !toColumns.some(toCol => toCol.name === fromCol.name));
|
|
183
|
-
//
|
|
183
|
+
// Added columns (in TO but not in FROM)
|
|
184
184
|
const addedColumns = toColumns.filter(toCol => !fromColumns.some(fromCol => fromCol.name === toCol.name));
|
|
185
185
|
return { removedColumns, addedColumns };
|
|
186
186
|
}
|
|
187
187
|
/**
|
|
188
|
-
*
|
|
188
|
+
* Generate manual migration template
|
|
189
189
|
*/
|
|
190
190
|
async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir) {
|
|
191
191
|
const migrationDir = path.join(projectDir, 'supabase', 'migrations');
|
|
192
192
|
if (!fs.existsSync(migrationDir)) {
|
|
193
193
|
fs.mkdirSync(migrationDir, { recursive: true });
|
|
194
194
|
}
|
|
195
|
-
//
|
|
195
|
+
// Analyze diff
|
|
196
196
|
const { removedColumns, addedColumns } = analyzeDiffForTemplate(fromDdl, toDdl);
|
|
197
|
-
//
|
|
197
|
+
// Generate filename from timestamp
|
|
198
198
|
const now = new Date();
|
|
199
199
|
const timestamp = now.toISOString()
|
|
200
200
|
.replace(/[-:]/g, '')
|
|
@@ -202,9 +202,9 @@ async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projec
|
|
|
202
202
|
.replace('T', '');
|
|
203
203
|
const filename = `${timestamp}_manual_update_${tableName}.sql`;
|
|
204
204
|
const filepath = path.join(migrationDir, filename);
|
|
205
|
-
//
|
|
205
|
+
// Build template from actual changes
|
|
206
206
|
let migrationStatements = [];
|
|
207
|
-
//
|
|
207
|
+
// Detect possible column renames
|
|
208
208
|
const potentialRenames = [];
|
|
209
209
|
for (const removed of removedColumns) {
|
|
210
210
|
const matching = addedColumns.find(added => added.definition === removed.definition);
|
|
@@ -216,10 +216,10 @@ async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projec
|
|
|
216
216
|
});
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
|
-
//
|
|
219
|
+
// Remaining removals and additions
|
|
220
220
|
const remainingRemoved = removedColumns.filter(r => !potentialRenames.some(p => p.from === r.name));
|
|
221
221
|
const remainingAdded = addedColumns.filter(a => !potentialRenames.some(p => p.to === a.name));
|
|
222
|
-
//
|
|
222
|
+
// Generate template statements (DROP -> ADD -> RENAME order)
|
|
223
223
|
if (remainingRemoved.length > 0) {
|
|
224
224
|
migrationStatements.push('-- Column removals:');
|
|
225
225
|
for (const removed of remainingRemoved) {
|
|
@@ -242,7 +242,7 @@ async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projec
|
|
|
242
242
|
migrationStatements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
-
//
|
|
245
|
+
// Fallback when nothing detected
|
|
246
246
|
if (migrationStatements.length === 0) {
|
|
247
247
|
migrationStatements = [
|
|
248
248
|
`-- Manual migration for ${tableName}`,
|
|
@@ -259,18 +259,18 @@ async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projec
|
|
|
259
259
|
${migrationStatements.join('\n')}
|
|
260
260
|
`;
|
|
261
261
|
fs.writeFileSync(filepath, content, 'utf-8');
|
|
262
|
-
console.log(
|
|
262
|
+
console.log(`Manual migration template generated: ${filename}`);
|
|
263
263
|
return filepath;
|
|
264
264
|
}
|
|
265
265
|
/**
|
|
266
|
-
*
|
|
266
|
+
* Advanced diff analysis (detect column changes)
|
|
267
267
|
*/
|
|
268
268
|
function analyzeColumnChanges(tableName, localDdl, remoteDdl) {
|
|
269
269
|
const statements = [];
|
|
270
|
-
//
|
|
270
|
+
// Simple case: detect column name change
|
|
271
271
|
const localLines = localDdl.split('\n').map(line => line.trim()).filter(line => line);
|
|
272
272
|
const remoteLines = remoteDdl.split('\n').map(line => line.trim()).filter(line => line);
|
|
273
|
-
//
|
|
274
|
-
//
|
|
273
|
+
// Advanced detection (type change, default change) to be implemented later
|
|
274
|
+
// Currently only basic add/remove
|
|
275
275
|
return statements;
|
|
276
276
|
}
|
|
@@ -37,16 +37,16 @@ exports.parseLocalSchemas = parseLocalSchemas;
|
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
/**
|
|
40
|
-
* DDL
|
|
40
|
+
* Normalize DDL string (unify whitespace, newlines, tabs)
|
|
41
41
|
*/
|
|
42
42
|
function normalizeDDL(ddl) {
|
|
43
43
|
return ddl
|
|
44
|
-
.replace(/\s+/g, ' ') //
|
|
45
|
-
.replace(/;\s+/g, ';\n') //
|
|
46
|
-
.trim(); //
|
|
44
|
+
.replace(/\s+/g, ' ') // collapse consecutive whitespace to single space
|
|
45
|
+
.replace(/;\s+/g, ';\n') // newline after semicolon
|
|
46
|
+
.trim(); // trim leading/trailing
|
|
47
47
|
}
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* Parse schema from local SQL files
|
|
50
50
|
*/
|
|
51
51
|
async function parseLocalSchemas(schemaDir) {
|
|
52
52
|
const schemas = {};
|
|
@@ -54,16 +54,19 @@ async function parseLocalSchemas(schemaDir) {
|
|
|
54
54
|
return schemas;
|
|
55
55
|
}
|
|
56
56
|
const files = fs.readdirSync(schemaDir);
|
|
57
|
+
console.log(`Reading schema directory: ${schemaDir}`);
|
|
58
|
+
console.log(`Found SQL files: ${files.filter(f => f.endsWith('.sql')).join(', ') || 'none'}`);
|
|
57
59
|
for (const file of files) {
|
|
58
60
|
if (!file.endsWith('.sql'))
|
|
59
61
|
continue;
|
|
60
62
|
const filePath = path.join(schemaDir, file);
|
|
61
63
|
const stats = fs.statSync(filePath);
|
|
62
64
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
63
|
-
//
|
|
65
|
+
// Get table name from filename (without .sql)
|
|
64
66
|
const tableName = path.basename(file, '.sql');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
console.log(`Processing file: ${file} -> tableName: ${tableName}`);
|
|
68
|
+
// Extract timestamp from comment in file; fallback to mtime
|
|
69
|
+
let timestamp = Math.floor(stats.mtime.getTime() / 1000);
|
|
67
70
|
const timestampMatch = fileContent.match(/-- Remote last updated: (.+)/);
|
|
68
71
|
if (timestampMatch) {
|
|
69
72
|
try {
|
|
@@ -74,16 +77,16 @@ async function parseLocalSchemas(schemaDir) {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
catch (error) {
|
|
77
|
-
//
|
|
80
|
+
// On error use file mtime
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
|
-
// DDL
|
|
83
|
+
// Extract DDL only (exclude comment and blank lines)
|
|
81
84
|
const ddlLines = fileContent.split('\n').filter(line => {
|
|
82
85
|
const trimmed = line.trim();
|
|
83
86
|
return trimmed && !trimmed.startsWith('--');
|
|
84
87
|
});
|
|
85
88
|
const rawDDL = ddlLines.join('\n').trim();
|
|
86
|
-
// DDL
|
|
89
|
+
// Normalize DDL
|
|
87
90
|
const ddl = normalizeDDL(rawDDL);
|
|
88
91
|
schemas[tableName] = {
|
|
89
92
|
ddl: rawDDL,
|
|
@@ -9,17 +9,17 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Fetch table data from remote DB and generate AI seed JSON
|
|
13
13
|
* @param options SeedGenOptions
|
|
14
14
|
*/
|
|
15
15
|
async function generateSeedsFromRemote(options) {
|
|
16
|
-
// tables.yaml
|
|
16
|
+
// Load tables.yaml
|
|
17
17
|
const yamlObj = js_yaml_1.default.load(fs_1.default.readFileSync(options.tablesYamlPath, 'utf8'));
|
|
18
18
|
if (!yamlObj || !Array.isArray(yamlObj.tables)) {
|
|
19
|
-
throw new Error('tables.yaml
|
|
19
|
+
throw new Error('Invalid tables.yaml format. Specify as tables: [ ... ]');
|
|
20
20
|
}
|
|
21
21
|
const tables = yamlObj.tables;
|
|
22
|
-
//
|
|
22
|
+
// Generate datetime subdir name (e.g. 20250705_1116_supatool)
|
|
23
23
|
const now = new Date();
|
|
24
24
|
const y = now.getFullYear();
|
|
25
25
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
@@ -28,28 +28,28 @@ async function generateSeedsFromRemote(options) {
|
|
|
28
28
|
const mm = String(now.getMinutes()).padStart(2, '0');
|
|
29
29
|
const folderName = `${y}${m}${d}_${hh}${mm}_supatool`;
|
|
30
30
|
const outDir = path_1.default.join(options.outputDir, folderName);
|
|
31
|
-
//
|
|
31
|
+
// Create output directory
|
|
32
32
|
if (!fs_1.default.existsSync(outDir)) {
|
|
33
33
|
fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
34
34
|
}
|
|
35
|
-
// DB
|
|
35
|
+
// DB connection
|
|
36
36
|
const client = new pg_1.Client({ connectionString: options.connectionString });
|
|
37
37
|
await client.connect();
|
|
38
38
|
let processedCount = 0;
|
|
39
39
|
for (const tableFullName of tables) {
|
|
40
|
-
//
|
|
40
|
+
// No schema specified -> public
|
|
41
41
|
let schema = 'public';
|
|
42
42
|
let table = tableFullName;
|
|
43
43
|
if (tableFullName.includes('.')) {
|
|
44
44
|
[schema, table] = tableFullName.split('.');
|
|
45
45
|
}
|
|
46
|
-
//
|
|
46
|
+
// Fetch data
|
|
47
47
|
const res = await client.query(`SELECT * FROM "${schema}"."${table}"`);
|
|
48
48
|
const rows = res.rows;
|
|
49
|
-
//
|
|
49
|
+
// File name
|
|
50
50
|
const fileName = `${table}_seed.json`;
|
|
51
51
|
const filePath = path_1.default.join(outDir, fileName);
|
|
52
|
-
//
|
|
52
|
+
// Output JSON
|
|
53
53
|
const json = {
|
|
54
54
|
table: `${schema}.${table}`,
|
|
55
55
|
fetched_at: now.toISOString(),
|
|
@@ -61,16 +61,17 @@ async function generateSeedsFromRemote(options) {
|
|
|
61
61
|
processedCount++;
|
|
62
62
|
}
|
|
63
63
|
await client.end();
|
|
64
|
-
// llms.txt
|
|
64
|
+
// llms.txt index output (overwrite under supabase/seeds each run)
|
|
65
65
|
const files = fs_1.default.readdirSync(outDir);
|
|
66
66
|
const seedFiles = files.filter(f => f.endsWith('_seed.json'));
|
|
67
67
|
let llmsTxt = `# AI seed data index (generated by supatool)\n`;
|
|
68
68
|
llmsTxt += `# fetched_at: ${now.toISOString()}\n`;
|
|
69
69
|
llmsTxt += `# folder: ${folderName}\n`;
|
|
70
|
+
llmsTxt += `# Schema catalog: ../schemas/llms.txt\n`;
|
|
70
71
|
for (const basename of seedFiles) {
|
|
71
72
|
const file = path_1.default.join(outDir, basename);
|
|
72
73
|
const content = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
73
|
-
//
|
|
74
|
+
// Table comment (empty if none)
|
|
74
75
|
let tableComment = '';
|
|
75
76
|
try {
|
|
76
77
|
const [schema, table] = content.table.split('.');
|
|
@@ -86,11 +87,11 @@ async function generateSeedsFromRemote(options) {
|
|
|
86
87
|
}
|
|
87
88
|
const llmsPath = path_1.default.join(options.outputDir, 'llms.txt');
|
|
88
89
|
fs_1.default.writeFileSync(llmsPath, llmsTxt, 'utf8');
|
|
89
|
-
//
|
|
90
|
+
// Output summary in English
|
|
90
91
|
console.log(`Seed export completed. Processed tables: ${processedCount}`);
|
|
91
92
|
console.log(`llms.txt index written to: ${llmsPath}`);
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
+
/** Utility to get table comment */
|
|
94
95
|
async function getTableComment(connectionString, schema, table) {
|
|
95
96
|
const client = new pg_1.Client({ connectionString });
|
|
96
97
|
await client.connect();
|