UNPKG

@gftdcojp/gftd-cli

Version:

GFTD Schema Management CLI - Drizzle-like schema management for Kafka and ksqlDB

542 lines (536 loc) 22.1 kB
"use strict"; /** * Database Manager - Supabase-like CLI with Drizzle-style migrations * Comprehensive database management for GFTD CLI */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseManager = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("node:path")); const node_crypto_1 = require("node:crypto"); const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); class DatabaseManager { constructor(configPath) { this.configPath = configPath || path.join(process.cwd(), 'gftd.json'); this.migrationsPath = path.join(process.cwd(), 'migrations'); this.schemasPath = path.join(process.cwd(), 'schemas'); this.seedsPath = path.join(process.cwd(), 'seeds'); this.config = this.loadDatabaseConfig(); } /** * プロジェクトのデータベース初期化 (supabase init相当) */ async init(options = {}) { const spinner = (0, ora_1.default)('Initializing GFTD database project...').start(); try { // ディレクトリ構造を作成 await this.createDirectoryStructure(); // 設定ファイルを作成/更新 await this.createConfigFile(options); // スキーマテンプレートを作成 await this.createSchemaTemplates(options); // 初期マイグレーションを作成 await this.createInitialMigration(); spinner.succeed('Database project initialized successfully!'); console.log(chalk_1.default.green('\n✨ Your GFTD database project is ready!')); console.log(chalk_1.default.gray('Next steps:')); console.log(chalk_1.default.gray(' 1. Configure your database in gftd.json')); console.log(chalk_1.default.gray(' 2. Define your schema in schemas/')); console.log(chalk_1.default.gray(' 3. Run: gftd migration generate')); console.log(chalk_1.default.gray(' 4. Run: gftd migration up')); } catch (error) { spinner.fail('Failed to initialize database project'); throw error; } } /** * マイグレーション生成 (drizzle generate相当) */ async generateMigration(name, options = {}) { const spinner = (0, ora_1.default)('Generating migration...').start(); try { const migrationName = name || `migration_${Date.now()}`; const timestamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '_').slice(0, 15); const version = `${timestamp}`; // スキーマファイルから変更を検出 const schemaChanges = await this.detectSchemaChanges(); if (!schemaChanges || schemaChanges.length === 0) { spinner.warn('No schema changes detected'); return; } // マイグレーションファイルを生成 const migrationFile = await this.createMigrationFile(version, migrationName, schemaChanges); if (options.dry) { spinner.info('Dry run - Migration preview:'); console.log(chalk_1.default.cyan('\n--- Migration Up ---')); console.log(migrationFile.up); console.log(chalk_1.default.cyan('\n--- Migration Down ---')); console.log(migrationFile.down); return; } // ファイルに保存 await this.saveMigrationFile(migrationFile); spinner.succeed(`Migration generated: ${migrationFile.name}`); console.log(chalk_1.default.green(`📝 Created: migrations/${version}_${migrationName}.sql`)); } catch (error) { spinner.fail('Failed to generate migration'); throw error; } } /** * マイグレーション実行 (supabase migration up相当) */ async runMigrationsUp(options = {}) { const spinner = (0, ora_1.default)('Applying migrations...').start(); try { const pendingMigrations = await this.getPendingMigrations(); if (pendingMigrations.length === 0) { spinner.info('No pending migrations'); return; } const connection = await this.getDbConnection(); for (const migration of pendingMigrations) { if (options.dry) { console.log(chalk_1.default.cyan(`Would apply: ${migration.name}`)); continue; } spinner.text = `Applying ${migration.name}...`; await this.executeMigration(connection, migration, 'up'); await this.recordMigration(connection, migration); console.log(chalk_1.default.green(`✓ Applied: ${migration.name}`)); } await this.closeDbConnection(connection); if (options.dry) { spinner.info(`Dry run completed. ${pendingMigrations.length} migrations would be applied`); } else { spinner.succeed(`Applied ${pendingMigrations.length} migrations`); } } catch (error) { spinner.fail('Failed to apply migrations'); throw error; } } /** * マイグレーション巻き戻し (supabase migration down相当) */ async runMigrationsDown(options = {}) { const spinner = (0, ora_1.default)('Rolling back migrations...').start(); try { const appliedMigrations = await this.getAppliedMigrations(); const steps = options.steps || 1; const migrationsToRollback = appliedMigrations.slice(-steps); if (migrationsToRollback.length === 0) { spinner.info('No migrations to rollback'); return; } const connection = await this.getDbConnection(); for (const migration of migrationsToRollback.reverse()) { if (options.dry) { console.log(chalk_1.default.yellow(`Would rollback: ${migration.name}`)); continue; } spinner.text = `Rolling back ${migration.name}...`; // Find the migration file for this version const allMigrations = await this.getAllMigrations(); const migrationFile = allMigrations.find(m => m.version === migration.version); if (migrationFile) { await this.executeMigration(connection, migrationFile, 'down'); } await this.removeMigrationRecord(connection, migration); console.log(chalk_1.default.yellow(`↩ Rolled back: ${migration.name}`)); } await this.closeDbConnection(connection); if (options.dry) { spinner.info(`Dry run completed. ${migrationsToRollback.length} migrations would be rolled back`); } else { spinner.succeed(`Rolled back ${migrationsToRollback.length} migrations`); } } catch (error) { spinner.fail('Failed to rollback migrations'); throw error; } } /** * マイグレーション状態表示 (supabase migration list相当) */ async listMigrations() { const spinner = (0, ora_1.default)('Fetching migration status...').start(); try { const allMigrations = await this.getAllMigrations(); const appliedMigrations = await this.getAppliedMigrations(); const appliedVersions = new Set(appliedMigrations.map(m => m.version)); spinner.stop(); console.log(chalk_1.default.bold('\n📋 Migration Status\n')); console.log(chalk_1.default.gray('VERSION'.padEnd(20) + 'NAME'.padEnd(30) + 'STATUS'.padEnd(15) + 'APPLIED')); console.log(chalk_1.default.gray('─'.repeat(80))); for (const migration of allMigrations) { const applied = appliedVersions.has(migration.version); const status = applied ? chalk_1.default.green('✓ Applied') : chalk_1.default.gray('○ Pending'); const appliedAt = applied ? appliedMigrations.find(m => m.version === migration.version)?.appliedAt?.toLocaleString() || '-' : '-'; console.log(migration.version.padEnd(20) + migration.name.padEnd(30) + status.padEnd(25) + chalk_1.default.gray(appliedAt)); } const pendingCount = allMigrations.length - appliedMigrations.length; console.log(chalk_1.default.gray('\n' + '─'.repeat(80))); console.log(chalk_1.default.cyan(`Total: ${allMigrations.length} migrations`)); console.log(chalk_1.default.green(`Applied: ${appliedMigrations.length}`)); console.log(chalk_1.default.yellow(`Pending: ${pendingCount}`)); } catch (error) { spinner.fail('Failed to list migrations'); throw error; } } /** * TypeScript型生成 (supabase gen types相当) */ async generateTypes(options = {}) { const spinner = (0, ora_1.default)('Generating TypeScript types...').start(); try { const outputPath = options.output || path.join(process.cwd(), 'types', 'database.ts'); const connection = await this.getDbConnection(); // データベースからスキーマ情報を取得 const schemaInfo = await this.getSchemaInfo(connection); // TypeScript型定義を生成 const typeDefinitions = this.generateTypeDefinitions(schemaInfo); // ファイルに保存 await fs.ensureDir(path.dirname(outputPath)); await fs.writeFile(outputPath, typeDefinitions); await this.closeDbConnection(connection); spinner.succeed('Types generated successfully!'); console.log(chalk_1.default.green(`📝 Generated: ${outputPath}`)); if (options.watch) { this.startTypeGenerationWatcher(outputPath); } } catch (error) { spinner.fail('Failed to generate types'); throw error; } } /** * スキーマプル (supabase db pull相当) */ async pullSchema(options = {}) { const spinner = (0, ora_1.default)('Pulling schema from database...').start(); try { const connection = await this.getDbConnection(); const currentSchema = await this.getSchemaInfo(connection); // スキーマファイルを生成 const schemaCode = this.generateSchemaCode(currentSchema); const schemaPath = path.join(this.schemasPath, 'schema.ts'); if (await fs.pathExists(schemaPath) && !options.force) { spinner.warn('Schema file already exists. Use --force to overwrite'); return; } await fs.writeFile(schemaPath, schemaCode); await this.closeDbConnection(connection); spinner.succeed('Schema pulled successfully!'); console.log(chalk_1.default.green(`📥 Updated: ${schemaPath}`)); } catch (error) { spinner.fail('Failed to pull schema'); throw error; } } /** * スキーマプッシュ (supabase db push相当) */ async pushSchema(options = {}) { const spinner = (0, ora_1.default)('Pushing schema to database...').start(); try { if (options.drop) { const confirmed = await this.confirmDangerousOperation('drop all tables'); if (!confirmed) { spinner.stop(); return; } } await this.generateMigration('schema_push', { dry: false }); await this.runMigrationsUp({ dry: false }); if (options.seed) { await this.runSeeders(); } spinner.succeed('Schema pushed successfully!'); } catch (error) { spinner.fail('Failed to push schema'); throw error; } } // --- プライベートメソッド --- loadDatabaseConfig() { try { if (fs.existsSync(this.configPath)) { const config = fs.readJsonSync(this.configPath); return config.database || this.getDefaultConfig(); } return this.getDefaultConfig(); } catch (error) { return this.getDefaultConfig(); } } getDefaultConfig() { return { type: 'postgresql', host: 'localhost', port: 5432, database: 'gftd_dev', username: 'postgres', password: 'postgres', migrationsPath: 'migrations', seedsPath: 'seeds' }; } async createDirectoryStructure() { await fs.ensureDir(this.migrationsPath); await fs.ensureDir(this.schemasPath); await fs.ensureDir(this.seedsPath); await fs.ensureDir(path.join(process.cwd(), 'types')); } async createConfigFile(options) { const existingConfig = fs.existsSync(this.configPath) ? fs.readJsonSync(this.configPath) : {}; const databaseConfig = { type: options.type || 'postgresql', host: 'localhost', port: options.type === 'mysql' ? 3306 : 5432, database: options.database || 'gftd_dev', username: 'postgres', password: 'postgres', migrationsPath: 'migrations', seedsPath: 'seeds' }; const config = { ...existingConfig, database: databaseConfig, migrations: { path: 'migrations', tableName: '_gftd_migrations', schemaName: 'public', createSchema: true } }; await fs.writeJson(this.configPath, config, { spaces: 2 }); } async createSchemaTemplates(options) { const schemaTemplate = `/** * GFTD Database Schema * Define your database schema using TypeScript */ export interface User { id: number; email: string; name?: string; createdAt: Date; updatedAt: Date; } export interface Post { id: number; title: string; content: string; authorId: number; published: boolean; createdAt: Date; updatedAt: Date; } // Export all schema types export type DatabaseSchema = { users: User; posts: Post; }; `; await fs.writeFile(path.join(this.schemasPath, 'schema.ts'), schemaTemplate); } async createInitialMigration() { const timestamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', '_').slice(0, 15); const migrationContent = `-- Initial migration -- Create migrations tracking table CREATE TABLE IF NOT EXISTS _gftd_migrations ( id SERIAL PRIMARY KEY, version VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, checksum VARCHAR(255) NOT NULL, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Create initial schema tables CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT, author_id INTEGER REFERENCES users(id) ON DELETE CASCADE, published BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );`; const downContent = `-- Rollback initial migration DROP TABLE IF EXISTS posts; DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS _gftd_migrations;`; await fs.writeFile(path.join(this.migrationsPath, `${timestamp}_initial_migration.up.sql`), migrationContent); await fs.writeFile(path.join(this.migrationsPath, `${timestamp}_initial_migration.down.sql`), downContent); } async detectSchemaChanges() { // スキーマファイルの変更を検出してSQL文を生成 // 実装は複雑になるため、ここでは簡略化 return [ 'CREATE TABLE example (id SERIAL PRIMARY KEY, name VARCHAR(255));' ]; } async createMigrationFile(version, name, changes) { const upSql = changes.join('\n\n'); const downSql = '-- Migration rollback statements\n-- TODO: Add rollback statements'; return { version, name, up: upSql, down: downSql, timestamp: new Date(), checksum: (0, node_crypto_1.createHash)('md5').update(upSql).digest('hex') }; } async saveMigrationFile(migration) { const fileName = `${migration.version}_${migration.name}`; await fs.writeFile(path.join(this.migrationsPath, `${fileName}.up.sql`), migration.up); await fs.writeFile(path.join(this.migrationsPath, `${fileName}.down.sql`), migration.down); } async getPendingMigrations() { const allMigrations = await this.getAllMigrations(); const appliedMigrations = await this.getAppliedMigrations(); const appliedVersions = new Set(appliedMigrations.map(m => m.version)); return allMigrations.filter(m => !appliedVersions.has(m.version)); } async getAllMigrations() { const files = await fs.readdir(this.migrationsPath); const migrationFiles = files .filter(f => f.endsWith('.up.sql')) .sort(); const migrations = []; for (const file of migrationFiles) { const [version, ...nameParts] = path.basename(file, '.up.sql').split('_'); const name = nameParts.join('_'); const upContent = await fs.readFile(path.join(this.migrationsPath, file), 'utf8'); const downFile = file.replace('.up.sql', '.down.sql'); const downContent = await fs.pathExists(path.join(this.migrationsPath, downFile)) ? await fs.readFile(path.join(this.migrationsPath, downFile), 'utf8') : ''; migrations.push({ version, name, up: upContent, down: downContent, timestamp: new Date(), checksum: (0, node_crypto_1.createHash)('md5').update(upContent).digest('hex') }); } return migrations; } async getAppliedMigrations() { // データベースから適用済みマイグレーション情報を取得 // 実装は簡略化 return []; } async getDbConnection() { // データベース接続を取得 // 実装は簡略化 return {}; } async closeDbConnection(connection) { // データベース接続を閉じる } async executeMigration(connection, migration, direction) { // マイグレーションを実行 } async recordMigration(connection, migration) { // マイグレーション実行記録を保存 } async removeMigrationRecord(connection, migration) { // マイグレーション実行記録を削除 } async getSchemaInfo(connection) { // スキーマ情報を取得 return {}; } generateTypeDefinitions(schemaInfo) { // TypeScript型定義を生成 return `// Generated types from database schema export interface DatabaseTables { // TODO: Generate from schema }`; } generateSchemaCode(schemaInfo) { // スキーマコードを生成 return `// Generated schema from database export const schema = { // TODO: Generate from database };`; } startTypeGenerationWatcher(outputPath) { // ファイル監視を開始 console.log(chalk_1.default.blue('👀 Watching for schema changes...')); } async runSeeders() { // シーダーを実行 console.log(chalk_1.default.blue('🌱 Running seeders...')); } async confirmDangerousOperation(operation) { // 危険な操作の確認 return true; // 簡略化 } } exports.DatabaseManager = DatabaseManager; //# sourceMappingURL=database-manager.js.map