From 2a9f2b4515d1f055b395f5360e8bec928c2c5e69 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 21 Mar 2024 09:08:29 -0500 Subject: [PATCH] refactor: app modules, main.ts (#8156) --- server/src/{apps => }/app.module.ts | 142 +++++++++++++++--- server/src/apps/api.main.ts | 56 ------- server/src/apps/api.module.ts | 77 ---------- server/src/apps/immich-admin.main.ts | 8 - server/src/apps/immich-admin.module.ts | 20 --- server/src/apps/microservices.main.ts | 20 --- server/src/apps/microservices.module.ts | 15 -- server/src/main.ts | 83 +++++++++- server/src/{apps => services}/api.service.ts | 0 .../microservices.service.ts | 0 server/test/utils.ts | 21 +-- 11 files changed, 205 insertions(+), 237 deletions(-) rename server/src/{apps => }/app.module.ts (66%) delete mode 100644 server/src/apps/api.main.ts delete mode 100644 server/src/apps/api.module.ts delete mode 100755 server/src/apps/immich-admin.main.ts delete mode 100644 server/src/apps/immich-admin.module.ts delete mode 100644 server/src/apps/microservices.main.ts delete mode 100644 server/src/apps/microservices.module.ts rename server/src/{apps => services}/api.service.ts (100%) rename server/src/{apps => services}/microservices.service.ts (100%) diff --git a/server/src/apps/app.module.ts b/server/src/app.module.ts similarity index 66% rename from server/src/apps/app.module.ts rename to server/src/app.module.ts index 0672f62dbb..c44b187272 100644 --- a/server/src/apps/app.module.ts +++ b/server/src/app.module.ts @@ -1,11 +1,38 @@ import { BullModule } from '@nestjs/bullmq'; -import { Global, Module, Provider } from '@nestjs/common'; +import { Module, OnModuleInit, Provider, ValidationPipe } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { OpenTelemetryModule } from 'nestjs-otel'; +import { ListUsersCommand } from 'src/commands/list-users.command'; +import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; +import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; +import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; import { bullConfig, bullQueues, immichAppConfig } from 'src/config'; +import { ActivityController } from 'src/controllers/activity.controller'; +import { AlbumController } from 'src/controllers/album.controller'; +import { APIKeyController } from 'src/controllers/api-key.controller'; +import { AppController } from 'src/controllers/app.controller'; +import { AssetControllerV1 } from 'src/controllers/asset-v1.controller'; +import { AssetController, AssetsController } from 'src/controllers/asset.controller'; +import { AuditController } from 'src/controllers/audit.controller'; +import { AuthController } from 'src/controllers/auth.controller'; +import { DownloadController } from 'src/controllers/download.controller'; +import { FaceController } from 'src/controllers/face.controller'; +import { JobController } from 'src/controllers/job.controller'; +import { LibraryController } from 'src/controllers/library.controller'; +import { OAuthController } from 'src/controllers/oauth.controller'; +import { PartnerController } from 'src/controllers/partner.controller'; +import { PersonController } from 'src/controllers/person.controller'; +import { SearchController } from 'src/controllers/search.controller'; +import { ServerInfoController } from 'src/controllers/server-info.controller'; +import { SharedLinkController } from 'src/controllers/shared-link.controller'; +import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { TagController } from 'src/controllers/tag.controller'; +import { TrashController } from 'src/controllers/trash.controller'; +import { UserController } from 'src/controllers/user.controller'; import { databaseConfig } from 'src/database.config'; import { databaseEntities } from 'src/entities'; import { IAccessRepository } from 'src/interfaces/access.interface'; @@ -36,6 +63,9 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { AuthGuard } from 'src/middleware/auth.guard'; +import { ErrorInterceptor } from 'src/middleware/error.interceptor'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; @@ -67,6 +97,7 @@ import { UserRepository } from 'src/repositories/user.repository'; import { ActivityService } from 'src/services/activity.service'; import { AlbumService } from 'src/services/album.service'; import { APIKeyService } from 'src/services/api-key.service'; +import { ApiService } from 'src/services/api.service'; import { AssetServiceV1 } from 'src/services/asset-v1.service'; import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; @@ -77,6 +108,7 @@ import { JobService } from 'src/services/job.service'; import { LibraryService } from 'src/services/library.service'; import { MediaService } from 'src/services/media.service'; import { MetadataService } from 'src/services/metadata.service'; +import { MicroservicesService } from 'src/services/microservices.service'; import { PartnerService } from 'src/services/partner.service'; import { PersonService } from 'src/services/person.service'; import { SearchService } from 'src/services/search.service'; @@ -92,7 +124,46 @@ import { UserService } from 'src/services/user.service'; import { otelConfig } from 'src/utils/instrumentation'; import { ImmichLogger } from 'src/utils/logger'; +const commands = [ + ResetAdminPasswordCommand, + PromptPasswordQuestions, + EnablePasswordLoginCommand, + DisablePasswordLoginCommand, + EnableOAuthLogin, + DisableOAuthLogin, + ListUsersCommand, +]; + +const controllers = [ + ActivityController, + AssetsController, + AssetControllerV1, + AssetController, + AppController, + AlbumController, + APIKeyController, + AuditController, + AuthController, + DownloadController, + FaceController, + JobController, + LibraryController, + OAuthController, + PartnerController, + SearchController, + ServerInfoController, + SharedLinkController, + SystemConfigController, + TagController, + TrashController, + UserController, + PersonController, +]; + const services: Provider[] = [ + ApiService, + MicroservicesService, + APIKeyService, ActivityService, AlbumService, @@ -152,33 +223,62 @@ const repositories: Provider[] = [ { provide: IUserTokenRepository, useClass: UserTokenRepository }, ]; -@Global() -@Module({ - imports: [ - ConfigModule.forRoot(immichAppConfig), - EventEmitterModule.forRoot(), - TypeOrmModule.forRoot(databaseConfig), - TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, - BullModule.forRoot(bullConfig), - BullModule.registerQueue(...bullQueues), - OpenTelemetryModule.forRoot(otelConfig), - ], - providers: [...services, ...repositories, SchedulerRegistry], - exports: [...services, ...repositories, BullModule, SchedulerRegistry], -}) -export class AppModule {} +const middleware = [ + FileUploadInterceptor, + { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, + { provide: APP_GUARD, useClass: AuthGuard }, +]; + +const imports = [ + BullModule.forRoot(bullConfig), + BullModule.registerQueue(...bullQueues), + ConfigModule.forRoot(immichAppConfig), + EventEmitterModule.forRoot(), + OpenTelemetryModule.forRoot(otelConfig), + TypeOrmModule.forRoot(databaseConfig), + TypeOrmModule.forFeature(databaseEntities), +]; + +@Module({ + imports: [...imports, ScheduleModule.forRoot()], + controllers: [...controllers], + providers: [...services, ...repositories, ...middleware], +}) +export class ApiModule implements OnModuleInit { + constructor(private service: ApiService) {} + + async onModuleInit() { + await this.service.init(); + } +} + +@Module({ + imports: [...imports], + providers: [...services, ...repositories, SchedulerRegistry], +}) +export class MicroservicesModule implements OnModuleInit { + constructor(private service: MicroservicesService) {} + + async onModuleInit() { + await this.service.init(); + } +} + +@Module({ + imports: [...imports], + providers: [...services, ...repositories, ...commands, SchedulerRegistry], +}) +export class ImmichAdminModule {} -@Global() @Module({ imports: [ ConfigModule.forRoot(immichAppConfig), EventEmitterModule.forRoot(), TypeOrmModule.forRoot(databaseConfig), TypeOrmModule.forFeature(databaseEntities), - ScheduleModule, ], - providers: [...services, ...repositories, SchedulerRegistry], - exports: [...services, ...repositories, SchedulerRegistry], + controllers: [...controllers], + providers: [...services, ...repositories, ...middleware, SchedulerRegistry], }) export class AppTestModule {} diff --git a/server/src/apps/api.main.ts b/server/src/apps/api.main.ts deleted file mode 100644 index bd46517236..0000000000 --- a/server/src/apps/api.main.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { NestExpressApplication } from '@nestjs/platform-express'; -import { json } from 'body-parser'; -import cookieParser from 'cookie-parser'; -import { existsSync } from 'node:fs'; -import sirv from 'sirv'; -import { ApiModule } from 'src/apps/api.module'; -import { ApiService } from 'src/apps/api.service'; -import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; -import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; -import { otelSDK } from 'src/utils/instrumentation'; -import { ImmichLogger } from 'src/utils/logger'; -import { useSwagger } from 'src/utils/misc'; - -const logger = new ImmichLogger('ImmichServer'); -const port = Number(process.env.SERVER_PORT) || 3001; - -export async function bootstrapApi() { - otelSDK.start(); - const app = await NestFactory.create(ApiModule, { bufferLogs: true }); - - app.useLogger(app.get(ImmichLogger)); - app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); - app.set('etag', 'strong'); - app.use(cookieParser()); - app.use(json({ limit: '10mb' })); - if (isDev) { - app.enableCors(); - } - app.useWebSocketAdapter(new WebSocketAdapter(app)); - useSwagger(app, isDev); - - app.setGlobalPrefix('api', { exclude: excludePaths }); - if (existsSync(WEB_ROOT)) { - // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 - // provides serving of precompressed assets and caching of immutable assets - app.use( - sirv(WEB_ROOT, { - etag: true, - gzip: true, - brotli: true, - setHeaders: (res, pathname) => { - if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { - res.setHeader('cache-control', 'public,max-age=31536000,immutable'); - } - }, - }), - ); - } - app.use(app.get(ApiService).ssr(excludePaths)); - - const server = await app.listen(port); - server.requestTimeout = 30 * 60 * 1000; - - logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); -} diff --git a/server/src/apps/api.module.ts b/server/src/apps/api.module.ts deleted file mode 100644 index a06eb26347..0000000000 --- a/server/src/apps/api.module.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common'; -import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { ScheduleModule } from '@nestjs/schedule'; -import { ApiService } from 'src/apps/api.service'; -import { AppModule } from 'src/apps/app.module'; -import { ActivityController } from 'src/controllers/activity.controller'; -import { AlbumController } from 'src/controllers/album.controller'; -import { APIKeyController } from 'src/controllers/api-key.controller'; -import { AppController } from 'src/controllers/app.controller'; -import { AssetControllerV1 } from 'src/controllers/asset-v1.controller'; -import { AssetController, AssetsController } from 'src/controllers/asset.controller'; -import { AuditController } from 'src/controllers/audit.controller'; -import { AuthController } from 'src/controllers/auth.controller'; -import { DownloadController } from 'src/controllers/download.controller'; -import { FaceController } from 'src/controllers/face.controller'; -import { JobController } from 'src/controllers/job.controller'; -import { LibraryController } from 'src/controllers/library.controller'; -import { OAuthController } from 'src/controllers/oauth.controller'; -import { PartnerController } from 'src/controllers/partner.controller'; -import { PersonController } from 'src/controllers/person.controller'; -import { SearchController } from 'src/controllers/search.controller'; -import { ServerInfoController } from 'src/controllers/server-info.controller'; -import { SharedLinkController } from 'src/controllers/shared-link.controller'; -import { SystemConfigController } from 'src/controllers/system-config.controller'; -import { TagController } from 'src/controllers/tag.controller'; -import { TrashController } from 'src/controllers/trash.controller'; -import { UserController } from 'src/controllers/user.controller'; -import { AuthGuard } from 'src/middleware/auth.guard'; -import { ErrorInterceptor } from 'src/middleware/error.interceptor'; -import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; - -@Module({ - imports: [ - // - AppModule, - ScheduleModule.forRoot(), - ], - controllers: [ - ActivityController, - AssetsController, - AssetControllerV1, - AssetController, - AppController, - AlbumController, - APIKeyController, - AuditController, - AuthController, - DownloadController, - FaceController, - JobController, - LibraryController, - OAuthController, - PartnerController, - SearchController, - ServerInfoController, - SharedLinkController, - SystemConfigController, - TagController, - TrashController, - UserController, - PersonController, - ], - providers: [ - ApiService, - FileUploadInterceptor, - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, - { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, - { provide: APP_GUARD, useClass: AuthGuard }, - ], -}) -export class ApiModule implements OnModuleInit { - constructor(private apiService: ApiService) {} - - async onModuleInit() { - await this.apiService.init(); - } -} diff --git a/server/src/apps/immich-admin.main.ts b/server/src/apps/immich-admin.main.ts deleted file mode 100755 index 5f528a21b6..0000000000 --- a/server/src/apps/immich-admin.main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CommandFactory } from 'nest-commander'; -import { ImmichAdminModule } from 'src/apps/immich-admin.module'; -import { LogLevel } from 'src/entities/system-config.entity'; - -export async function bootstrapImmichAdmin() { - process.env.LOG_LEVEL = LogLevel.WARN; - await CommandFactory.run(ImmichAdminModule); -} diff --git a/server/src/apps/immich-admin.module.ts b/server/src/apps/immich-admin.module.ts deleted file mode 100644 index eff2e9cc0c..0000000000 --- a/server/src/apps/immich-admin.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppModule } from 'src/apps/app.module'; -import { ListUsersCommand } from 'src/commands/list-users.command'; -import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login'; -import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login'; -import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command'; - -@Module({ - imports: [AppModule], - providers: [ - ResetAdminPasswordCommand, - PromptPasswordQuestions, - EnablePasswordLoginCommand, - DisablePasswordLoginCommand, - EnableOAuthLogin, - DisableOAuthLogin, - ListUsersCommand, - ], -}) -export class ImmichAdminModule {} diff --git a/server/src/apps/microservices.main.ts b/server/src/apps/microservices.main.ts deleted file mode 100644 index d35483d72e..0000000000 --- a/server/src/apps/microservices.main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { MicroservicesModule } from 'src/apps/microservices.module'; -import { envName, serverVersion } from 'src/constants'; -import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; -import { otelSDK } from 'src/utils/instrumentation'; -import { ImmichLogger } from 'src/utils/logger'; - -const logger = new ImmichLogger('ImmichMicroservice'); -const port = Number(process.env.MICROSERVICES_PORT) || 3002; - -export async function bootstrapMicroservices() { - otelSDK.start(); - const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); - app.useLogger(app.get(ImmichLogger)); - app.useWebSocketAdapter(new WebSocketAdapter(app)); - - await app.listen(port); - - logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); -} diff --git a/server/src/apps/microservices.module.ts b/server/src/apps/microservices.module.ts deleted file mode 100644 index d9a28a91c8..0000000000 --- a/server/src/apps/microservices.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module, OnModuleInit } from '@nestjs/common'; -import { AppModule } from 'src/apps/app.module'; -import { MicroservicesService } from 'src/apps/microservices.service'; - -@Module({ - imports: [AppModule], - providers: [MicroservicesService], -}) -export class MicroservicesModule implements OnModuleInit { - constructor(private appService: MicroservicesService) {} - - async onModuleInit() { - await this.appService.init(); - } -} diff --git a/server/src/main.ts b/server/src/main.ts index ee60c793c7..3a93038683 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,6 +1,75 @@ -import { bootstrapApi } from 'src/apps/api.main'; -import { bootstrapImmichAdmin } from 'src/apps/immich-admin.main'; -import { bootstrapMicroservices } from 'src/apps/microservices.main'; +import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; +import { json } from 'body-parser'; +import cookieParser from 'cookie-parser'; +import { CommandFactory } from 'nest-commander'; +import { existsSync } from 'node:fs'; +import sirv from 'sirv'; +import { ApiModule, ImmichAdminModule, MicroservicesModule } from 'src/app.module'; +import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants'; +import { LogLevel } from 'src/entities/system-config.entity'; +import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; +import { ApiService } from 'src/services/api.service'; +import { otelSDK } from 'src/utils/instrumentation'; +import { ImmichLogger } from 'src/utils/logger'; +import { useSwagger } from 'src/utils/misc'; + +async function bootstrapMicroservices() { + const logger = new ImmichLogger('ImmichMicroservice'); + const port = Number(process.env.MICROSERVICES_PORT) || 3002; + + otelSDK.start(); + const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); + app.useLogger(app.get(ImmichLogger)); + app.useWebSocketAdapter(new WebSocketAdapter(app)); + + await app.listen(port); + + logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); +} + +async function bootstrapApi() { + const logger = new ImmichLogger('ImmichServer'); + const port = Number(process.env.SERVER_PORT) || 3001; + + otelSDK.start(); + const app = await NestFactory.create(ApiModule, { bufferLogs: true }); + + app.useLogger(app.get(ImmichLogger)); + app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); + app.set('etag', 'strong'); + app.use(cookieParser()); + app.use(json({ limit: '10mb' })); + if (isDev) { + app.enableCors(); + } + app.useWebSocketAdapter(new WebSocketAdapter(app)); + useSwagger(app, isDev); + + app.setGlobalPrefix('api', { exclude: excludePaths }); + if (existsSync(WEB_ROOT)) { + // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 + // provides serving of precompressed assets and caching of immutable assets + app.use( + sirv(WEB_ROOT, { + etag: true, + gzip: true, + brotli: true, + setHeaders: (res, pathname) => { + if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { + res.setHeader('cache-control', 'public,max-age=31536000,immutable'); + } + }, + }), + ); + } + app.use(app.get(ApiService).ssr(excludePaths)); + + const server = await app.listen(port); + server.requestTimeout = 30 * 60 * 1000; + + logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); +} const immichApp = process.argv[2] || process.env.IMMICH_APP; @@ -8,6 +77,11 @@ if (process.argv[2] === immichApp) { process.argv.splice(2, 1); } +async function bootstrapImmichAdmin() { + process.env.LOG_LEVEL = LogLevel.WARN; + await CommandFactory.run(ImmichAdminModule); +} + function bootstrap() { switch (immichApp) { case 'immich': { @@ -23,8 +97,9 @@ function bootstrap() { return bootstrapImmichAdmin(); } default: { - throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); + throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`); } } } + void bootstrap(); diff --git a/server/src/apps/api.service.ts b/server/src/services/api.service.ts similarity index 100% rename from server/src/apps/api.service.ts rename to server/src/services/api.service.ts diff --git a/server/src/apps/microservices.service.ts b/server/src/services/microservices.service.ts similarity index 100% rename from server/src/apps/microservices.service.ts rename to server/src/services/microservices.service.ts diff --git a/server/test/utils.ts b/server/test/utils.ts index e87150dca8..c7732eabc1 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -5,16 +5,14 @@ import fs from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { EventEmitter } from 'node:stream'; -import { Server } from 'node:tls'; -import { ApiModule } from 'src/apps/api.module'; -import { ApiService } from 'src/apps/api.service'; -import { AppModule, AppTestModule } from 'src/apps/app.module'; -import { MicroservicesService } from 'src/apps/microservices.service'; +import { AppTestModule } from 'src/app.module'; import { dataSource } from 'src/database.config'; import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface'; import { IMediaRepository } from 'src/interfaces/media.interface'; import { StorageEventType } from 'src/interfaces/storage.interface'; import { MediaRepository } from 'src/repositories/media.repository'; +import { ApiService } from 'src/services/api.service'; +import { MicroservicesService } from 'src/services/microservices.service'; import { EntityTarget, ObjectLiteral } from 'typeorm'; export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string; @@ -104,12 +102,7 @@ let app: INestApplication; export const testApp = { create: async (): Promise => { - const moduleFixture = await Test.createTestingModule({ - imports: [ApiModule], - providers: [ApiService, MicroservicesService], - }) - .overrideModule(AppModule) - .useModule(AppTestModule) + const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] }) .overrideProvider(IJobRepository) .useClass(JobMock) .overrideProvider(IMediaRepository) @@ -117,15 +110,11 @@ export const testApp = { .compile(); app = await moduleFixture.createNestApplication().init(); - await app.listen(0); + await app.get(ApiService).init(); await db.reset(); await app.get(ApiService).init(); await app.get(MicroservicesService).init(); - const port = app.getHttpServer().address().port; - const protocol = app instanceof Server ? 'https' : 'http'; - process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port; - return app; }, reset: async (options?: ResetOptions) => {