diff --git a/package.json b/package.json index 7596fc1f8..a4512cc60 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,15 @@ "test:watch": "lerna run test:watch", "test:e2e": "lerna run test:e2e", "start:debug": "lerna run start:debug", - "prepare": "husky install" + "prepare": "husky install", + "system:migrate:make": "lerna run cli:system:migrate:make --scope \"@bigcapital/server\"", + "tenants:migrate:make": "lerna run cli:tenants:migrate:make --scope \"@bigcapital/server\"", + "system:migrate:rollback": "lerna run cli:system:migrate:rollback --scope \"@bigcapital/server\"", + "tenants:migrate:rollback": "lerna run cli:tenants:migrate:rollback --scope \"@bigcapital/server\"", + "system:migrate:latest": "lerna run cli:system:migrate:latest --scope \"@bigcapital/server\"", + "tenants:migrate:latest": "lerna run cli:tenants:migrate:latest --scope \"@bigcapital/server\"", + "system:seed:latest": "lerna run cli:system:seed:latest --scope \"@bigcapital/server\"", + "tenants:seed:latest": "lerna run cli:tenants:seed:latest --scope \"@bigcapital/server\"" }, "devDependencies": { "@commitlint/cli": "^17.4.2", diff --git a/packages/server/package.json b/packages/server/package.json index 79a1fc39d..feae4f981 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -18,7 +18,17 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json --watchAll" + "test:e2e": "jest --config ./test/jest-e2e.json --watchAll", + "cli": "ts-node -r tsconfig-paths/register src/cli.ts", + "cli:system:migrate:latest": "ts-node -r tsconfig-paths/register src/cli.ts system:migrate:latest", + "cli:system:migrate:rollback": "ts-node -r tsconfig-paths/register src/cli.ts system:migrate:rollback", + "cli:system:migrate:make": "ts-node -r tsconfig-paths/register src/cli.ts system:migrate:make", + "cli:tenants:migrate:latest": "ts-node -r tsconfig-paths/register src/cli.ts tenants:migrate:latest", + "cli:tenants:migrate:rollback": "ts-node -r tsconfig-paths/register src/cli.ts tenants:migrate:rollback", + "cli:tenants:migrate:make": "ts-node -r tsconfig-paths/register src/cli.ts tenants:migrate:make", + "cli:tenants:list": "ts-node -r tsconfig-paths/register src/cli.ts tenants:list", + "cli:system:seed:latest": "ts-node -r tsconfig-paths/register src/cli.ts system:seed:latest", + "cli:tenants:seed:latest": "ts-node -r tsconfig-paths/register src/cli.ts tenants:seed:latest" }, "dependencies": { "@aws-sdk/client-s3": "^3.576.0", @@ -84,6 +94,7 @@ "multer-s3": "^3.0.1", "mysql": "^2.18.1", "mysql2": "^3.11.3", + "nest-commander": "^3.20.1", "nestjs-cls": "^5.2.0", "nestjs-i18n": "^10.4.9", "nestjs-redis": "^1.3.3", diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts new file mode 100644 index 000000000..7d96b896b --- /dev/null +++ b/packages/server/src/cli.ts @@ -0,0 +1,8 @@ +import { CommandFactory } from 'nest-commander'; +import { CLIModule } from './modules/CLI/CLI.module'; + +async function bootstrap() { + await CommandFactory.run(CLIModule); +} + +bootstrap(); diff --git a/packages/server/src/common/config/signup-confirmation.ts b/packages/server/src/common/config/signup-confirmation.ts index 3549f60dd..8253d5735 100644 --- a/packages/server/src/common/config/signup-confirmation.ts +++ b/packages/server/src/common/config/signup-confirmation.ts @@ -1,5 +1,5 @@ -import { parseBoolean } from '@/utils/parse-boolean'; import { registerAs } from '@nestjs/config'; +import { parseBoolean } from '@/utils/parse-boolean'; export default registerAs('signupConfirmation', () => ({ enabled: parseBoolean(process.env.SIGNUP_EMAIL_CONFIRMATION, false), diff --git a/packages/server/src/common/config/system-database.ts b/packages/server/src/common/config/system-database.ts index 0ada1b4b3..99c8a8c11 100644 --- a/packages/server/src/common/config/system-database.ts +++ b/packages/server/src/common/config/system-database.ts @@ -7,4 +7,6 @@ export default registerAs('systemDatabase', () => ({ user: process.env.SYSTEM_DB_USER || process.env.DB_USER, password: process.env.SYSTEM_DB_PASSWORD || process.env.DB_PASSWORD, databaseName: process.env.SYSTEM_DB_NAME || process.env.DB_NAME, + migrationDir: process.env.SYSTEM_DB_MIGRATION_DIR || './src/database/migrations', + seedsDir: process.env.SYSTEM_DB_SEEDS_DIR || './src/database/seeds', })); diff --git a/packages/server/src/modules/CLI/CLI.module.ts b/packages/server/src/modules/CLI/CLI.module.ts new file mode 100644 index 000000000..d62b9f13a --- /dev/null +++ b/packages/server/src/modules/CLI/CLI.module.ts @@ -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 { } diff --git a/packages/server/src/modules/CLI/commands/BaseCommand.ts b/packages/server/src/modules/CLI/commands/BaseCommand.ts new file mode 100644 index 000000000..b69f37c68 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/BaseCommand.ts @@ -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); + } +} diff --git a/packages/server/src/modules/CLI/commands/SystemMigrateLatest.command.ts b/packages/server/src/modules/CLI/commands/SystemMigrateLatest.command.ts new file mode 100644 index 000000000..8732d3205 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/SystemMigrateLatest.command.ts @@ -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 { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/SystemMigrateMake.command.ts b/packages/server/src/modules/CLI/commands/SystemMigrateMake.command.ts new file mode 100644 index 000000000..26aef53ec --- /dev/null +++ b/packages/server/src/modules/CLI/commands/SystemMigrateMake.command.ts @@ -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: '', +}) +export class SystemMigrateMakeCommand extends BaseCommand { + constructor(configService: ConfigService) { + super(configService); + } + + async run(passedParams: string[]): Promise { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/SystemMigrateRollback.command.ts b/packages/server/src/modules/CLI/commands/SystemMigrateRollback.command.ts new file mode 100644 index 000000000..d82620595 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/SystemMigrateRollback.command.ts @@ -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 { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/SystemSeedLatest.command.ts b/packages/server/src/modules/CLI/commands/SystemSeedLatest.command.ts new file mode 100644 index 000000000..66e7c3cd7 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/SystemSeedLatest.command.ts @@ -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 { + console.log('System seeding with latest data - No operation performed'); + // TODO: Implement system seeding logic + } +} diff --git a/packages/server/src/modules/CLI/commands/TenantsList.command.ts b/packages/server/src/modules/CLI/commands/TenantsList.command.ts new file mode 100644 index 000000000..a268ffc73 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/TenantsList.command.ts @@ -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 { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/TenantsMigrateLatest.command.ts b/packages/server/src/modules/CLI/commands/TenantsMigrateLatest.command.ts new file mode 100644 index 000000000..ab0b8cc4f --- /dev/null +++ b/packages/server/src/modules/CLI/commands/TenantsMigrateLatest.command.ts @@ -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 { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/TenantsMigrateMake.command.ts b/packages/server/src/modules/CLI/commands/TenantsMigrateMake.command.ts new file mode 100644 index 000000000..136e5dd65 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/TenantsMigrateMake.command.ts @@ -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: '', +}) +export class TenantsMigrateMakeCommand extends BaseCommand { + constructor(configService: ConfigService) { + super(configService); + } + + async run(passedParams: string[]): Promise { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/TenantsMigrateRollback.command.ts b/packages/server/src/modules/CLI/commands/TenantsMigrateRollback.command.ts new file mode 100644 index 000000000..adb068a22 --- /dev/null +++ b/packages/server/src/modules/CLI/commands/TenantsMigrateRollback.command.ts @@ -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 { + 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); + } + } +} diff --git a/packages/server/src/modules/CLI/commands/TenantsSeedLatest.command.ts b/packages/server/src/modules/CLI/commands/TenantsSeedLatest.command.ts new file mode 100644 index 000000000..e506f9a9f --- /dev/null +++ b/packages/server/src/modules/CLI/commands/TenantsSeedLatest.command.ts @@ -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 { + console.log('Tenants seeding with latest data - No operation performed'); + // TODO: Implement tenants seeding logic + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20b03e37e..8acac1b7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -228,6 +228,9 @@ importers: mysql2: specifier: ^3.11.3 version: 3.11.4 + nest-commander: + specifier: ^3.20.1 + version: 3.20.1(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(@types/inquirer@8.2.12)(@types/node@20.5.1)(typescript@5.6.3) nestjs-cls: specifier: ^5.2.0 version: 5.2.0(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5867,6 +5870,15 @@ packages: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} dev: true + /@fig/complete-commander@3.2.0(commander@11.1.0): + resolution: {integrity: sha512-1Holl3XtRiANVKURZwgpjCnPuV4RsHp+XC0MhgvyAX/avQwj7F2HUItYOvGi/bXjJCkEzgBZmVfCr0HBA+q+Bw==} + peerDependencies: + commander: ^11.1.0 + dependencies: + commander: 11.1.0 + prettier: 3.3.3 + dev: false + /@floating-ui/core@1.6.8: resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} dependencies: @@ -5907,6 +5919,17 @@ packages: deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package dev: false + /@golevelup/nestjs-discovery@5.0.0(@nestjs/common@10.4.7)(@nestjs/core@10.4.7): + resolution: {integrity: sha512-NaIWLCLI+XvneUK05LH2idHLmLNITYT88YnpOuUQmllKtiJNIS3woSt7QXrMZ5k3qUWuZpehEVz1JtlX4I1KyA==} + peerDependencies: + '@nestjs/common': ^11.0.20 + '@nestjs/core': ^11.0.20 + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7)(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.1) + lodash: 4.17.21 + dev: false + /@humanfs/core@0.19.0: resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} engines: {node: '>=18.18.0'} @@ -5961,6 +5984,20 @@ packages: warning: 4.0.3 dev: false + /@inquirer/external-editor@1.0.2(@types/node@20.5.1): + resolution: {integrity: sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@types/node': 20.5.1 + chardet: 2.1.0 + iconv-lite: 0.7.0 + dev: false + /@ioredis/commands@1.2.0: resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} dev: false @@ -11681,6 +11718,13 @@ packages: '@types/node': 20.5.1 dev: false + /@types/inquirer@8.2.12: + resolution: {integrity: sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ==} + dependencies: + '@types/through': 0.0.33 + rxjs: 7.8.1 + dev: false + /@types/ioredis@5.0.0: resolution: {integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==} deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. @@ -12073,6 +12117,12 @@ packages: pretty-format: 25.5.0 dev: false + /@types/through@0.0.33: + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + dependencies: + '@types/node': 20.5.1 + dev: false + /@types/trusted-types@2.0.7: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false @@ -14135,7 +14185,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true /bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -14338,7 +14387,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -14679,6 +14727,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + dev: false + /check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -14768,7 +14820,6 @@ packages: engines: {node: '>=8'} dependencies: restore-cursor: 3.1.0 - dev: true /cli-spinners@2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} @@ -14778,7 +14829,6 @@ packages: /cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} - dev: true /cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} @@ -14792,7 +14842,6 @@ packages: /cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} - dev: true /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} @@ -14826,7 +14875,6 @@ packages: /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - dev: true /clsx@1.2.1: resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} @@ -14928,6 +14976,11 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -15909,7 +15962,6 @@ packages: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} dependencies: clone: 1.0.4 - dev: true /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} @@ -17735,7 +17787,6 @@ packages: engines: {node: '>=8'} dependencies: escape-string-regexp: 1.0.5 - dev: true /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} @@ -19136,6 +19187,13 @@ packages: dependencies: safer-buffer: 2.1.2 + /iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /icss-utils@5.1.0(postcss@8.4.47): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -19287,6 +19345,29 @@ packages: wrap-ansi: 6.2.0 dev: true + /inquirer@8.2.7(@types/node@20.5.1): + resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} + engines: {node: '>=12.0.0'} + dependencies: + '@inquirer/external-editor': 1.0.2(@types/node@20.5.1) + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' + dev: false + /inquirer@9.2.15: resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} engines: {node: '>=18'} @@ -19581,7 +19662,6 @@ packages: /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - dev: true /is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} @@ -19766,7 +19846,6 @@ packages: /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - dev: true /is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} @@ -21695,7 +21774,6 @@ packages: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} @@ -22408,7 +22486,6 @@ packages: /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} @@ -22527,6 +22604,26 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + /nest-commander@3.20.1(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(@types/inquirer@8.2.12)(@types/node@20.5.1)(typescript@5.6.3): + resolution: {integrity: sha512-LRI7z6UlWy2vWyQR0PYnAXsaRyJvpfiuvOCmx2jk2kLXJH9+y/omPDl9NE3fq4WMaE0/AhviuUjA12eC/zDqXw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@types/inquirer': ^8.1.3 + dependencies: + '@fig/complete-commander': 3.2.0(commander@11.1.0) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@10.4.7)(@nestjs/core@10.4.7) + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7)(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@types/inquirer': 8.2.12 + commander: 11.1.0 + cosmiconfig: 8.3.6(typescript@5.6.3) + inquirer: 8.2.7(@types/node@20.5.1) + transitivePeerDependencies: + - '@types/node' + - typescript + dev: false + /nestjs-cls@5.2.0(@nestjs/common@10.4.7)(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1): resolution: {integrity: sha512-xabZQ7aPHttZ5TwC4rEyYgsxm3/ArM+Dz4oJPWc5Q1p+jOp+UaDe37fKna6sIMeUmYpvZxMVtUKIhv7CfLxbOw==} engines: {node: '>=18'} @@ -23254,7 +23351,6 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true /orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} @@ -24697,7 +24793,6 @@ packages: resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} hasBin: true - dev: true /pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} @@ -26698,7 +26793,6 @@ packages: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true /ret@0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} @@ -26821,7 +26915,6 @@ packages: /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} - dev: true /run-async@3.0.0: resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} @@ -29754,7 +29847,6 @@ packages: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} dependencies: defaults: 1.0.4 - dev: true /web-vitals@2.1.4: resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} @@ -30425,7 +30517,6 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}