supatool 0.1.23 → 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 +227 -25
- 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,271 @@
|
|
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.runMigrationsUp = runMigrationsUp;
|
37
|
+
const fs = __importStar(require("fs"));
|
38
|
+
const path = __importStar(require("path"));
|
39
|
+
const pg_1 = require("pg");
|
40
|
+
/**
|
41
|
+
* Supabaseの実際のマイグレーション管理テーブルを特定
|
42
|
+
* ソース: https://github.com/supabase/supabase
|
43
|
+
*/
|
44
|
+
async function findMigrationTable(client) {
|
45
|
+
// Supabaseソースコードに基づく実際のテーブル候補
|
46
|
+
const possibleTables = [
|
47
|
+
'supabase_migrations.schema_migrations',
|
48
|
+
'_supabase_migrations.schema_migrations',
|
49
|
+
'supabase_migrations.migrations',
|
50
|
+
'_supabase_migrations.migrations',
|
51
|
+
'_supabase.migrations',
|
52
|
+
'supabase.schema_migrations',
|
53
|
+
'public.schema_migrations'
|
54
|
+
];
|
55
|
+
console.log('🔍 Supabaseマイグレーション管理テーブルを検索中...');
|
56
|
+
for (const tableName of possibleTables) {
|
57
|
+
try {
|
58
|
+
const schema = tableName.split('.')[0];
|
59
|
+
const table = tableName.split('.')[1];
|
60
|
+
const result = await client.query(`
|
61
|
+
SELECT EXISTS (
|
62
|
+
SELECT FROM information_schema.tables
|
63
|
+
WHERE table_schema = $1 AND table_name = $2
|
64
|
+
);
|
65
|
+
`, [schema, table]);
|
66
|
+
if (result.rows[0].exists) {
|
67
|
+
console.log(`✅ 発見: ${tableName}`);
|
68
|
+
// テーブル構造も確認
|
69
|
+
try {
|
70
|
+
const columnsResult = await client.query(`
|
71
|
+
SELECT column_name, data_type
|
72
|
+
FROM information_schema.columns
|
73
|
+
WHERE table_schema = $1 AND table_name = $2
|
74
|
+
ORDER BY ordinal_position;
|
75
|
+
`, [schema, table]);
|
76
|
+
console.log(`📋 カラム構造:`, columnsResult.rows.map(r => `${r.column_name}:${r.data_type}`).join(', '));
|
77
|
+
}
|
78
|
+
catch (err) {
|
79
|
+
// カラム構造確認失敗は無視
|
80
|
+
}
|
81
|
+
return tableName;
|
82
|
+
}
|
83
|
+
}
|
84
|
+
catch (error) {
|
85
|
+
// テーブルが存在しない場合は続行
|
86
|
+
}
|
87
|
+
}
|
88
|
+
console.log('❌ Supabaseマイグレーション管理テーブルが見つかりません');
|
89
|
+
return null;
|
90
|
+
}
|
91
|
+
/**
|
92
|
+
* 適用済みマイグレーションを取得(既存テーブル使用)
|
93
|
+
*/
|
94
|
+
async function getAppliedMigrations(client) {
|
95
|
+
const migrationTable = await findMigrationTable(client);
|
96
|
+
if (!migrationTable) {
|
97
|
+
console.log('⚠️ マイグレーション管理テーブルが見つからないため、全て未適用として扱います');
|
98
|
+
return new Set();
|
99
|
+
}
|
100
|
+
try {
|
101
|
+
const result = await client.query(`
|
102
|
+
SELECT version FROM ${migrationTable} ORDER BY inserted_at
|
103
|
+
`);
|
104
|
+
console.log(`📋 適用済みマイグレーション: ${result.rows.length} 個`);
|
105
|
+
return new Set(result.rows.map(row => row.version));
|
106
|
+
}
|
107
|
+
catch (error) {
|
108
|
+
console.log('⚠️ マイグレーション履歴の取得に失敗、全て未適用として扱います');
|
109
|
+
return new Set();
|
110
|
+
}
|
111
|
+
}
|
112
|
+
/**
|
113
|
+
* マイグレーションファイルをスキャン
|
114
|
+
*/
|
115
|
+
function scanMigrationFiles(migrationsDir) {
|
116
|
+
if (!fs.existsSync(migrationsDir)) {
|
117
|
+
console.log(`📁 マイグレーションディレクトリが見つかりません: ${migrationsDir}`);
|
118
|
+
return [];
|
119
|
+
}
|
120
|
+
const files = fs.readdirSync(migrationsDir)
|
121
|
+
.filter(file => file.endsWith('.sql'))
|
122
|
+
.sort(); // タイムスタンプ順に並び替え
|
123
|
+
return files.map(filename => {
|
124
|
+
const filepath = path.join(migrationsDir, filename);
|
125
|
+
const match = filename.match(/^(\d{14})_(.+)\.sql$/);
|
126
|
+
return {
|
127
|
+
filename,
|
128
|
+
filepath,
|
129
|
+
timestamp: match ? match[1] : '',
|
130
|
+
name: match ? match[2] : filename.replace('.sql', '')
|
131
|
+
};
|
132
|
+
});
|
133
|
+
}
|
134
|
+
/**
|
135
|
+
* SQLファイルの内容をクリーンアップ
|
136
|
+
*/
|
137
|
+
function cleanSqlContent(content) {
|
138
|
+
return content
|
139
|
+
.split('\n')
|
140
|
+
.filter(line => !line.trim().startsWith('--')) // コメント行を除去
|
141
|
+
.join('\n')
|
142
|
+
.trim();
|
143
|
+
}
|
144
|
+
/**
|
145
|
+
* マイグレーションファイルを適用
|
146
|
+
*/
|
147
|
+
async function applyMigration(client, migration, projectDir) {
|
148
|
+
try {
|
149
|
+
console.log(`⚡ 適用中: ${migration.filename}`);
|
150
|
+
const content = fs.readFileSync(migration.filepath, 'utf-8');
|
151
|
+
const cleanContent = cleanSqlContent(content);
|
152
|
+
if (!cleanContent) {
|
153
|
+
console.log(`⚠️ 空のマイグレーションファイル: ${migration.filename}`);
|
154
|
+
return false;
|
155
|
+
}
|
156
|
+
// トランザクション内で実行
|
157
|
+
await client.query('BEGIN');
|
158
|
+
try {
|
159
|
+
// マイグレーションSQLを実行
|
160
|
+
await client.query(cleanContent);
|
161
|
+
// 既存のマイグレーション管理テーブルに記録(テーブルが存在する場合のみ)
|
162
|
+
const migrationTable = await findMigrationTable(client);
|
163
|
+
if (migrationTable) {
|
164
|
+
await client.query(`
|
165
|
+
INSERT INTO ${migrationTable} (version, inserted_at)
|
166
|
+
VALUES ($1, NOW())
|
167
|
+
ON CONFLICT (version) DO NOTHING
|
168
|
+
`, [migration.timestamp]);
|
169
|
+
}
|
170
|
+
await client.query('COMMIT');
|
171
|
+
console.log(`✅ 成功: ${migration.filename}`);
|
172
|
+
return true;
|
173
|
+
}
|
174
|
+
catch (error) {
|
175
|
+
await client.query('ROLLBACK');
|
176
|
+
throw error;
|
177
|
+
}
|
178
|
+
}
|
179
|
+
catch (error) {
|
180
|
+
console.error(`❌ 失敗: ${migration.filename}`);
|
181
|
+
if (error instanceof Error) {
|
182
|
+
console.error(` エラー: ${error.message}`);
|
183
|
+
}
|
184
|
+
return false;
|
185
|
+
}
|
186
|
+
}
|
187
|
+
/**
|
188
|
+
* 保留中のマイグレーションを適用
|
189
|
+
*/
|
190
|
+
async function runMigrationsUp(connectionString, projectDir = '.') {
|
191
|
+
const migrationsDir = path.join(projectDir, 'supabase', 'migrations');
|
192
|
+
// PostgreSQL接続(fetchRemoteSchemas.tsと同じ接続ロジックを使用)
|
193
|
+
console.log('データベースに接続中...');
|
194
|
+
const url = new URL(connectionString);
|
195
|
+
console.log(`接続先: ${url.hostname}:${url.port}`);
|
196
|
+
let client;
|
197
|
+
try {
|
198
|
+
// 接続試行(fetchRemoteSchemas.tsと同じフォールバック戦略)
|
199
|
+
const clientConfig = {
|
200
|
+
connectionString,
|
201
|
+
ssl: { rejectUnauthorized: false },
|
202
|
+
statement_timeout: 30000,
|
203
|
+
query_timeout: 30000,
|
204
|
+
connectionTimeoutMillis: 10000,
|
205
|
+
idleTimeoutMillis: 10000
|
206
|
+
};
|
207
|
+
try {
|
208
|
+
client = new pg_1.Client(clientConfig);
|
209
|
+
await client.connect();
|
210
|
+
}
|
211
|
+
catch (sslError) {
|
212
|
+
console.log('SSL接続失敗、SSL無効で再試行中...');
|
213
|
+
const noSslConnectionString = connectionString.replace('sslmode=require', 'sslmode=disable');
|
214
|
+
client = new pg_1.Client({
|
215
|
+
...clientConfig,
|
216
|
+
connectionString: noSslConnectionString,
|
217
|
+
ssl: false
|
218
|
+
});
|
219
|
+
await client.connect();
|
220
|
+
}
|
221
|
+
console.log('✅ データベース接続成功');
|
222
|
+
// マイグレーションファイルをスキャン
|
223
|
+
const migrations = scanMigrationFiles(migrationsDir);
|
224
|
+
if (migrations.length === 0) {
|
225
|
+
console.log('📝 マイグレーションファイルが見つかりません');
|
226
|
+
return;
|
227
|
+
}
|
228
|
+
// 適用済みマイグレーションを取得
|
229
|
+
const appliedMigrations = await getAppliedMigrations(client);
|
230
|
+
// 未適用のマイグレーションをフィルタ(タイムスタンプで比較)
|
231
|
+
const pendingMigrations = migrations.filter(m => !appliedMigrations.has(m.timestamp));
|
232
|
+
if (pendingMigrations.length === 0) {
|
233
|
+
console.log('🎉 すべてのマイグレーションが適用済みです');
|
234
|
+
return;
|
235
|
+
}
|
236
|
+
console.log(`📋 ${pendingMigrations.length} 個の未適用マイグレーションが見つかりました:`);
|
237
|
+
pendingMigrations.forEach(m => {
|
238
|
+
console.log(` - ${m.filename}`);
|
239
|
+
});
|
240
|
+
// マイグレーションを順次適用
|
241
|
+
let successCount = 0;
|
242
|
+
let failCount = 0;
|
243
|
+
for (const migration of pendingMigrations) {
|
244
|
+
const success = await applyMigration(client, migration, projectDir);
|
245
|
+
if (success) {
|
246
|
+
successCount++;
|
247
|
+
}
|
248
|
+
else {
|
249
|
+
failCount++;
|
250
|
+
break; // エラーが発生したら停止
|
251
|
+
}
|
252
|
+
}
|
253
|
+
console.log(`\n📊 マイグレーション完了:`);
|
254
|
+
console.log(` 成功: ${successCount} 個`);
|
255
|
+
if (failCount > 0) {
|
256
|
+
console.log(` 失敗: ${failCount} 個`);
|
257
|
+
}
|
258
|
+
}
|
259
|
+
catch (error) {
|
260
|
+
console.error('❌ マイグレーション実行エラー:');
|
261
|
+
if (error instanceof Error) {
|
262
|
+
console.error(error.message);
|
263
|
+
}
|
264
|
+
throw error;
|
265
|
+
}
|
266
|
+
finally {
|
267
|
+
if (client) {
|
268
|
+
await client.end();
|
269
|
+
}
|
270
|
+
}
|
271
|
+
}
|
@@ -0,0 +1,97 @@
|
|
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.parseLocalSchemas = parseLocalSchemas;
|
37
|
+
const fs = __importStar(require("fs"));
|
38
|
+
const path = __importStar(require("path"));
|
39
|
+
/**
|
40
|
+
* DDL文字列を正規化(空白・改行・タブを統一)
|
41
|
+
*/
|
42
|
+
function normalizeDDL(ddl) {
|
43
|
+
return ddl
|
44
|
+
.replace(/\s+/g, ' ') // 連続する空白文字を1つのスペースに
|
45
|
+
.replace(/;\s+/g, ';\n') // セミコロン後に改行
|
46
|
+
.trim(); // 前後の空白を削除
|
47
|
+
}
|
48
|
+
/**
|
49
|
+
* ローカルSQLファイルからスキーマを解析
|
50
|
+
*/
|
51
|
+
async function parseLocalSchemas(schemaDir) {
|
52
|
+
const schemas = {};
|
53
|
+
if (!fs.existsSync(schemaDir)) {
|
54
|
+
return schemas;
|
55
|
+
}
|
56
|
+
const files = fs.readdirSync(schemaDir);
|
57
|
+
for (const file of files) {
|
58
|
+
if (!file.endsWith('.sql'))
|
59
|
+
continue;
|
60
|
+
const filePath = path.join(schemaDir, file);
|
61
|
+
const stats = fs.statSync(filePath);
|
62
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
63
|
+
// ファイル名からテーブル名を取得(.sqlを除く)
|
64
|
+
const tableName = path.basename(file, '.sql');
|
65
|
+
// ファイル内のコメントからタイムスタンプを抽出
|
66
|
+
let timestamp = Math.floor(stats.mtime.getTime() / 1000); // フォールバック
|
67
|
+
const timestampMatch = fileContent.match(/-- Remote last updated: (.+)/);
|
68
|
+
if (timestampMatch) {
|
69
|
+
try {
|
70
|
+
const dateStr = timestampMatch[1];
|
71
|
+
const parsedDate = new Date(dateStr);
|
72
|
+
if (!isNaN(parsedDate.getTime())) {
|
73
|
+
timestamp = Math.floor(parsedDate.getTime() / 1000);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
catch (error) {
|
77
|
+
// エラーの場合はファイル更新日時を使用
|
78
|
+
}
|
79
|
+
}
|
80
|
+
// DDL部分のみを抽出(コメント行と空白行を完全に除外)
|
81
|
+
const ddlLines = fileContent.split('\n').filter(line => {
|
82
|
+
const trimmed = line.trim();
|
83
|
+
return trimmed && !trimmed.startsWith('--');
|
84
|
+
});
|
85
|
+
const rawDDL = ddlLines.join('\n').trim();
|
86
|
+
// DDLを正規化
|
87
|
+
const ddl = normalizeDDL(rawDDL);
|
88
|
+
schemas[tableName] = {
|
89
|
+
ddl: rawDDL,
|
90
|
+
normalizedDdl: normalizeDDL(rawDDL),
|
91
|
+
timestamp,
|
92
|
+
fileTimestamp: Math.floor(stats.mtime.getTime() / 1000),
|
93
|
+
filePath
|
94
|
+
};
|
95
|
+
}
|
96
|
+
return schemas;
|
97
|
+
}
|
@@ -0,0 +1,208 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.syncAllTables = syncAllTables;
|
4
|
+
const parseLocalSchemas_1 = require("./parseLocalSchemas");
|
5
|
+
const fetchRemoteSchemas_1 = require("./fetchRemoteSchemas");
|
6
|
+
const writeSchema_1 = require("./writeSchema");
|
7
|
+
const generateMigration_1 = require("./generateMigration");
|
8
|
+
const diff_1 = require("diff");
|
9
|
+
const utils_1 = require("./utils");
|
10
|
+
// グローバル承認状態(writeSchema.tsと共有)
|
11
|
+
let globalApproveAll = false;
|
12
|
+
/**
|
13
|
+
* DDL文字列を正規化(空白・改行・タブを統一)
|
14
|
+
*/
|
15
|
+
function normalizeDDL(ddl) {
|
16
|
+
return ddl
|
17
|
+
.replace(/\s+/g, ' ') // 連続する空白文字を1つのスペースに
|
18
|
+
.replace(/;\s+/g, ';\n') // セミコロン後に改行
|
19
|
+
.trim(); // 前後の空白を削除
|
20
|
+
}
|
21
|
+
/**
|
22
|
+
* SQLを見やすく整形
|
23
|
+
*/
|
24
|
+
function formatSQL(sql) {
|
25
|
+
return sql
|
26
|
+
.replace(/,\s*/g, ',\n ') // カンマ後に改行とインデント
|
27
|
+
.replace(/\(\s*/g, ' (\n ') // 開き括弧後に改行とインデント
|
28
|
+
.replace(/\s*\)/g, '\n)') // 閉じ括弧前に改行
|
29
|
+
.replace(/\bCREATE\s+TABLE\b/g, '\nCREATE TABLE') // CREATE TABLE前に改行
|
30
|
+
.replace(/\bPRIMARY\s+KEY\b/g, '\n PRIMARY KEY') // PRIMARY KEY前に改行とインデント
|
31
|
+
.replace(/;\s*/g, ';\n') // セミコロン後に改行
|
32
|
+
.split('\n')
|
33
|
+
.map(line => line.trim())
|
34
|
+
.filter(line => line)
|
35
|
+
.join('\n');
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* すべてのテーブルスキーマを同期
|
39
|
+
*/
|
40
|
+
async function syncAllTables({ connectionString, schemaDir, tablePattern = '*', force = false }) {
|
41
|
+
// 承認状態をリセット
|
42
|
+
(0, writeSchema_1.resetApprovalState)();
|
43
|
+
const localSchemas = await (0, parseLocalSchemas_1.parseLocalSchemas)(schemaDir);
|
44
|
+
const remoteSchemas = await (0, fetchRemoteSchemas_1.fetchRemoteSchemas)(connectionString);
|
45
|
+
const allTables = new Set([...Object.keys(localSchemas), ...Object.keys(remoteSchemas)]);
|
46
|
+
const remoteTables = new Set(Object.keys(remoteSchemas));
|
47
|
+
// 存在しないテーブルのファイルをバックアップ
|
48
|
+
await (0, writeSchema_1.backupOrphanedFiles)(schemaDir, remoteTables, force);
|
49
|
+
for (const tableName of allTables) {
|
50
|
+
if (!(0, utils_1.wildcardMatch)(tableName, tablePattern)) {
|
51
|
+
continue;
|
52
|
+
}
|
53
|
+
const local = localSchemas[tableName];
|
54
|
+
const remote = remoteSchemas[tableName];
|
55
|
+
if (local && !remote) {
|
56
|
+
console.log(`[${tableName}] ローカルのみ - リモートに存在しません(バックアップ済み)`);
|
57
|
+
}
|
58
|
+
else if (!local && remote) {
|
59
|
+
console.log(`[${tableName}] リモートのみ - スキーマを取得`);
|
60
|
+
const success = await (0, writeSchema_1.writeSchemaToFile)(remote.ddl, schemaDir, remote.timestamp, `${tableName}.sql`, tableName, force);
|
61
|
+
if (success) {
|
62
|
+
console.log(`[${tableName}] スキーマファイルを作成しました`);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
else if (local && remote) {
|
66
|
+
// 最初にDDLの差分をチェック
|
67
|
+
const normalizedLocal = local.normalizedDdl;
|
68
|
+
const normalizedRemote = normalizeDDL(remote.ddl);
|
69
|
+
const diff = (0, diff_1.diffLines)(normalizedLocal, normalizedRemote);
|
70
|
+
const hasDiff = diff.some(part => part.added || part.removed);
|
71
|
+
// 差分がある場合のみ処理・表示
|
72
|
+
if (hasDiff) {
|
73
|
+
// タイムスタンプ比較(ローカルファイルの実際の更新時刻も考慮)
|
74
|
+
const isRemoteNewer = remote.timestamp > local.timestamp;
|
75
|
+
const isLocalFileNewer = local.fileTimestamp > remote.timestamp;
|
76
|
+
const timeDiff = remote.timestamp - local.timestamp;
|
77
|
+
const fileDiff = local.fileTimestamp - remote.timestamp;
|
78
|
+
const timeDiffHours = Math.abs(timeDiff) / 3600;
|
79
|
+
const fileDiffHours = Math.abs(fileDiff) / 3600;
|
80
|
+
// ローカルファイルの方が新しい場合はリモートへマイグレーション提案
|
81
|
+
if (isLocalFileNewer) {
|
82
|
+
console.log(`[${tableName}] ローカルが ${fileDiffHours.toFixed(1)}時間新しい - マイグレーション生成`);
|
83
|
+
}
|
84
|
+
else if (isRemoteNewer) {
|
85
|
+
console.log(`[${tableName}] リモートが ${timeDiffHours.toFixed(1)}時間新しい - ローカル更新`);
|
86
|
+
}
|
87
|
+
else {
|
88
|
+
console.log(`[${tableName}] 差分を検出 - 確認が必要`);
|
89
|
+
}
|
90
|
+
// 整形されたSQLで行単位の差分を表示
|
91
|
+
const formattedLocal = formatSQL(normalizedLocal);
|
92
|
+
const formattedRemote = formatSQL(normalizedRemote);
|
93
|
+
// ローカルが新しい場合は、ローカル→リモートの差分(マイグレーション用)
|
94
|
+
// リモートが新しい場合は、リモート→ローカルの差分(ローカル更新用)
|
95
|
+
const lineDiff = isLocalFileNewer
|
96
|
+
? (0, diff_1.diffLines)(formattedRemote, formattedLocal) // ローカル→リモート(マイグレーション用)
|
97
|
+
: (0, diff_1.diffLines)(formattedLocal, formattedRemote); // リモート→ローカル(ローカル更新用)
|
98
|
+
// 前後のコンテキスト行数
|
99
|
+
const contextLines = 1;
|
100
|
+
let lineNumber = 0;
|
101
|
+
let outputLines = [];
|
102
|
+
lineDiff.forEach((part, index) => {
|
103
|
+
const lines = part.value.split('\n').filter(line => line.trim() || index === lineDiff.length - 1);
|
104
|
+
if (part.added) {
|
105
|
+
// リモートから追加される行
|
106
|
+
lines.forEach(line => {
|
107
|
+
if (line.trim()) {
|
108
|
+
outputLines.push(`\x1b[32m+ ${line}\x1b[0m`);
|
109
|
+
}
|
110
|
+
});
|
111
|
+
}
|
112
|
+
else if (part.removed) {
|
113
|
+
// ローカルから削除される行
|
114
|
+
lines.forEach(line => {
|
115
|
+
if (line.trim()) {
|
116
|
+
outputLines.push(`\x1b[31m- ${line}\x1b[0m`);
|
117
|
+
}
|
118
|
+
});
|
119
|
+
}
|
120
|
+
else {
|
121
|
+
// 変更されていない行(コンテキスト)
|
122
|
+
const unchangedLines = lines.filter(line => line.trim());
|
123
|
+
// 差分の前後で適切にコンテキストを表示
|
124
|
+
const hasChangeBefore = index > 0 && (lineDiff[index - 1].added || lineDiff[index - 1].removed);
|
125
|
+
const hasChangeAfter = index < lineDiff.length - 1 && (lineDiff[index + 1].added || lineDiff[index + 1].removed);
|
126
|
+
if (hasChangeBefore && hasChangeAfter) {
|
127
|
+
// 前後に変更がある場合は全行表示
|
128
|
+
unchangedLines.forEach(line => {
|
129
|
+
outputLines.push(` ${line}`);
|
130
|
+
});
|
131
|
+
}
|
132
|
+
else if (hasChangeBefore) {
|
133
|
+
// 前に変更がある場合は最初の数行のみ表示
|
134
|
+
const showLines = Math.min(contextLines, unchangedLines.length);
|
135
|
+
for (let i = 0; i < showLines; i++) {
|
136
|
+
outputLines.push(` ${unchangedLines[i]}`);
|
137
|
+
}
|
138
|
+
if (unchangedLines.length > contextLines) {
|
139
|
+
outputLines.push(` \x1b[36m...(${unchangedLines.length - contextLines} more lines)...\x1b[0m`);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
else if (hasChangeAfter) {
|
143
|
+
// 後に変更がある場合は最後の数行のみ表示
|
144
|
+
const showLines = Math.min(contextLines, unchangedLines.length);
|
145
|
+
const startIndex = unchangedLines.length - showLines;
|
146
|
+
// 冒頭のCREATE TABLE行は常に表示
|
147
|
+
const hasCreateTable = unchangedLines.some(line => line.trim().toUpperCase().startsWith('CREATE TABLE'));
|
148
|
+
if (hasCreateTable && startIndex > 0) {
|
149
|
+
// CREATE TABLE行を探して表示
|
150
|
+
const createTableIndex = unchangedLines.findIndex(line => line.trim().toUpperCase().startsWith('CREATE TABLE'));
|
151
|
+
if (createTableIndex >= 0) {
|
152
|
+
outputLines.push(` ${unchangedLines[createTableIndex]}`);
|
153
|
+
if (createTableIndex < startIndex - 1) {
|
154
|
+
outputLines.push(` \x1b[36m...(${startIndex - createTableIndex - 1} lines)...\x1b[0m`);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
}
|
158
|
+
else if (startIndex > 0) {
|
159
|
+
outputLines.push(` \x1b[36m...(${startIndex} lines)...\x1b[0m`);
|
160
|
+
}
|
161
|
+
for (let i = startIndex; i < unchangedLines.length; i++) {
|
162
|
+
outputLines.push(` ${unchangedLines[i]}`);
|
163
|
+
}
|
164
|
+
}
|
165
|
+
else if (unchangedLines.length <= contextLines * 2) {
|
166
|
+
// 短い場合は全部表示
|
167
|
+
unchangedLines.forEach(line => {
|
168
|
+
outputLines.push(` ${line}`);
|
169
|
+
});
|
170
|
+
}
|
171
|
+
else {
|
172
|
+
// 長い場合でも冒頭のCREATE TABLE行は表示
|
173
|
+
const hasCreateTable = unchangedLines.some(line => line.trim().toUpperCase().startsWith('CREATE TABLE'));
|
174
|
+
if (hasCreateTable) {
|
175
|
+
const createTableIndex = unchangedLines.findIndex(line => line.trim().toUpperCase().startsWith('CREATE TABLE'));
|
176
|
+
if (createTableIndex >= 0) {
|
177
|
+
outputLines.push(` ${unchangedLines[createTableIndex]}`);
|
178
|
+
if (unchangedLines.length > 1) {
|
179
|
+
outputLines.push(` \x1b[36m...(${unchangedLines.length - 1} unchanged lines)...\x1b[0m`);
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
else {
|
184
|
+
outputLines.push(` \x1b[36m...(${unchangedLines.length} unchanged lines)...\x1b[0m`);
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
});
|
189
|
+
outputLines.forEach(line => console.log(line));
|
190
|
+
if (isLocalFileNewer) {
|
191
|
+
// マイグレーションファイルを生成(ローカル→リモートの差分)
|
192
|
+
const migrationPath = await (0, generateMigration_1.generateMigrationFile)(tableName, normalizedRemote, // from(現在のリモート状態)
|
193
|
+
normalizedLocal, // to(ローカルの目標状態)
|
194
|
+
process.cwd());
|
195
|
+
}
|
196
|
+
else {
|
197
|
+
// リモートが新しいかタイムスタンプが同じ場合:ローカルを更新
|
198
|
+
const shouldAutoUpdate = isRemoteNewer && !isLocalFileNewer;
|
199
|
+
const success = await (0, writeSchema_1.writeSchemaToFile)(remote.ddl, schemaDir, remote.timestamp, `${tableName}.sql`, tableName, shouldAutoUpdate);
|
200
|
+
if (success) {
|
201
|
+
console.log(`[${tableName}] ローカルファイルを更新しました`);
|
202
|
+
}
|
203
|
+
}
|
204
|
+
}
|
205
|
+
// 差分がない場合は何も表示せず、何もしない
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
"use strict";
|
2
|
+
// readline import removed - using raw mode instead
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
4
|
+
exports.wildcardMatch = wildcardMatch;
|
5
|
+
exports.askUserConfirmation = askUserConfirmation;
|
6
|
+
// ワイルドカードマッチング関数
|
7
|
+
function wildcardMatch(str, pattern) {
|
8
|
+
return pattern === '*' || str.includes(pattern);
|
9
|
+
}
|
10
|
+
/**
|
11
|
+
* ユーザーに確認を求める(1文字入力で即座に判定)
|
12
|
+
*/
|
13
|
+
function askUserConfirmation(message, options) {
|
14
|
+
return new Promise((resolve) => {
|
15
|
+
const promptMessage = options?.message || '(y/N): ';
|
16
|
+
process.stdout.write(`${message} ${promptMessage}`);
|
17
|
+
// rawモードを有効にして1文字入力を受け取る
|
18
|
+
process.stdin.setRawMode(true);
|
19
|
+
process.stdin.resume();
|
20
|
+
process.stdin.setEncoding('utf8');
|
21
|
+
const onData = (key) => {
|
22
|
+
// クリーンアップ
|
23
|
+
process.stdin.setRawMode(false);
|
24
|
+
process.stdin.pause();
|
25
|
+
process.stdin.removeListener('data', onData);
|
26
|
+
const lowerKey = key.toLowerCase();
|
27
|
+
if (lowerKey === 'y') {
|
28
|
+
console.log('y');
|
29
|
+
resolve(true);
|
30
|
+
}
|
31
|
+
else if (options?.allowAll && lowerKey === 'a') {
|
32
|
+
console.log('a');
|
33
|
+
resolve('all');
|
34
|
+
}
|
35
|
+
else if (lowerKey === 'n' || key === '\r' || key === '\n' || key === '\u0003') {
|
36
|
+
// n、Enter、または Ctrl+C
|
37
|
+
if (key === '\u0003') {
|
38
|
+
console.log('\nOperation cancelled');
|
39
|
+
process.exit(0);
|
40
|
+
}
|
41
|
+
console.log(lowerKey === 'n' ? 'n' : 'N');
|
42
|
+
resolve(false);
|
43
|
+
}
|
44
|
+
else {
|
45
|
+
// 無効なキー:デフォルトでfalse
|
46
|
+
console.log(`${key} (invalid input, treating as N)`);
|
47
|
+
resolve(false);
|
48
|
+
}
|
49
|
+
};
|
50
|
+
process.stdin.on('data', onData);
|
51
|
+
});
|
52
|
+
}
|