@gftdcojp/gftd-cli
Version:
GFTD Schema Management CLI - Drizzle-like schema management for Kafka and ksqlDB
542 lines (536 loc) • 22.1 kB
JavaScript
;
/**
* 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