add db migration
This commit is contained in:
parent
64fcacdb3d
commit
77e27a4b93
@ -30,7 +30,7 @@ export async function MigrationCreateHandler(
|
|||||||
}
|
}
|
||||||
const runner = new TypeormMigrationCreate();
|
const runner = new TypeormMigrationCreate();
|
||||||
console.log();
|
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'));
|
spinner.start(chalk.greenBright.underline('\n 👍 Finished create migration'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await panic({ spinner, message: 'Create migration failed!', error: e });
|
await panic({ spinner, message: 'Create migration failed!', error: e });
|
||||||
|
58
src/modules/database/commands/migration.generate.command.ts
Normal file
58
src/modules/database/commands/migration.generate.command.ts
Normal 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),
|
||||||
|
});
|
52
src/modules/database/commands/migration.generate.handler.ts
Normal file
52
src/modules/database/commands/migration.generate.handler.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
53
src/modules/database/commands/migration.run.command.ts
Normal file
53
src/modules/database/commands/migration.run.command.ts
Normal 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),
|
||||||
|
});
|
75
src/modules/database/commands/migration.run.handler.ts
Normal file
75
src/modules/database/commands/migration.run.handler.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 { 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';
|
import { MigrationGenerateOptions } from '@/modules/database/commands/types';
|
||||||
|
|
||||||
type HandlerOptions = MigrationGenerateOptions & { dataSource: DataSource };
|
type HandlerOptions = MigrationGenerateOptions & { dataSource: DataSource } & { dir: string };
|
||||||
|
|
||||||
export class TypeormMigrationGenerate {
|
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}
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
src/modules/database/commands/typeorm.migration.run.ts
Normal file
28
src/modules/database/commands/typeorm.migration.run.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
dryrun?: boolean;
|
||||||
check?: boolean;
|
check?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行迁移的命令参数
|
||||||
|
*/
|
||||||
|
export type MigrationRunArguments = TypeOrmArguments & MigrationRunOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行迁移处理器选项
|
||||||
|
*/
|
||||||
|
export interface MigrationRunOptions extends MigrationRevertOptions {
|
||||||
|
refresh?: boolean;
|
||||||
|
|
||||||
|
onlydrop?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复迁移处理器选项
|
||||||
|
*/
|
||||||
|
export interface MigrationRevertOptions {
|
||||||
|
transaction?: string;
|
||||||
|
|
||||||
|
fake?: boolean;
|
||||||
|
}
|
||||||
|
@ -84,7 +84,7 @@ export type DBOptions = RecordAny & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DBAdditionalOption = {
|
type DBAdditionalOption = {
|
||||||
path?: {
|
paths?: {
|
||||||
migration?: string;
|
migration?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user