add db migration

This commit is contained in:
liuyi 2025-06-20 18:59:42 +08:00
parent 64fcacdb3d
commit 77e27a4b93
9 changed files with 438 additions and 5 deletions

View File

@ -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 });

View File

@ -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<any, MigrationGenerateArguments> = 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<MigrationGenerateArguments>) =>
MigrationGenerateHandler(configure, args),
});

View File

@ -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<MigrationGenerateArguments>,
) {
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<DBOptions>('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 });
}
}

View File

@ -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<any, MigrationRunArguments> = 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<MigrationRunArguments>) => MigrationRunHandler(configure, args),
});

View File

@ -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<MigrationRunArguments>,
) {
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<DBOptions>('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();
}
}
}

View File

@ -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<void> {
${upSqls.join(`
`)}
}
public async down(queryRunner: typeorm.QueryRunner): Promise<void> {
${downSqls.join(`
`)}
}
}
module.exports = ${migrationName}
`;
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -84,7 +84,7 @@ export type DBOptions = RecordAny & {
};
type DBAdditionalOption = {
path?: {
paths?: {
migration?: string;
};
};