From 77e27a4b9349c8b676a540b85ae6525021dd09f5 Mon Sep 17 00:00:00 2001 From: liuyi Date: Fri, 20 Jun 2025 18:59:42 +0800 Subject: [PATCH] add db migration --- .../commands/migration.create.handler.ts | 2 +- .../commands/migration.generate.command.ts | 58 +++++++ .../commands/migration.generate.handler.ts | 52 ++++++ .../commands/migration.run.command.ts | 53 +++++++ .../commands/migration.run.handler.ts | 75 +++++++++ .../commands/typeorm.migration.generate.ts | 148 +++++++++++++++++- .../commands/typeorm.migration.run.ts | 28 ++++ src/modules/database/commands/types.ts | 25 ++- src/modules/database/types.ts | 2 +- 9 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 src/modules/database/commands/migration.generate.command.ts create mode 100644 src/modules/database/commands/migration.generate.handler.ts create mode 100644 src/modules/database/commands/migration.run.command.ts create mode 100644 src/modules/database/commands/migration.run.handler.ts create mode 100644 src/modules/database/commands/typeorm.migration.run.ts diff --git a/src/modules/database/commands/migration.create.handler.ts b/src/modules/database/commands/migration.create.handler.ts index be68e58..13cf718 100644 --- a/src/modules/database/commands/migration.create.handler.ts +++ b/src/modules/database/commands/migration.create.handler.ts @@ -30,7 +30,7 @@ export async function MigrationCreateHandler( } const runner = new TypeormMigrationCreate(); console.log(); - await runner.handler({ name: cname, dir: dbConfig.path.migration }); + await runner.handler({ name: cname, dir: dbConfig.paths.migration }); spinner.start(chalk.greenBright.underline('\n 👍 Finished create migration')); } catch (e) { await panic({ spinner, message: 'Create migration failed!', error: e }); diff --git a/src/modules/database/commands/migration.generate.command.ts b/src/modules/database/commands/migration.generate.command.ts new file mode 100644 index 0000000..5c4f363 --- /dev/null +++ b/src/modules/database/commands/migration.generate.command.ts @@ -0,0 +1,58 @@ +import { Arguments } from 'yargs'; + +import { CommandItem } from '@/modules/core/types'; +import { MigrationGenerateHandler } from '@/modules/database/commands/migration.generate.handler'; +import { MigrationGenerateArguments } from '@/modules/database/commands/types'; + +export const GenerateMigrationCommand: CommandItem = async ({ + configure, +}) => ({ + instant: true, + command: ['db:migration:generate', 'dbmg'], + describe: 'Auto generates a new migration file with sql needs to be executed to update schema.', + builder: { + connection: { + type: 'string', + alias: 'c', + describe: 'Connection name of typeorm to connect database.', + }, + name: { + type: 'string', + alias: 'n', + describe: 'Name of the migration class.', + }, + run: { + type: 'boolean', + alias: 'r', + describe: 'Run migration after generated.', + default: false, + }, + dir: { + type: 'string', + alias: 'd', + describe: 'Which directory where migration should be generated.', + }, + pretty: { + type: 'boolean', + alias: 'p', + describe: 'Pretty-print generated SQL', + default: false, + }, + dryrun: { + type: 'boolean', + alias: 'dr', + describe: 'Prints out the contents of the migration instead of writing it to a file', + default: false, + }, + check: { + type: 'boolean', + alias: 'ch', + describe: + 'Verifies that the current database is up to date and that no migrations are needed. Otherwise exits with code 1.', + default: false, + }, + } as const, + + handler: async (args: Arguments) => + MigrationGenerateHandler(configure, args), +}); diff --git a/src/modules/database/commands/migration.generate.handler.ts b/src/modules/database/commands/migration.generate.handler.ts new file mode 100644 index 0000000..bfd8d06 --- /dev/null +++ b/src/modules/database/commands/migration.generate.handler.ts @@ -0,0 +1,52 @@ +import chalk from 'chalk'; +import { isNil, pick } from 'lodash'; +import ora from 'ora'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { Arguments } from 'yargs'; + +import { Configure } from '@/modules/config/configure'; +import { getRandomString, panic } from '@/modules/core/helpers'; +import { MigrationRunHandler } from '@/modules/database/commands/migration.run.handler'; +import { TypeormMigrationGenerate } from '@/modules/database/commands/typeorm.migration.generate'; +import { MigrationGenerateArguments } from '@/modules/database/commands/types'; + +import { DBOptions } from '../types'; + +export async function MigrationGenerateHandler( + configure: Configure, + args: Arguments, +) { + await MigrationRunHandler(configure, { connection: args.connection } as any); + console.log(); + const spinner = ora('Start to generate migration'); + const cname = args.connection ?? 'default'; + try { + spinner.start(); + console.log(); + const { connections = [] }: DBOptions = await configure.get('database'); + const dbConfig = connections.find(({ name }) => name === cname); + if (isNil(dbConfig)) { + await panic(`Database connection named ${cname} not exists!`); + } + console.log(); + const runner = new TypeormMigrationGenerate(); + const dataSource = new DataSource({ ...dbConfig } as DataSourceOptions); + console.log(); + await runner.handler({ + name: args.name ?? getRandomString(6), + dir: dbConfig.paths.migration, + dataSource, + ...pick(args, ['pretty', 'outputJs', 'dryrun', 'check']), + }); + if (dataSource.isInitialized) { + await dataSource.destroy(); + } + spinner.succeed(chalk.greenBright.underline('\n 👍 Finished generate migration')); + if (args.run) { + console.log(); + await MigrationRunHandler(configure, { connection: args.connection } as any); + } + } catch (error) { + await panic({ spinner, message: 'Generate migration failed!', error }); + } +} diff --git a/src/modules/database/commands/migration.run.command.ts b/src/modules/database/commands/migration.run.command.ts new file mode 100644 index 0000000..8978807 --- /dev/null +++ b/src/modules/database/commands/migration.run.command.ts @@ -0,0 +1,53 @@ +import { Arguments } from 'yargs'; + +import { CommandItem } from '@/modules/core/types'; +import { MigrationRunHandler } from '@/modules/database/commands/migration.run.handler'; +import { MigrationRunArguments } from '@/modules/database/commands/types'; + +/** + * 运行迁移 + * @param configure + * @constructor + */ +export const RunMigrationCommand: CommandItem = async ({ + configure, +}) => ({ + source: true, + command: ['db:migration:run', 'dbmr'], + describe: 'Runs all pending migrations.', + builder: { + connection: { + type: 'string', + alias: 'c', + describe: 'Connection name of typeorm to connect database.', + }, + transaction: { + type: 'string', + alias: 't', + describe: + 'Indicates if transaction should be used or not for migration run/revert/reflash. Enabled by default.', + default: 'default', + }, + fake: { + type: 'boolean', + alias: 'f', + describe: + 'Fakes running the migrations if table schema has already been changed manually or externally ' + + '(e.g. through another project)', + }, + refresh: { + type: 'boolean', + alias: 'r', + describe: 'drop database schema and run migration', + default: false, + }, + + onlydrop: { + type: 'boolean', + alias: 'o', + describe: 'only drop database schema', + default: false, + }, + } as const, + handler: async (args: Arguments) => MigrationRunHandler(configure, args), +}); diff --git a/src/modules/database/commands/migration.run.handler.ts b/src/modules/database/commands/migration.run.handler.ts new file mode 100644 index 0000000..e4e7ad6 --- /dev/null +++ b/src/modules/database/commands/migration.run.handler.ts @@ -0,0 +1,75 @@ +import { join } from 'path'; + +import chalk from 'chalk'; +import { isNil } from 'lodash'; +import ora from 'ora'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { Arguments } from 'yargs'; + +import { Configure } from '@/modules/config/configure'; +import { panic } from '@/modules/core/helpers'; +import { TypeormMigrationRun } from '@/modules/database/commands/typeorm.migration.run'; +import { MigrationRunArguments } from '@/modules/database/commands/types'; +import { DBOptions } from '@/modules/database/types'; + +/** + * 运行迁移处理器 + * @param configure + * @param args + * @constructor + */ +export async function MigrationRunHandler( + configure: Configure, + args: Arguments, +) { + const spinner = ora('Start to run migration...'); + const cname = args.connection ?? 'default'; + let dataSource: DataSource | undefined; + try { + spinner.start(); + const { connections = [] }: DBOptions = await configure.get('database'); + const dbConfig = connections.find(({ name }) => name === cname); + if (isNil(dbConfig)) { + await panic(`Database connection named ${cname} not exists!`); + } + const dropSchema = args.refresh || args.onlydrop; + console.log(); + const runner = new TypeormMigrationRun(); + dataSource = new DataSource({ ...dbConfig } as DataSourceOptions); + if (dataSource && dataSource.isInitialized) { + await dataSource.destroy(); + } + const options = { + subscribers: [], + synchronize: false, + migrationsRun: false, + dropSchema, + logging: ['error'], + migrations: [ + join(dbConfig.paths.migration, '**/*.ts'), + join(dbConfig.paths.migration, '**/*.js'), + ], + } as any; + if (dropSchema) { + dataSource.setOptions(options); + await dataSource.initialize(); + await dataSource.destroy(); + spinner.succeed(chalk.greenBright.underline('\n 👍 Finished drop database schema')); + if (args.onlydrop) { + process.exit(); + } + } + + dataSource.setOptions({ ...options, dropSchema: false }); + await dataSource.initialize(); + console.log(); + await runner.handler({ dataSource, transaction: args.transaction, fake: args.fake }); + spinner.succeed(chalk.greenBright.underline('\n 👍 Finished run migrations')); + } catch (error) { + await panic({ spinner, message: 'Run migrations failed!', error }); + } finally { + if (dataSource && dataSource.isInitialized) { + await dataSource.destroy(); + } + } +} diff --git a/src/modules/database/commands/typeorm.migration.generate.ts b/src/modules/database/commands/typeorm.migration.generate.ts index 41d1782..84dde55 100644 --- a/src/modules/database/commands/typeorm.migration.generate.ts +++ b/src/modules/database/commands/typeorm.migration.generate.ts @@ -1,8 +1,152 @@ +import { resolve } from 'path'; + +import chalk from 'chalk'; +import { upperFirst } from 'lodash'; +import { format } from 'mysql2'; import { DataSource } from 'typeorm'; +import { CommandUtils } from 'typeorm/commands/CommandUtils'; +import { PlatformTools } from 'typeorm/platform/PlatformTools'; + +import { camelCase } from 'typeorm/util/StringUtils'; + import { MigrationGenerateOptions } from '@/modules/database/commands/types'; -type HandlerOptions = MigrationGenerateOptions & { dataSource: DataSource }; +type HandlerOptions = MigrationGenerateOptions & { dataSource: DataSource } & { dir: string }; + export class TypeormMigrationGenerate { - async handler(args: HandlerOptions) {} + async handler(args: HandlerOptions) { + const timestamp = new Date().getTime(); + const fileExt = '.ts'; + const directory = args.dir.startsWith('/') ? args.dir : resolve(process.cwd(), args.dir); + const filename = `${timestamp}-${args.name}`; + const filePath = `${directory}/${filename}${fileExt}`; + const { dataSource } = args; + + try { + dataSource.setOptions({ + synchronize: false, + migrationsRun: false, + dropSchema: false, + logging: false, + }); + await dataSource.initialize(); + const upSqls: string[] = []; + const downSqls: string[] = []; + + try { + const sqlInMemory = await dataSource.driver.createSchemaBuilder().log(); + if (args.pretty) { + sqlInMemory.upQueries.forEach((upSql) => { + upSql.query = TypeormMigrationGenerate.prettifyQuery(upSql.query); + }); + sqlInMemory.downQueries.forEach((downSql) => { + downSql.query = TypeormMigrationGenerate.prettifyQuery(downSql.query); + }); + } + sqlInMemory.upQueries.forEach((upQuery) => { + upSqls.push( + ` await queryRunner.query(\`${upQuery.query.replace( + /`/g, + '\\`', + )}\`${TypeormMigrationGenerate.queryParams(upQuery.parameters)});`, + ); + }); + sqlInMemory.downQueries.forEach((downQuery) => { + downSqls.push( + ` await queryRunner.query(\`${downQuery.query.replace( + /`/g, + '\\`', + )}\`${TypeormMigrationGenerate.queryParams(downQuery.parameters)});`, + ); + }); + } finally { + await dataSource.destroy(); + } + + if (!upSqls.length) { + console.log(chalk.green(`No changes in database schema were found`)); + process.exit(0); + } + + const fileContent = TypeormMigrationGenerate.getTemplate( + args.name, + timestamp, + upSqls, + downSqls.reverse(), + ); + if (args.check) { + console.log( + chalk.yellow( + `Unexpected changes in database schema were found in check mode:\n\n${chalk.white( + fileContent, + )}`, + ), + ); + process.exit(1); + } + + if (args.dryrun) { + console.log( + chalk.green( + `Migration ${chalk.blue( + filePath, + )} has content:\n\n${chalk.white(fileContent)}`, + ), + ); + } else { + await CommandUtils.createFile(filePath, fileContent); + + console.log( + chalk.green( + `Migration ${chalk.blue(filePath)} has been generated successfully.`, + ), + ); + } + } catch (e) { + PlatformTools.logCmdErr('Error during migration generation:', e); + process.exit(1); + } + } + + protected static queryParams(params: any[] | undefined): string { + if (!params || !params.length) { + return ''; + } + return `,${JSON.stringify(params)}`; + } + + protected static prettifyQuery(query: string) { + const formatQuery = format(query, { indent: ' ' }); + return `\n${formatQuery.replace(/^/gm, ' ')}\n `; + } + + protected static getTemplate( + name: string, + timestamp: number, + upSqls: string[], + downSqls: string[], + ): string { + const migrationName = `${camelCase(upperFirst(name), true)}${timestamp}`; + + return `import typeorm = require('typeorm'); + +class ${migrationName} implements typeorm.MigrationInterface { + name = '${migrationName}' + + public async up(queryRunner: typeorm.QueryRunner): Promise { +${upSqls.join(` +`)} + } + + public async down(queryRunner: typeorm.QueryRunner): Promise { +${downSqls.join(` +`)} + } + +} + +module.exports = ${migrationName} +`; + } } diff --git a/src/modules/database/commands/typeorm.migration.run.ts b/src/modules/database/commands/typeorm.migration.run.ts new file mode 100644 index 0000000..80b1760 --- /dev/null +++ b/src/modules/database/commands/typeorm.migration.run.ts @@ -0,0 +1,28 @@ +import { DataSource } from 'typeorm'; + +import { MigrationRunOptions } from '@/modules/database/commands/types'; + +type HandlerOptions = MigrationRunOptions & { dataSource: DataSource }; + +export class TypeormMigrationRun { + async handler({ transaction, fake, dataSource }: HandlerOptions) { + const options = { + transaction: dataSource.options.migrationsTransactionMode ?? 'all', + fake, + }; + switch (transaction) { + case 'all': + options.transaction = 'all'; + break; + case 'none': + case 'false': + options.transaction = 'none'; + break; + case 'each': + options.transaction = 'each'; + break; + default: + } + await dataSource.runMigrations(options); + } +} diff --git a/src/modules/database/commands/types.ts b/src/modules/database/commands/types.ts index 187ccf5..e2c5b37 100644 --- a/src/modules/database/commands/types.ts +++ b/src/modules/database/commands/types.ts @@ -20,7 +20,7 @@ export interface MigrationCreateOptions { /** * 生成迁移命令参数 */ -export type MigrationGenerateArguments = TypeOrmArguments & MigrationCreateOptions; +export type MigrationGenerateArguments = TypeOrmArguments & MigrationGenerateOptions; /** * 生成迁移处理器选项 @@ -32,3 +32,26 @@ export interface MigrationGenerateOptions { dryrun?: boolean; check?: boolean; } + +/** + * 运行迁移的命令参数 + */ +export type MigrationRunArguments = TypeOrmArguments & MigrationRunOptions; + +/** + * 运行迁移处理器选项 + */ +export interface MigrationRunOptions extends MigrationRevertOptions { + refresh?: boolean; + + onlydrop?: boolean; +} + +/** + * 恢复迁移处理器选项 + */ +export interface MigrationRevertOptions { + transaction?: string; + + fake?: boolean; +} diff --git a/src/modules/database/types.ts b/src/modules/database/types.ts index dabcd95..9a04193 100644 --- a/src/modules/database/types.ts +++ b/src/modules/database/types.ts @@ -84,7 +84,7 @@ export type DBOptions = RecordAny & { }; type DBAdditionalOption = { - path?: { + paths?: { migration?: string; }; };