diff --git a/apps/server/src/modules/management/console/database-management.console.spec.ts b/apps/server/src/modules/management/console/database-management.console.spec.ts index 5e08af4edd5..51bb344ec58 100644 --- a/apps/server/src/modules/management/console/database-management.console.spec.ts +++ b/apps/server/src/modules/management/console/database-management.console.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; import { ConsoleWriterService } from '@infra/console'; +import { Test, TestingModule } from '@nestjs/testing'; import { DatabaseManagementUc } from '../uc/database-management.uc'; import { DatabaseManagementConsole } from './database-management.console'; @@ -100,16 +100,25 @@ describe('DatabaseManagementConsole', () => { expect(consoleInfoSpy).toHaveBeenCalledWith('migration up is completed'); expect(databaseManagementUc.migrationUp).toHaveBeenCalled(); }); + it('should migrate down', async () => { await service.migration({ down: true }); expect(consoleInfoSpy).toHaveBeenCalledWith('migration down is completed'); expect(databaseManagementUc.migrationDown).toHaveBeenCalled(); }); + it('should check pending migrations', async () => { await service.migration({ pending: true }); expect(consoleInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Pending:')); expect(databaseManagementUc.migrationPending).toHaveBeenCalled(); }); + + it('should check create migrations', async () => { + await service.migration({ create: true }); + expect(consoleInfoSpy).toHaveBeenCalledWith('migration created'); + expect(databaseManagementUc.migrationCreate).toHaveBeenCalled(); + }); + it('should no migrate if no param specified', async () => { await service.migration({}); expect(consoleErrorSpy).toHaveBeenCalledWith('no migration option was given'); diff --git a/apps/server/src/modules/management/console/database-management.console.ts b/apps/server/src/modules/management/console/database-management.console.ts index 0947c18af83..2f2e12d3d62 100644 --- a/apps/server/src/modules/management/console/database-management.console.ts +++ b/apps/server/src/modules/management/console/database-management.console.ts @@ -15,6 +15,7 @@ interface MigrationOptions { to?: string; only?: string; pending?: boolean; + create?: boolean; } @Console({ command: 'database', description: 'database setup console' }) @@ -118,12 +119,17 @@ export class DatabaseManagementConsole { required: false, description: 'list pending migrations', }, + { + flags: '--create', + required: false, + description: 'create a new migration', + }, ], description: 'Execute MikroOrm migration up/down', }) public async migration(migrationOptions: MigrationOptions): Promise { let report = 'no migration option was given'; - if (!migrationOptions.up && !migrationOptions.down && !migrationOptions.pending) { + if (!migrationOptions.up && !migrationOptions.down && !migrationOptions.pending && !migrationOptions.create) { this.consoleWriter.error(report); return report; } @@ -139,6 +145,10 @@ export class DatabaseManagementConsole { const pendingMigrations = await this.databaseManagementUc.migrationPending(); report = `Pending: ${JSON.stringify(pendingMigrations.map((migration) => migration.name))}`; } + if (migrationOptions.create) { + await this.databaseManagementUc.migrationCreate(); + report = 'migration created'; + } this.consoleWriter.info(report); return report; diff --git a/apps/server/src/modules/management/mikro-orm-cli.config.ts b/apps/server/src/modules/management/mikro-orm-cli.config.ts index 8559b73c498..8637810456d 100644 --- a/apps/server/src/modules/management/mikro-orm-cli.config.ts +++ b/apps/server/src/modules/management/mikro-orm-cli.config.ts @@ -3,10 +3,22 @@ import { MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import path from 'path'; import { ENTITIES } from './management.entity.imports'; -export const migrationsPath = path.resolve(__dirname, '..', '..', 'migrations', 'mikro-orm'); +const migrationsDistPath = path.resolve(__dirname, '..', '..', 'migrations', 'mikro-orm'); +const migrationsSourcePath = path.resolve( + __dirname, + '..', + '..', + '..', + '..', + '..', + 'apps', + 'server', + 'src', + 'migrations', + 'mikro-orm' +); const mikroOrmCliConfig: MikroOrmModuleSyncOptions = { - // TODO repeats server module definitions type: 'mongo', clientUrl: DB_URL, password: DB_PASSWORD, @@ -15,8 +27,8 @@ const mikroOrmCliConfig: MikroOrmModuleSyncOptions = { allowGlobalContext: true, migrations: { tableName: 'migrations', // name of database table with log of executed transactions - path: migrationsPath, // path to the folder with migrations - pathTs: migrationsPath, // path to the folder with TS migrations (if used, we should put path to compiled files in `path`) + path: migrationsDistPath, // path to the folder with migrations + pathTs: migrationsSourcePath, // path to the folder with TS migrations glob: '!(*.d).{js,ts}', // how to match migration files (all .js and .ts files, but not .d.ts) transactional: false, // wrap each migration in a transaction disableForeignKeys: true, // wrap statements with `set foreign_key_checks = 0` or equivalent diff --git a/apps/server/src/modules/management/service/database-management.service.spec.ts b/apps/server/src/modules/management/service/database-management.service.spec.ts index 03c8bfb6674..39143587f8f 100644 --- a/apps/server/src/modules/management/service/database-management.service.spec.ts +++ b/apps/server/src/modules/management/service/database-management.service.spec.ts @@ -1,4 +1,5 @@ -import { MikroORM } from '@mikro-orm/core'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { IMigrator, MikroORM } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { createCollections } from '@testing/create-collections'; @@ -169,4 +170,15 @@ describe('DatabaseManagementService', () => { expect(spy).toHaveBeenCalled(); }); }); + + describe('When call migrationCreate()', () => { + it('should call migrator.createInitialMigration()', async () => { + const spyGetMigrator = jest.spyOn(orm, 'getMigrator'); + const spyMigrator: DeepMocked = createMock(); + spyGetMigrator.mockReturnValue(spyMigrator); + + await service.migrationCreate(); + expect(spyMigrator.createInitialMigration).toHaveBeenCalled(); + }); + }); }); diff --git a/apps/server/src/modules/management/service/database-management.service.ts b/apps/server/src/modules/management/service/database-management.service.ts index 4167d85ca16..e40182da19b 100644 --- a/apps/server/src/modules/management/service/database-management.service.ts +++ b/apps/server/src/modules/management/service/database-management.service.ts @@ -66,7 +66,7 @@ export class DatabaseManagementService { } public async syncIndexes(): Promise { - return this.orm.getSchemaGenerator().ensureIndexes(); + await this.orm.getSchemaGenerator().ensureIndexes(); } public async migrationUp(from?: string, to?: string, only?: string): Promise { @@ -88,7 +88,13 @@ export class DatabaseManagementService { return pendingMigrations; } - private migrationParams(only?: string, from?: string, to?: string) { + public async migrationCreate(): Promise { + const migrator = this.orm.getMigrator(); + + await migrator.createInitialMigration(); + } + + private migrationParams(only?: string, from?: string, to?: string): MigrateOptions { const params: MigrateOptions = {}; if (only) { params.migrations = [only]; diff --git a/apps/server/src/modules/management/uc/database-management.uc.spec.ts b/apps/server/src/modules/management/uc/database-management.uc.spec.ts index 592c4d0a9a4..c68bf000bbc 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.spec.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.spec.ts @@ -701,5 +701,10 @@ describe('DatabaseManagementService', () => { await uc.migrationPending(); expect(dbService.migrationPending).toHaveBeenCalled(); }); + it('should call migrationCreate', async () => { + jest.spyOn(dbService, 'migrationDown').mockImplementation(); + await uc.migrationCreate(); + expect(dbService.migrationCreate).toHaveBeenCalled(); + }); }); }); diff --git a/apps/server/src/modules/management/uc/database-management.uc.ts b/apps/server/src/modules/management/uc/database-management.uc.ts index ab91aebddf4..800efb9e26a 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.ts @@ -57,7 +57,7 @@ export class DatabaseManagementUc { /** * setup dir with json files */ - private getSeedFolder() { + private getSeedFolder(): string { return this.fileSystemAdapter.joinPath(this.baseDir, 'setup'); } @@ -65,7 +65,7 @@ export class DatabaseManagementUc { * export folder name based on current date * @returns */ - private getTargetFolder(toSeedFolder?: boolean) { + private getTargetFolder(toSeedFolder?: boolean): string { if (toSeedFolder === true) { const targetFolder = this.getSeedFolder(); return targetFolder; @@ -118,7 +118,7 @@ export class DatabaseManagementUc { source: 'files' | 'database', folder: string, collectionNameFilter?: string[] - ) { + ): Promise { let allCollectionsWithFilePaths: CollectionFilePath[] = []; // load all available collections from source @@ -151,7 +151,7 @@ export class DatabaseManagementUc { return allCollectionsWithFilePaths; } - private async dropCollectionIfExists(collectionName: string) { + private async dropCollectionIfExists(collectionName: string): Promise { const collectionExists = await this.databaseManagementService.collectionExists(collectionName); if (collectionExists) { // clear existing documents, if collection exists @@ -162,7 +162,7 @@ export class DatabaseManagementUc { } } - async seedDatabaseCollectionsFromFactories(collections?: string[]): Promise { + public async seedDatabaseCollectionsFromFactories(collections?: string[]): Promise { const promises = generateSeedData((s: string) => this.injectEnvVars(s)) .filter((data) => { if (collections && collections.length > 0) { @@ -266,7 +266,7 @@ export class DatabaseManagementUc { * @param toSeedFolder optional override existing seed data files * @returns the list of collection names exported */ - async exportCollectionsToFileSystem(collections?: string[], toSeedFolder?: boolean): Promise { + public async exportCollectionsToFileSystem(collections?: string[], toSeedFolder?: boolean): Promise { const targetFolder = this.getTargetFolder(toSeedFolder); await this.fileSystemAdapter.createDir(targetFolder); // detect collections to export based on database collections @@ -301,7 +301,7 @@ export class DatabaseManagementUc { /** * Updates the indexes in the database based on definitions in entities */ - async syncIndexes(): Promise { + public async syncIndexes(): Promise { await this.createUserSearchIndex(); return this.databaseManagementService.syncIndexes(); } @@ -357,7 +357,7 @@ export class DatabaseManagementUc { return json; } - private resolvePlaceholder(placeholder: string) { + private resolvePlaceholder(placeholder: string): string { if (Configuration.has(placeholder)) { return Configuration.get(placeholder) as string; } @@ -369,13 +369,13 @@ export class DatabaseManagementUc { return ''; } - private encryptSecrets(collectionName: string, jsonDocuments: unknown[]) { + private encryptSecrets(collectionName: string, jsonDocuments: unknown[]): void { if (collectionName === systemsCollectionName) { this.encryptSecretsInSystems(jsonDocuments as SystemEntity[]); } } - private encryptSecretsInSystems(systems: SystemEntity[]) { + private encryptSecretsInSystems(systems: SystemEntity[]): SystemEntity[] { systems.forEach((system) => { if (system.oauthConfig) { system.oauthConfig.clientSecret = this.defaultEncryptionService.encrypt(system.oauthConfig.clientSecret); @@ -397,7 +397,7 @@ export class DatabaseManagementUc { * Manual replacement with the intend placeholders or value is mandatory. * Currently, this affects system and storageproviders collections. */ - private removeSecrets(collectionName: string, jsonDocuments: unknown[]) { + private removeSecrets(collectionName: string, jsonDocuments: unknown[]): void { if (collectionName === systemsCollectionName) { this.removeSecretsFromSystems(jsonDocuments as SystemEntity[]); } @@ -406,14 +406,14 @@ export class DatabaseManagementUc { } } - private removeSecretsFromStorageproviders(storageProviders: StorageProviderEntity[]) { + private removeSecretsFromStorageproviders(storageProviders: StorageProviderEntity[]): void { storageProviders.forEach((storageProvider) => { storageProvider.accessKeyId = defaultSecretReplacementHintText; storageProvider.secretAccessKey = defaultSecretReplacementHintText; }); } - private removeSecretsFromSystems(systems: SystemEntity[]) { + private removeSecretsFromSystems(systems: SystemEntity[]): SystemEntity[] { systems.forEach((system) => { if (system.oauthConfig) { system.oauthConfig.clientSecret = defaultSecretReplacementHintText; @@ -428,16 +428,22 @@ export class DatabaseManagementUc { return systems; } + public async migrationCreate(): Promise { + await this.databaseManagementService.migrationCreate(); + } + public async migrationUp(from?: string, to?: string, only?: string): Promise { - return this.databaseManagementService.migrationUp(from, to, only); + await this.databaseManagementService.migrationUp(from, to, only); } public async migrationDown(from?: string, to?: string, only?: string): Promise { - return this.databaseManagementService.migrationDown(from, to, only); + await this.databaseManagementService.migrationDown(from, to, only); } public async migrationPending(): Promise { - return this.databaseManagementService.migrationPending(); + const result = await this.databaseManagementService.migrationPending(); + + return result; } public encryptPlainText(plainText: string): string { diff --git a/package.json b/package.json index 7593d9a3c0e..4c692f1da6c 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,6 @@ "node": "22", "npm": ">=9" }, - "mikro-orm": { - "useTsNode": true, - "configPaths": [ - "./apps/server/src/modules/management/mikro-orm-cli.config.ts", - "./dist/server/modules/management/mikro-orm-cli.config.js" - ] - }, "scripts": { "lint": "eslint . --ignore-path .gitignore", "test": "npm run nest:test && npm run feathers:test", @@ -53,6 +46,7 @@ "migration:down": "npm run nest:start:console -- database migration --down", "migration:pending": "npm run nest:start:console -- database migration --pending", "migration:persisted": "npm run nest:start:console -- database export --collection migrations --override", + "migration:create": "npm run nest:start:console -- database migration --create", "nest:prebuild": "rimraf dist", "nest:build": "nest build", "nest:build:all": "npm run nest:build",