mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
feat: migration commands (#828)
* feat: migration commands * Update packages/server/src/modules/CLI/commands/TenantsMigrateRollback.command.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/server/src/modules/CLI/commands/TenantsMigrateLatest.command.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/server/src/modules/CLI/commands/TenantsList.command.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/server/src/modules/CLI/commands/SystemMigrateRollback.command.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/server/src/modules/CLI/commands/TenantsMigrateLatest.command.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
35
packages/server/src/modules/CLI/CLI.module.ts
Normal file
35
packages/server/src/modules/CLI/CLI.module.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { config } from '../../common/config';
|
||||
import { CommandRunnerModule } from 'nest-commander';
|
||||
import { SystemMigrateLatestCommand } from './commands/SystemMigrateLatest.command';
|
||||
import { SystemMigrateRollbackCommand } from './commands/SystemMigrateRollback.command';
|
||||
import { SystemMigrateMakeCommand } from './commands/SystemMigrateMake.command';
|
||||
import { TenantsMigrateLatestCommand } from './commands/TenantsMigrateLatest.command';
|
||||
import { TenantsMigrateRollbackCommand } from './commands/TenantsMigrateRollback.command';
|
||||
import { TenantsMigrateMakeCommand } from './commands/TenantsMigrateMake.command';
|
||||
import { TenantsListCommand } from './commands/TenantsList.command';
|
||||
import { SystemSeedLatestCommand } from './commands/SystemSeedLatest.command';
|
||||
import { TenantsSeedLatestCommand } from './commands/TenantsSeedLatest.command';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
load: config,
|
||||
isGlobal: true,
|
||||
}),
|
||||
CommandRunnerModule,
|
||||
],
|
||||
providers: [
|
||||
SystemMigrateLatestCommand,
|
||||
SystemMigrateRollbackCommand,
|
||||
SystemMigrateMakeCommand,
|
||||
TenantsMigrateLatestCommand,
|
||||
TenantsMigrateRollbackCommand,
|
||||
TenantsMigrateMakeCommand,
|
||||
TenantsListCommand,
|
||||
SystemSeedLatestCommand,
|
||||
TenantsSeedLatestCommand,
|
||||
],
|
||||
})
|
||||
export class CLIModule { }
|
||||
83
packages/server/src/modules/CLI/commands/BaseCommand.ts
Normal file
83
packages/server/src/modules/CLI/commands/BaseCommand.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { CommandRunner } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseCommand extends CommandRunner {
|
||||
constructor(protected readonly configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected initSystemKnex(): any {
|
||||
return Knex({
|
||||
client: this.configService.get('systemDatabase.client'),
|
||||
connection: {
|
||||
host: this.configService.get('systemDatabase.host'),
|
||||
user: this.configService.get('systemDatabase.user'),
|
||||
password: this.configService.get('systemDatabase.password'),
|
||||
database: this.configService.get('systemDatabase.databaseName'),
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: this.configService.get('systemDatabase.migrationDir'),
|
||||
},
|
||||
seeds: {
|
||||
directory: this.configService.get('systemDatabase.seedsDir'),
|
||||
},
|
||||
pool: { min: 0, max: 7 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
}
|
||||
|
||||
protected initTenantKnex(organizationId: string = ''): any {
|
||||
return Knex({
|
||||
client: this.configService.get('tenantDatabase.client'),
|
||||
connection: {
|
||||
host: this.configService.get('tenantDatabase.host'),
|
||||
user: this.configService.get('tenantDatabase.user'),
|
||||
password: this.configService.get('tenantDatabase.password'),
|
||||
database: `${this.configService.get('tenantDatabase.dbNamePrefix')}${organizationId}`,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
|
||||
},
|
||||
seeds: {
|
||||
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',
|
||||
},
|
||||
pool: {
|
||||
min: 0,
|
||||
max: 5,
|
||||
},
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
}
|
||||
|
||||
protected getAllSystemTenants(knex: any) {
|
||||
return knex('tenants');
|
||||
}
|
||||
|
||||
protected getAllInitializedTenants(knex: any) {
|
||||
return knex('tenants').whereNotNull('initializedAt');
|
||||
}
|
||||
|
||||
protected exit(text: any): never {
|
||||
if (text instanceof Error) {
|
||||
console.error(`Error: ${text.message}\n${text.stack}`);
|
||||
} else {
|
||||
console.error(`Error: ${text}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
protected success(text: string): never {
|
||||
console.log(text);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
protected log(text: string): void {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'system:migrate:latest',
|
||||
description: 'Migrate latest migration of the system database.',
|
||||
})
|
||||
export class SystemMigrateLatestCommand extends BaseCommand {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const [batchNo, log] = await sysKnex.migrate.latest();
|
||||
|
||||
if (log.length === 0) {
|
||||
this.success('Already up to date');
|
||||
}
|
||||
|
||||
this.success(
|
||||
`Batch ${batchNo} run: ${log.length} migrations`
|
||||
);
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'system:migrate:make',
|
||||
description: 'Create a named migration file to the system database.',
|
||||
arguments: '<name>',
|
||||
})
|
||||
export class SystemMigrateMakeCommand extends BaseCommand {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async run(passedParams: string[]): Promise<void> {
|
||||
const [name] = passedParams;
|
||||
|
||||
if (!name) {
|
||||
this.exit('Migration name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const migrationName = await sysKnex.migrate.make(name);
|
||||
this.success(`Created Migration: ${migrationName}`);
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'system:migrate:rollback',
|
||||
description: 'Rollback the last batch of system migrations.',
|
||||
})
|
||||
export class SystemMigrateRollbackCommand extends BaseCommand {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const [batchNo, _log] = await sysKnex.migrate.rollback();
|
||||
|
||||
if (_log.length === 0) {
|
||||
this.success('Already at the base migration');
|
||||
}
|
||||
|
||||
this.success(
|
||||
`Batch ${batchNo} rolled back: ${_log.length} migrations`
|
||||
);
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
@Command({
|
||||
name: 'system:seed:latest',
|
||||
description: 'Seed system database with the latest data',
|
||||
})
|
||||
export class SystemSeedLatestCommand extends CommandRunner {
|
||||
async run(): Promise<void> {
|
||||
console.log('System seeding with latest data - No operation performed');
|
||||
// TODO: Implement system seeding logic
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
interface TenantsListOptions {
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'tenants:list',
|
||||
description: 'Retrieve a list of all system tenants databases.',
|
||||
})
|
||||
export class TenantsListCommand extends BaseCommand {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-a, --all',
|
||||
description: 'All tenants even if not initialized.',
|
||||
})
|
||||
parseAll(val: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async run(passedParams: string[], options: TenantsListOptions): Promise<void> {
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const tenants = options.all
|
||||
? await this.getAllSystemTenants(sysKnex)
|
||||
: await this.getAllInitializedTenants(sysKnex);
|
||||
|
||||
tenants.forEach((tenant: any) => {
|
||||
const dbName = `${this.configService.get('tenantDatabase.dbNamePrefix')}${tenant.organizationId}`;
|
||||
console.log(
|
||||
`ID: ${tenant.id} | Organization ID: ${tenant.organizationId} | DB Name: ${dbName}`
|
||||
);
|
||||
});
|
||||
|
||||
this.success('---');
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
interface TenantsMigrateLatestOptions {
|
||||
tenant_id?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'tenants:migrate:latest',
|
||||
description: 'Migrate all tenants or the given tenant id.',
|
||||
})
|
||||
export class TenantsMigrateLatestCommand extends BaseCommand {
|
||||
private readonly MIGRATION_CONCURRENCY = 10;
|
||||
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-t, --tenant_id [tenant_id]',
|
||||
description: 'Which organization id do you migrate.',
|
||||
})
|
||||
parseTenantId(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
async run(passedParams: string[], options: TenantsMigrateLatestOptions): Promise<void> {
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const tenants = await this.getAllInitializedTenants(sysKnex);
|
||||
const tenantsOrgsIds = tenants.map((tenant: any) => tenant.organizationId);
|
||||
|
||||
if (options.tenant_id && tenantsOrgsIds.indexOf(options.tenant_id) === -1) {
|
||||
this.exit(`The given tenant id ${options.tenant_id} does not exist.`);
|
||||
}
|
||||
|
||||
const migrateTenant = async (organizationId: string) => {
|
||||
try {
|
||||
const tenantKnex = this.initTenantKnex(organizationId);
|
||||
const [batchNo, _log] = await tenantKnex.migrate.latest();
|
||||
const tenantDb = `${this.configService.get('tenantDatabase.dbNamePrefix')}${organizationId}`;
|
||||
|
||||
if (_log.length === 0) {
|
||||
this.log('Already up to date');
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Tenant ${tenantDb} > Batch ${batchNo} run: ${_log.length} migrations`
|
||||
);
|
||||
this.log('-------------------');
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!options.tenant_id) {
|
||||
await PromisePool.withConcurrency(this.MIGRATION_CONCURRENCY)
|
||||
.for(tenants)
|
||||
.process((tenant: any) => {
|
||||
return migrateTenant(tenant.organizationId);
|
||||
});
|
||||
this.success('All tenants are migrated.');
|
||||
} else {
|
||||
await migrateTenant(options.tenant_id);
|
||||
}
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'tenants:migrate:make',
|
||||
description: 'Create a named migration file to the tenants database.',
|
||||
arguments: '<name>',
|
||||
})
|
||||
export class TenantsMigrateMakeCommand extends BaseCommand {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async run(passedParams: string[]): Promise<void> {
|
||||
const [name] = passedParams;
|
||||
|
||||
if (!name) {
|
||||
this.exit('Migration name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tenantKnex = this.initTenantKnex();
|
||||
const migrationName = await tenantKnex.migrate.make(name);
|
||||
this.success(`Created Migration: ${migrationName}`);
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Command, Option } from 'nest-commander';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PromisePool } from '@supercharge/promise-pool';
|
||||
import { BaseCommand } from './BaseCommand';
|
||||
|
||||
interface TenantsMigrateRollbackOptions {
|
||||
tenant_id?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'tenants:migrate:rollback',
|
||||
description: 'Rollback the last batch of tenants migrations.',
|
||||
})
|
||||
export class TenantsMigrateRollbackCommand extends BaseCommand {
|
||||
private readonly MIGRATION_CONCURRENCY = 10;
|
||||
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-t, --tenant_id [tenant_id]',
|
||||
description: 'Which organization id do you migrate.',
|
||||
})
|
||||
parseTenantId(val: string): string {
|
||||
return val;
|
||||
}
|
||||
|
||||
async run(passedParams: string[], options: TenantsMigrateRollbackOptions): Promise<void> {
|
||||
try {
|
||||
const sysKnex = this.initSystemKnex();
|
||||
const tenants = await this.getAllInitializedTenants(sysKnex);
|
||||
const tenantsOrgsIds = tenants.map((tenant: any) => tenant.organizationId);
|
||||
|
||||
if (options.tenant_id && tenantsOrgsIds.indexOf(options.tenant_id) === -1) {
|
||||
this.exit(`The given tenant id ${options.tenant_id} does not exist.`);
|
||||
}
|
||||
|
||||
const migrateTenant = async (organizationId: string) => {
|
||||
try {
|
||||
const tenantKnex = this.initTenantKnex(organizationId);
|
||||
const [batchNo, _log] = await tenantKnex.migrate.rollback();
|
||||
const tenantDb = `${this.configService.get('tenantDatabase.dbNamePrefix')}${organizationId}`;
|
||||
|
||||
if (_log.length === 0) {
|
||||
this.log('Already at the base migration');
|
||||
}
|
||||
|
||||
this.log(
|
||||
`Tenant: ${tenantDb} > Batch ${batchNo} rolled back: ${_log.length} migrations`
|
||||
);
|
||||
this.log('---------------');
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!options.tenant_id) {
|
||||
await PromisePool.withConcurrency(this.MIGRATION_CONCURRENCY)
|
||||
.for(tenants)
|
||||
.process((tenant: any) => {
|
||||
return migrateTenant(tenant.organizationId);
|
||||
});
|
||||
this.success('All tenants are rollbacked.');
|
||||
} else {
|
||||
await migrateTenant(options.tenant_id);
|
||||
}
|
||||
} catch (error) {
|
||||
this.exit(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
@Command({
|
||||
name: 'tenants:seed:latest',
|
||||
description: 'Seed all tenant databases with the latest data',
|
||||
})
|
||||
export class TenantsSeedLatestCommand extends CommandRunner {
|
||||
async run(): Promise<void> {
|
||||
console.log('Tenants seeding with latest data - No operation performed');
|
||||
// TODO: Implement tenants seeding logic
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user