supatool 0.1.22 → 0.3.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 +228 -22
- package/dist/bin/helptext.js +106 -20
- package/dist/bin/supatool.js +172 -21
- package/dist/generator/client.js +9 -0
- package/dist/generator/crudGenerator.js +57 -0
- package/dist/generator/docGenerator.js +64 -0
- package/dist/generator/rlsGenerator.js +111 -0
- package/dist/generator/schemaCrudGenerator.js +560 -0
- package/dist/generator/sqlGenerator.js +100 -0
- package/dist/generator/typeGenerator.js +55 -0
- package/dist/generator/types.js +2 -0
- package/dist/parser/modelParser.js +18 -0
- package/dist/sync/config.js +98 -0
- package/dist/sync/definitionExtractor.js +1205 -0
- package/dist/sync/fetchRemoteSchemas.js +369 -0
- package/dist/sync/generateMigration.js +276 -0
- package/dist/sync/index.js +22 -0
- package/dist/sync/migrationRunner.js +271 -0
- package/dist/sync/parseLocalSchemas.js +97 -0
- package/dist/sync/sync.js +208 -0
- package/dist/sync/utils.js +52 -0
- package/dist/sync/writeSchema.js +161 -0
- package/package.json +18 -3
@@ -0,0 +1,369 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.fetchRemoteSchemas = fetchRemoteSchemas;
|
37
|
+
const pg_1 = require("pg");
|
38
|
+
const dns = __importStar(require("dns"));
|
39
|
+
const util_1 = require("util");
|
40
|
+
const dnsLookup = (0, util_1.promisify)(dns.lookup);
|
41
|
+
/**
|
42
|
+
* DDL文字列を正規化(空白・改行・タブを統一)
|
43
|
+
*/
|
44
|
+
function normalizeDDL(ddl) {
|
45
|
+
return ddl
|
46
|
+
.replace(/\s+/g, ' ') // 連続する空白文字を1つのスペースに
|
47
|
+
.replace(/;\s+/g, ';\n') // セミコロン後に改行
|
48
|
+
.trim(); // 前後の空白を削除
|
49
|
+
}
|
50
|
+
/**
|
51
|
+
* リモートSupabaseからスキーマを取得
|
52
|
+
*/
|
53
|
+
async function fetchRemoteSchemas(connectionString) {
|
54
|
+
const schemas = {};
|
55
|
+
// console.log('データベースに接続中...');
|
56
|
+
try {
|
57
|
+
// 接続文字列の基本チェック
|
58
|
+
if (!connectionString || !connectionString.startsWith('postgresql://')) {
|
59
|
+
throw new Error('無効な接続文字列です。postgresql://で始まる形式で指定してください。');
|
60
|
+
}
|
61
|
+
// URL解析して接続先を表示
|
62
|
+
const url = new URL(connectionString);
|
63
|
+
console.log(`接続先: ${url.hostname}:${url.port}`);
|
64
|
+
console.log(`ユーザー: ${url.username}`);
|
65
|
+
// パスワードに特殊文字が含まれている場合の対処
|
66
|
+
const decodedPassword = decodeURIComponent(url.password || '');
|
67
|
+
// 接続文字列を再構築(パスワードを適切にエンコード)
|
68
|
+
const encodedPassword = encodeURIComponent(decodedPassword);
|
69
|
+
// Session poolerのモードを明示的に指定
|
70
|
+
let reconstructedConnectionString = `postgresql://${url.username}:${encodedPassword}@${url.hostname}:${url.port}${url.pathname}`;
|
71
|
+
// Session poolingモードのパラメータを追加
|
72
|
+
const searchParams = new URLSearchParams(url.search);
|
73
|
+
searchParams.set('sslmode', 'require');
|
74
|
+
searchParams.set('application_name', 'supatool');
|
75
|
+
reconstructedConnectionString += `?${searchParams.toString()}`;
|
76
|
+
// IPv4を優先に変更
|
77
|
+
dns.setDefaultResultOrder('ipv4first');
|
78
|
+
// DNS解決テスト
|
79
|
+
try {
|
80
|
+
const addresses = await dns.promises.lookup(url.hostname, { all: true });
|
81
|
+
}
|
82
|
+
catch (dnsError) {
|
83
|
+
console.error('❌ DNS解決に失敗しました');
|
84
|
+
throw dnsError;
|
85
|
+
}
|
86
|
+
// Session pooler用の設定
|
87
|
+
const clientConfig = {
|
88
|
+
connectionString: reconstructedConnectionString,
|
89
|
+
ssl: {
|
90
|
+
rejectUnauthorized: false
|
91
|
+
},
|
92
|
+
// Session pooler用の追加設定
|
93
|
+
statement_timeout: 30000,
|
94
|
+
query_timeout: 30000,
|
95
|
+
connectionTimeoutMillis: 10000,
|
96
|
+
idleTimeoutMillis: 10000
|
97
|
+
};
|
98
|
+
console.log('Session pooler経由で接続中...');
|
99
|
+
let client;
|
100
|
+
try {
|
101
|
+
client = new pg_1.Client(clientConfig);
|
102
|
+
await client.connect();
|
103
|
+
}
|
104
|
+
catch (sslError) {
|
105
|
+
if (sslError instanceof Error && (sslError.message.includes('SASL') ||
|
106
|
+
sslError.message.includes('SCRAM') ||
|
107
|
+
sslError.message.includes('certificate') ||
|
108
|
+
sslError.message.includes('SELF_SIGNED_CERT'))) {
|
109
|
+
console.log('SSL接続不可のため、SSL無効化で再試行中...');
|
110
|
+
// SSL無効化バージョンを試行
|
111
|
+
const noSslConnectionString = reconstructedConnectionString.replace('sslmode=require', 'sslmode=disable');
|
112
|
+
const noSslConfig = {
|
113
|
+
...clientConfig,
|
114
|
+
connectionString: noSslConnectionString,
|
115
|
+
ssl: false
|
116
|
+
};
|
117
|
+
try {
|
118
|
+
client = new pg_1.Client(noSslConfig);
|
119
|
+
await client.connect();
|
120
|
+
console.log('SSL無効化での接続に成功');
|
121
|
+
}
|
122
|
+
catch (noSslError) {
|
123
|
+
if (noSslError instanceof Error && (noSslError.message.includes('SASL') || noSslError.message.includes('SCRAM'))) {
|
124
|
+
console.log('Session poolerでSASLエラー継続。Direct connectionで再試行中...');
|
125
|
+
// Direct connection (ポート5432) で再試行
|
126
|
+
const directConnectionString = noSslConnectionString.replace('pooler.supabase.com:5432', 'supabase.co:5432');
|
127
|
+
const directConfig = {
|
128
|
+
...noSslConfig,
|
129
|
+
connectionString: directConnectionString
|
130
|
+
};
|
131
|
+
client = new pg_1.Client(directConfig);
|
132
|
+
await client.connect();
|
133
|
+
console.log('Direct connectionでの接続に成功');
|
134
|
+
}
|
135
|
+
else {
|
136
|
+
throw noSslError;
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
else {
|
141
|
+
throw sslError;
|
142
|
+
}
|
143
|
+
}
|
144
|
+
// 接続テストクエリ
|
145
|
+
const testResult = await client.query('SELECT version()');
|
146
|
+
// テーブル一覧を取得
|
147
|
+
const tablesResult = await client.query(`
|
148
|
+
SELECT tablename
|
149
|
+
FROM pg_tables
|
150
|
+
WHERE schemaname = 'public'
|
151
|
+
ORDER BY tablename
|
152
|
+
`);
|
153
|
+
console.log(`リモートテーブル:${tablesResult.rows.length}`);
|
154
|
+
// ローディングアニメーション用
|
155
|
+
const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
156
|
+
let spinnerIndex = 0;
|
157
|
+
const startTime = Date.now();
|
158
|
+
const spinnerInterval = setInterval(() => {
|
159
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
160
|
+
process.stdout.write(`\r${spinner[spinnerIndex]} スキーマ取得中... ${elapsed}s`);
|
161
|
+
spinnerIndex = (spinnerIndex + 1) % spinner.length;
|
162
|
+
}, 100);
|
163
|
+
// 各テーブルのスキーマ情報を取得
|
164
|
+
for (const row of tablesResult.rows) {
|
165
|
+
const tableName = row.tablename;
|
166
|
+
// テーブルの最終更新時刻を取得(複数の方法を試行)
|
167
|
+
let timestamp = Math.floor(Date.now() / 1000); // デフォルト値
|
168
|
+
// pg_stat_user_tablesから取得
|
169
|
+
try {
|
170
|
+
const tableStatsResult = await client.query(`
|
171
|
+
SELECT
|
172
|
+
EXTRACT(EPOCH FROM GREATEST(
|
173
|
+
COALESCE(last_vacuum, '1970-01-01'::timestamp),
|
174
|
+
COALESCE(last_autovacuum, '1970-01-01'::timestamp),
|
175
|
+
COALESCE(last_analyze, '1970-01-01'::timestamp),
|
176
|
+
COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
|
177
|
+
))::bigint as last_updated
|
178
|
+
FROM pg_stat_user_tables
|
179
|
+
WHERE relname = $1 AND schemaname = 'public'
|
180
|
+
`, [tableName]);
|
181
|
+
if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
|
182
|
+
timestamp = tableStatsResult.rows[0].last_updated;
|
183
|
+
}
|
184
|
+
else {
|
185
|
+
// デフォルトタイムスタンプ(十分に古い固定値)
|
186
|
+
timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
187
|
+
}
|
188
|
+
}
|
189
|
+
catch (statsError) {
|
190
|
+
// デフォルトタイムスタンプ(十分に古い固定値)
|
191
|
+
timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
192
|
+
}
|
193
|
+
// カラム情報を取得
|
194
|
+
const columnsResult = await client.query(`
|
195
|
+
SELECT
|
196
|
+
column_name,
|
197
|
+
data_type,
|
198
|
+
character_maximum_length,
|
199
|
+
is_nullable,
|
200
|
+
column_default
|
201
|
+
FROM information_schema.columns
|
202
|
+
WHERE table_schema = 'public'
|
203
|
+
AND table_name = $1
|
204
|
+
ORDER BY ordinal_position
|
205
|
+
`, [tableName]);
|
206
|
+
// 主キー情報を取得
|
207
|
+
const primaryKeyResult = await client.query(`
|
208
|
+
SELECT column_name
|
209
|
+
FROM information_schema.table_constraints tc
|
210
|
+
JOIN information_schema.key_column_usage kcu
|
211
|
+
ON tc.constraint_name = kcu.constraint_name
|
212
|
+
WHERE tc.table_schema = 'public'
|
213
|
+
AND tc.table_name = $1
|
214
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
215
|
+
ORDER BY kcu.ordinal_position
|
216
|
+
`, [tableName]);
|
217
|
+
// CREATE TABLE文を生成
|
218
|
+
let ddl = `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
|
219
|
+
const columnDefs = [];
|
220
|
+
for (const col of columnsResult.rows) {
|
221
|
+
let colDef = ` ${col.column_name} ${col.data_type.toUpperCase()}`;
|
222
|
+
// 長さ指定
|
223
|
+
if (col.character_maximum_length) {
|
224
|
+
colDef += `(${col.character_maximum_length})`;
|
225
|
+
}
|
226
|
+
// NOT NULL制約
|
227
|
+
if (col.is_nullable === 'NO') {
|
228
|
+
colDef += ' NOT NULL';
|
229
|
+
}
|
230
|
+
// デフォルト値
|
231
|
+
if (col.column_default) {
|
232
|
+
colDef += ` DEFAULT ${col.column_default}`;
|
233
|
+
}
|
234
|
+
columnDefs.push(colDef);
|
235
|
+
}
|
236
|
+
ddl += columnDefs.join(',\n');
|
237
|
+
// 主キー制約
|
238
|
+
if (primaryKeyResult.rows.length > 0) {
|
239
|
+
const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
|
240
|
+
ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
|
241
|
+
}
|
242
|
+
// UNIQUE制約を取得してCREATE TABLE内に含める
|
243
|
+
const uniqueConstraintResult = await client.query(`
|
244
|
+
SELECT
|
245
|
+
tc.constraint_name,
|
246
|
+
string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns
|
247
|
+
FROM information_schema.table_constraints tc
|
248
|
+
JOIN information_schema.key_column_usage kcu
|
249
|
+
ON tc.constraint_name = kcu.constraint_name
|
250
|
+
WHERE tc.table_schema = 'public'
|
251
|
+
AND tc.table_name = $1
|
252
|
+
AND tc.constraint_type = 'UNIQUE'
|
253
|
+
GROUP BY tc.constraint_name
|
254
|
+
ORDER BY tc.constraint_name
|
255
|
+
`, [tableName]);
|
256
|
+
// UNIQUE制約をCREATE TABLE内に追加
|
257
|
+
for (const unique of uniqueConstraintResult.rows) {
|
258
|
+
ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
|
259
|
+
}
|
260
|
+
// CHECK制約は一時的に無効化(NOT NULL制約と重複するため)
|
261
|
+
// const checkConstraintResult = await client.query(`...`);
|
262
|
+
// CHECK制約をCREATE TABLE内に追加は一時的に無効化
|
263
|
+
// for (const check of checkConstraintResult.rows) {
|
264
|
+
// ddl += `,\n CONSTRAINT ${check.constraint_name} CHECK ${check.check_clause}`;
|
265
|
+
// }
|
266
|
+
ddl += '\n);';
|
267
|
+
// 外部キー制約のみを別途生成
|
268
|
+
const foreignKeyResult = await client.query(`
|
269
|
+
SELECT
|
270
|
+
tc.constraint_name,
|
271
|
+
kcu.column_name,
|
272
|
+
ccu.table_name AS foreign_table_name,
|
273
|
+
ccu.column_name AS foreign_column_name,
|
274
|
+
rc.delete_rule,
|
275
|
+
rc.update_rule
|
276
|
+
FROM information_schema.table_constraints tc
|
277
|
+
JOIN information_schema.key_column_usage kcu
|
278
|
+
ON tc.constraint_name = kcu.constraint_name
|
279
|
+
JOIN information_schema.constraint_column_usage ccu
|
280
|
+
ON ccu.constraint_name = tc.constraint_name
|
281
|
+
JOIN information_schema.referential_constraints rc
|
282
|
+
ON tc.constraint_name = rc.constraint_name
|
283
|
+
WHERE tc.table_schema = 'public'
|
284
|
+
AND tc.table_name = $1
|
285
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
286
|
+
ORDER BY tc.constraint_name
|
287
|
+
`, [tableName]);
|
288
|
+
// 外部キー制約のみをALTER TABLE文として追加
|
289
|
+
for (const fk of foreignKeyResult.rows) {
|
290
|
+
ddl += `\n\nALTER TABLE ${tableName} ADD CONSTRAINT ${fk.constraint_name}`;
|
291
|
+
ddl += ` FOREIGN KEY (${fk.column_name})`;
|
292
|
+
ddl += ` REFERENCES ${fk.foreign_table_name} (${fk.foreign_column_name})`;
|
293
|
+
if (fk.delete_rule && fk.delete_rule !== 'NO ACTION') {
|
294
|
+
ddl += ` ON DELETE ${fk.delete_rule}`;
|
295
|
+
}
|
296
|
+
if (fk.update_rule && fk.update_rule !== 'NO ACTION') {
|
297
|
+
ddl += ` ON UPDATE ${fk.update_rule}`;
|
298
|
+
}
|
299
|
+
ddl += ';';
|
300
|
+
}
|
301
|
+
// インデックス情報を取得
|
302
|
+
const indexResult = await client.query(`
|
303
|
+
SELECT
|
304
|
+
indexname,
|
305
|
+
indexdef
|
306
|
+
FROM pg_indexes
|
307
|
+
WHERE schemaname = 'public'
|
308
|
+
AND tablename = $1
|
309
|
+
AND indexname NOT LIKE '%_pkey'
|
310
|
+
AND indexname NOT IN (
|
311
|
+
SELECT tc.constraint_name
|
312
|
+
FROM information_schema.table_constraints tc
|
313
|
+
WHERE tc.table_name = $1 AND tc.constraint_type = 'UNIQUE'
|
314
|
+
)
|
315
|
+
ORDER BY indexname
|
316
|
+
`, [tableName]);
|
317
|
+
// インデックスを追加
|
318
|
+
for (const idx of indexResult.rows) {
|
319
|
+
ddl += `\n\n${idx.indexdef};`;
|
320
|
+
}
|
321
|
+
// DDLを元の形式で保存
|
322
|
+
schemas[tableName] = {
|
323
|
+
ddl,
|
324
|
+
timestamp
|
325
|
+
};
|
326
|
+
}
|
327
|
+
// ローディングアニメーション停止
|
328
|
+
clearInterval(spinnerInterval);
|
329
|
+
const totalTime = Math.floor((Date.now() - startTime) / 1000);
|
330
|
+
process.stdout.write(`\rSchema fetch completed (${totalTime}s) \n`);
|
331
|
+
await client.end();
|
332
|
+
}
|
333
|
+
catch (error) {
|
334
|
+
console.error('❌ Remote schema fetch error:');
|
335
|
+
if (error instanceof Error) {
|
336
|
+
if (error.message.includes('ENOTFOUND')) {
|
337
|
+
console.error('🌐 DNS resolution error: Host not found');
|
338
|
+
console.error('💡 Check:');
|
339
|
+
console.error(' - Is your internet connection working?');
|
340
|
+
console.error(' - Is your Supabase project connection string correct?');
|
341
|
+
}
|
342
|
+
else if (error.message.includes('authentication failed')) {
|
343
|
+
console.error('🔐 Authentication error: Incorrect username or password');
|
344
|
+
}
|
345
|
+
else if (error.message.includes('SASL') || error.message.includes('SCRAM')) {
|
346
|
+
console.error('🔐 SASL authentication error: Password or connection settings issue');
|
347
|
+
console.error('💡 Session pooler connection checklist:');
|
348
|
+
console.error(' - Reset database password in Supabase dashboard');
|
349
|
+
console.error(' - Update connection string with new password');
|
350
|
+
console.error(' - URL encode special characters in password if needed');
|
351
|
+
console.error(' - Ensure project is not paused');
|
352
|
+
console.error(' - Supabase Dashboard → Settings → Database → Reset database password');
|
353
|
+
}
|
354
|
+
else if (error.message.includes('connect ETIMEDOUT')) {
|
355
|
+
console.error('⏰ Connection timeout: Cannot reach database server');
|
356
|
+
}
|
357
|
+
else {
|
358
|
+
console.error(`🐛 ${error.message}`);
|
359
|
+
}
|
360
|
+
}
|
361
|
+
console.error('\n📖 Session pooler connection setup:');
|
362
|
+
console.error('1. Supabase Dashboard → Settings → Database');
|
363
|
+
console.error('2. Select "Session pooler" tab');
|
364
|
+
console.error('3. Reset password (Reset database password)');
|
365
|
+
console.error('4. Set new connection string in .env.local');
|
366
|
+
throw error;
|
367
|
+
}
|
368
|
+
return schemas;
|
369
|
+
}
|
@@ -0,0 +1,276 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.generateMigrationFile = generateMigrationFile;
|
37
|
+
const fs = __importStar(require("fs"));
|
38
|
+
const path = __importStar(require("path"));
|
39
|
+
const diff_1 = require("diff");
|
40
|
+
/**
|
41
|
+
* DDL差分からALTER TABLE文を生成
|
42
|
+
*/
|
43
|
+
function generateAlterStatements(tableName, fromDdl, toDdl) {
|
44
|
+
const statements = [];
|
45
|
+
const diff = (0, diff_1.diffLines)(fromDdl, toDdl);
|
46
|
+
// デバッグ情報削除 - シンプルな表示
|
47
|
+
// カラム名変更の検出用
|
48
|
+
const removedColumns = [];
|
49
|
+
const addedColumns = [];
|
50
|
+
for (const part of diff) {
|
51
|
+
if (part.added) {
|
52
|
+
// 追加された行をALTER TABLE文に変換
|
53
|
+
const lines = part.value.split('\n').filter(line => line.trim());
|
54
|
+
for (const line of lines) {
|
55
|
+
const trimmed = line.trim();
|
56
|
+
// カラム定義の追加を検出
|
57
|
+
if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
|
58
|
+
const columnDef = trimmed.replace(/,$/, '').trim();
|
59
|
+
const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
|
60
|
+
if (columnMatch) {
|
61
|
+
const columnName = columnMatch[1];
|
62
|
+
const columnType = columnMatch[2];
|
63
|
+
addedColumns.push({ name: columnName, definition: columnType });
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
else if (part.removed) {
|
69
|
+
// 削除された行をALTER TABLE文に変換
|
70
|
+
const lines = part.value.split('\n').filter(line => line.trim());
|
71
|
+
for (const line of lines) {
|
72
|
+
const trimmed = line.trim();
|
73
|
+
// カラム定義の削除を検出
|
74
|
+
if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
|
75
|
+
const columnDef = trimmed.replace(/,$/, '').trim();
|
76
|
+
const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
|
77
|
+
if (columnMatch) {
|
78
|
+
const columnName = columnMatch[1];
|
79
|
+
const columnType = columnMatch[2];
|
80
|
+
removedColumns.push({ name: columnName, definition: columnType });
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
// カラム名変更の検出(型定義が同じで名前が違う場合)
|
87
|
+
const renames = [];
|
88
|
+
for (const removed of removedColumns) {
|
89
|
+
const matching = addedColumns.find(added => added.definition === removed.definition);
|
90
|
+
if (matching) {
|
91
|
+
renames.push({ from: removed.name, to: matching.name });
|
92
|
+
// 処理済みとしてマーク
|
93
|
+
removedColumns.splice(removedColumns.indexOf(removed), 1);
|
94
|
+
addedColumns.splice(addedColumns.indexOf(matching), 1);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
// デバッグログ削除 - シンプルな表示
|
98
|
+
// RENAME COLUMN文を生成
|
99
|
+
for (const rename of renames) {
|
100
|
+
statements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
|
101
|
+
}
|
102
|
+
// 残りの削除されたカラム
|
103
|
+
for (const removed of removedColumns) {
|
104
|
+
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${removed.name};`);
|
105
|
+
}
|
106
|
+
// 残りの追加されたカラム
|
107
|
+
for (const added of addedColumns) {
|
108
|
+
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${added.name} ${added.definition};`);
|
109
|
+
}
|
110
|
+
return statements;
|
111
|
+
}
|
112
|
+
/**
|
113
|
+
* マイグレーションファイルを生成
|
114
|
+
*/
|
115
|
+
async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.') {
|
116
|
+
const migrationDir = path.join(projectDir, 'supabase', 'migrations');
|
117
|
+
// migrations ディレクトリを作成
|
118
|
+
if (!fs.existsSync(migrationDir)) {
|
119
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
120
|
+
}
|
121
|
+
// ALTER TABLE文を生成
|
122
|
+
const alterStatements = generateAlterStatements(tableName, fromDdl, toDdl);
|
123
|
+
if (alterStatements.length === 0) {
|
124
|
+
return await generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir);
|
125
|
+
}
|
126
|
+
// タイムスタンプでファイル名を生成
|
127
|
+
const now = new Date();
|
128
|
+
const timestamp = now.toISOString()
|
129
|
+
.replace(/[-:]/g, '')
|
130
|
+
.replace(/\..+/, '')
|
131
|
+
.replace('T', '');
|
132
|
+
const filename = `${timestamp}_update_${tableName}.sql`;
|
133
|
+
const filepath = path.join(migrationDir, filename);
|
134
|
+
// マイグレーションファイルの内容を生成
|
135
|
+
const content = `-- Migration generated by supatool
|
136
|
+
-- Table: ${tableName}
|
137
|
+
-- Generated at: ${now.toISOString()}
|
138
|
+
|
139
|
+
${alterStatements.join('\n')}
|
140
|
+
`;
|
141
|
+
// ファイルを書き込み
|
142
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
143
|
+
console.log(`マイグレーションファイル生成: ${filename}`);
|
144
|
+
return filepath;
|
145
|
+
}
|
146
|
+
/**
|
147
|
+
* DDLからカラム定義を抽出
|
148
|
+
*/
|
149
|
+
function extractColumns(ddl) {
|
150
|
+
const columns = [];
|
151
|
+
// CREATE TABLE部分を抽出
|
152
|
+
const createTableMatch = ddl.match(/CREATE TABLE[^(]*\((.*)\);?/is);
|
153
|
+
if (!createTableMatch) {
|
154
|
+
return columns;
|
155
|
+
}
|
156
|
+
const tableContent = createTableMatch[1];
|
157
|
+
// カラム定義とCONSTRAINTを分離
|
158
|
+
const parts = tableContent.split(',').map(part => part.trim());
|
159
|
+
for (const part of parts) {
|
160
|
+
// CONSTRAINTやPRIMARY KEYは除外
|
161
|
+
if (part.match(/^(PRIMARY|CONSTRAINT|UNIQUE|FOREIGN|CHECK)/i)) {
|
162
|
+
continue;
|
163
|
+
}
|
164
|
+
// カラム名とデータ型を分離
|
165
|
+
const columnMatch = part.trim().match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$/);
|
166
|
+
if (columnMatch) {
|
167
|
+
const columnName = columnMatch[1];
|
168
|
+
const columnType = columnMatch[2];
|
169
|
+
columns.push({ name: columnName, definition: columnType });
|
170
|
+
}
|
171
|
+
}
|
172
|
+
return columns;
|
173
|
+
}
|
174
|
+
/**
|
175
|
+
* 差分からカラム変更を解析
|
176
|
+
*/
|
177
|
+
function analyzeDiffForTemplate(fromDdl, toDdl) {
|
178
|
+
// 各DDLからカラムを抽出
|
179
|
+
const fromColumns = extractColumns(fromDdl);
|
180
|
+
const toColumns = extractColumns(toDdl);
|
181
|
+
// 削除されたカラム(FROMにあってTOにない)
|
182
|
+
const removedColumns = fromColumns.filter(fromCol => !toColumns.some(toCol => toCol.name === fromCol.name));
|
183
|
+
// 追加されたカラム(TOにあってFROMにない)
|
184
|
+
const addedColumns = toColumns.filter(toCol => !fromColumns.some(fromCol => fromCol.name === toCol.name));
|
185
|
+
return { removedColumns, addedColumns };
|
186
|
+
}
|
187
|
+
/**
|
188
|
+
* 手動マイグレーションテンプレートを生成
|
189
|
+
*/
|
190
|
+
async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir) {
|
191
|
+
const migrationDir = path.join(projectDir, 'supabase', 'migrations');
|
192
|
+
if (!fs.existsSync(migrationDir)) {
|
193
|
+
fs.mkdirSync(migrationDir, { recursive: true });
|
194
|
+
}
|
195
|
+
// 差分を解析
|
196
|
+
const { removedColumns, addedColumns } = analyzeDiffForTemplate(fromDdl, toDdl);
|
197
|
+
// タイムスタンプでファイル名を生成
|
198
|
+
const now = new Date();
|
199
|
+
const timestamp = now.toISOString()
|
200
|
+
.replace(/[-:]/g, '')
|
201
|
+
.replace(/\..+/, '')
|
202
|
+
.replace('T', '');
|
203
|
+
const filename = `${timestamp}_manual_update_${tableName}.sql`;
|
204
|
+
const filepath = path.join(migrationDir, filename);
|
205
|
+
// 実際の変更を基にテンプレートを生成
|
206
|
+
let migrationStatements = [];
|
207
|
+
// カラム名変更の可能性を検出
|
208
|
+
const potentialRenames = [];
|
209
|
+
for (const removed of removedColumns) {
|
210
|
+
const matching = addedColumns.find(added => added.definition === removed.definition);
|
211
|
+
if (matching) {
|
212
|
+
potentialRenames.push({
|
213
|
+
from: removed.name,
|
214
|
+
to: matching.name,
|
215
|
+
definition: removed.definition
|
216
|
+
});
|
217
|
+
}
|
218
|
+
}
|
219
|
+
// 残りの削除・追加
|
220
|
+
const remainingRemoved = removedColumns.filter(r => !potentialRenames.some(p => p.from === r.name));
|
221
|
+
const remainingAdded = addedColumns.filter(a => !potentialRenames.some(p => p.to === a.name));
|
222
|
+
// テンプレート文を生成(DROP → ADD → RENAME の順序)
|
223
|
+
if (remainingRemoved.length > 0) {
|
224
|
+
migrationStatements.push('-- Column removals:');
|
225
|
+
for (const removed of remainingRemoved) {
|
226
|
+
migrationStatements.push(`ALTER TABLE ${tableName} DROP COLUMN ${removed.name};`);
|
227
|
+
}
|
228
|
+
}
|
229
|
+
if (remainingAdded.length > 0) {
|
230
|
+
if (migrationStatements.length > 0)
|
231
|
+
migrationStatements.push('');
|
232
|
+
migrationStatements.push('-- Column additions:');
|
233
|
+
for (const added of remainingAdded) {
|
234
|
+
migrationStatements.push(`ALTER TABLE ${tableName} ADD COLUMN ${added.name} ${added.definition};`);
|
235
|
+
}
|
236
|
+
}
|
237
|
+
if (potentialRenames.length > 0) {
|
238
|
+
if (migrationStatements.length > 0)
|
239
|
+
migrationStatements.push('');
|
240
|
+
migrationStatements.push('-- Possible column renames:');
|
241
|
+
for (const rename of potentialRenames) {
|
242
|
+
migrationStatements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
|
243
|
+
}
|
244
|
+
}
|
245
|
+
// フォールバック: 何も検出されなかった場合
|
246
|
+
if (migrationStatements.length === 0) {
|
247
|
+
migrationStatements = [
|
248
|
+
`-- Manual migration for ${tableName}`,
|
249
|
+
`-- No specific changes detected, please edit manually`,
|
250
|
+
``,
|
251
|
+
`-- ALTER TABLE ${tableName} RENAME COLUMN old_name TO new_name;`,
|
252
|
+
`-- ALTER TABLE ${tableName} ADD COLUMN new_column TYPE;`,
|
253
|
+
`-- ALTER TABLE ${tableName} DROP COLUMN old_column;`
|
254
|
+
];
|
255
|
+
}
|
256
|
+
const content = `-- Manual migration for ${tableName}
|
257
|
+
-- Edit this file and apply manually or using supabase CLI
|
258
|
+
|
259
|
+
${migrationStatements.join('\n')}
|
260
|
+
`;
|
261
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
262
|
+
console.log(`手動マイグレーションテンプレート生成: ${filename}`);
|
263
|
+
return filepath;
|
264
|
+
}
|
265
|
+
/**
|
266
|
+
* より高度な差分解析(カラム変更を検出)
|
267
|
+
*/
|
268
|
+
function analyzeColumnChanges(tableName, localDdl, remoteDdl) {
|
269
|
+
const statements = [];
|
270
|
+
// 簡単な例:カラム名の変更を検出
|
271
|
+
const localLines = localDdl.split('\n').map(line => line.trim()).filter(line => line);
|
272
|
+
const remoteLines = remoteDdl.split('\n').map(line => line.trim()).filter(line => line);
|
273
|
+
// カラムの型変更やデフォルト値変更などの高度な検出は今後実装
|
274
|
+
// 現在は基本的な追加/削除のみ対応
|
275
|
+
return statements;
|
276
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
|
+
};
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./sync"), exports);
|
18
|
+
__exportStar(require("./fetchRemoteSchemas"), exports);
|
19
|
+
__exportStar(require("./parseLocalSchemas"), exports);
|
20
|
+
__exportStar(require("./writeSchema"), exports);
|
21
|
+
__exportStar(require("./utils"), exports);
|
22
|
+
__exportStar(require("./config"), exports);
|