diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts new file mode 100644 index 0000000000..11bccbe348 --- /dev/null +++ b/server/src/interfaces/config.interface.ts @@ -0,0 +1,14 @@ +import { VectorExtension } from 'src/interfaces/database.interface'; + +export const IConfigRepository = 'IConfigRepository'; + +export interface EnvData { + database: { + skipMigrations: boolean; + vectorExtension: VectorExtension; + }; +} + +export interface IConfigRepository { + getEnv(): EnvData; +} diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts new file mode 100644 index 0000000000..f16fa3bbd4 --- /dev/null +++ b/server/src/repositories/config.repository.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { getVectorExtension } from 'src/database.config'; +import { EnvData, IConfigRepository } from 'src/interfaces/config.interface'; + +@Injectable() +export class ConfigRepository implements IConfigRepository { + getEnv(): EnvData { + return { + database: { + skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true', + vectorExtension: getVectorExtension(), + }, + }; + } +} diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7082fc031f..fac250d667 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -5,6 +5,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; @@ -39,6 +40,7 @@ import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { EventRepository } from 'src/repositories/event.repository'; @@ -74,6 +76,7 @@ export const repositories = [ { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, { provide: IAuditRepository, useClass: AuditRepository }, + { provide: IConfigRepository, useClass: ConfigRepository }, { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IDatabaseRepository, useClass: DatabaseRepository }, { provide: IEventRepository, useClass: EventRepository }, diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index c63428560e..fc8130cadc 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,12 +1,20 @@ -import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IConfigRepository } from 'src/interfaces/config.interface'; +import { + DatabaseExtension, + EXTENSION_NAMES, + IDatabaseRepository, + VectorExtension, +} from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DatabaseService } from 'src/services/database.service'; +import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { Mocked } from 'vitest'; describe(DatabaseService.name, () => { let sut: DatabaseService; + let configMock: Mocked; let databaseMock: Mocked; let loggerMock: Mocked; let extensionRange: string; @@ -16,9 +24,11 @@ describe(DatabaseService.name, () => { let versionAboveRange: string; beforeEach(() => { + configMock = newConfigRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); loggerMock = newLoggerRepositoryMock(); - sut = new DatabaseService(databaseMock, loggerMock); + + sut = new DatabaseService(configMock, databaseMock, loggerMock); extensionRange = '0.2.x'; databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); @@ -33,11 +43,6 @@ describe(DatabaseService.name, () => { }); }); - afterEach(() => { - delete process.env.DB_SKIP_MIGRATIONS; - delete process.env.DB_VECTOR_EXTENSION; - }); - it('should work', () => { expect(sut).toBeDefined(); }); @@ -50,12 +55,12 @@ describe(DatabaseService.name, () => { expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); - describe.each([ + describe.each(>[ { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, ])('should work with $extensionName', ({ extension, extensionName }) => { beforeEach(() => { - process.env.DB_VECTOR_EXTENSION = extensionName; + configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } }); }); it(`should start up successfully with ${extension}`, async () => { @@ -236,18 +241,28 @@ describe(DatabaseService.name, () => { expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); expect(loggerMock.fatal).not.toHaveBeenCalled(); }); + }); - it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { - process.env.DB_SKIP_MIGRATIONS = 'true'; - - await expect(sut.onBootstrap()).resolves.toBeUndefined(); - - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { + configMock.getEnv.mockReturnValue({ + database: { + skipMigrations: true, + vectorExtension: DatabaseExtension.VECTORS, + }, }); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); it(`should throw error if pgvector extension could not be created`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; + configMock.getEnv.mockReturnValue({ + database: { + skipMigrations: true, + vectorExtension: DatabaseExtension.VECTOR, + }, + }); databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: minVersionInRange, diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index a5280ff28b..ee6176115b 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Duration } from 'luxon'; import semver from 'semver'; -import { getVectorExtension } from 'src/database.config'; import { OnEmit } from 'src/decorators'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseExtension, DatabaseLock, @@ -67,6 +67,7 @@ export class DatabaseService { private reconnection?: NodeJS.Timeout; constructor( + @Inject(IConfigRepository) private configRepository: IConfigRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { @@ -85,7 +86,8 @@ export class DatabaseService { } await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { - const extension = getVectorExtension(); + const envData = this.configRepository.getEnv(); + const extension = envData.database.vectorExtension; const name = EXTENSION_NAMES[extension]; const extensionRange = this.databaseRepository.getExtensionVersionRange(extension); @@ -116,7 +118,8 @@ export class DatabaseService { await this.checkReindexing(); - if (process.env.DB_SKIP_MIGRATIONS !== 'true') { + const { database } = this.configRepository.getEnv(); + if (!database.skipMigrations) { await this.databaseRepository.runMigrations(); } }); diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts new file mode 100644 index 0000000000..40110186f4 --- /dev/null +++ b/server/test/repositories/config.repository.mock.ts @@ -0,0 +1,14 @@ +import { IConfigRepository } from 'src/interfaces/config.interface'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newConfigRepositoryMock = (): Mocked => { + return { + getEnv: vitest.fn().mockReturnValue({ + database: { + skipMigration: false, + vectorExtension: DatabaseExtension.VECTORS, + }, + }), + }; +};