mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 01:11:20 +03:00
chore: move apps and test utils (#8129)
This commit is contained in:
@@ -4,10 +4,10 @@ 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 { excludePaths } from 'src/config';
|
||||
import { WEB_ROOT, envName, isDev, serverVersion } from 'src/domain/domain.constant';
|
||||
import { AppModule } from 'src/immich/app.module';
|
||||
import { AppService } from 'src/immich/app.service';
|
||||
import { useSwagger } from 'src/immich/app.utils';
|
||||
import { otelSDK } from 'src/infra/instrumentation';
|
||||
import { ImmichLogger } from 'src/infra/logger';
|
||||
@@ -16,9 +16,9 @@ import { WebSocketAdapter } from 'src/infra/websocket.adapter';
|
||||
const logger = new ImmichLogger('ImmichServer');
|
||||
const port = Number(process.env.SERVER_PORT) || 3001;
|
||||
|
||||
export async function bootstrap() {
|
||||
export async function bootstrapApi() {
|
||||
otelSDK.start();
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, { bufferLogs: true });
|
||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
||||
|
||||
app.useLogger(app.get(ImmichLogger));
|
||||
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
||||
@@ -48,7 +48,7 @@ export async function bootstrap() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
app.use(app.get(AppService).ssr(excludePaths));
|
||||
app.use(app.get(ApiService).ssr(excludePaths));
|
||||
|
||||
const server = await app.listen(port);
|
||||
server.requestTimeout = 30 * 60 * 1000;
|
||||
@@ -2,6 +2,7 @@ import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ApiService } from 'src/apps/api.service';
|
||||
import { ActivityController } from 'src/controllers/activity.controller';
|
||||
import { AlbumController } from 'src/controllers/album.controller';
|
||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
@@ -29,7 +30,6 @@ import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { AssetRepositoryV1, IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
|
||||
import { AssetController as AssetControllerV1 } from 'src/immich/api-v1/asset/asset.controller';
|
||||
import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
|
||||
import { AppService } from 'src/immich/app.service';
|
||||
import { InfraModule } from 'src/infra/infra.module';
|
||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||
@@ -73,13 +73,13 @@ import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
||||
{ provide: APP_GUARD, useClass: AuthGuard },
|
||||
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
|
||||
AppService,
|
||||
ApiService,
|
||||
AssetServiceV1,
|
||||
FileUploadInterceptor,
|
||||
],
|
||||
})
|
||||
export class AppModule implements OnModuleInit {
|
||||
constructor(private appService: AppService) {}
|
||||
export class ApiModule implements OnModuleInit {
|
||||
constructor(private appService: ApiService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.appService.init();
|
||||
@@ -35,8 +35,8 @@ const render = (index: string, meta: OpenGraphTags) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
private logger = new ImmichLogger(AppService.name);
|
||||
export class ApiService {
|
||||
private logger = new ImmichLogger(ApiService.name);
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
8
server/src/apps/immich-admin.main.ts
Executable file
8
server/src/apps/immich-admin.main.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
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);
|
||||
}
|
||||
@@ -18,4 +18,4 @@ import { InfraModule } from 'src/infra/infra.module';
|
||||
ListUsersCommand,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class ImmichAdminModule {}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { MicroservicesModule } from 'src/apps/microservices.module';
|
||||
import { envName, serverVersion } from 'src/domain/domain.constant';
|
||||
import { otelSDK } from 'src/infra/instrumentation';
|
||||
import { ImmichLogger } from 'src/infra/logger';
|
||||
import { WebSocketAdapter } from 'src/infra/websocket.adapter';
|
||||
import { MicroservicesModule } from 'src/microservices/microservices.module';
|
||||
|
||||
const logger = new ImmichLogger('ImmichMicroservice');
|
||||
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
|
||||
|
||||
export async function bootstrap() {
|
||||
export async function bootstrapMicroservices() {
|
||||
otelSDK.start();
|
||||
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
|
||||
app.useLogger(app.get(ImmichLogger));
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { MicroservicesService } from 'src/apps/microservices.service';
|
||||
import { DomainModule } from 'src/domain/domain.module';
|
||||
import { InfraModule } from 'src/infra/infra.module';
|
||||
import { AppService } from 'src/microservices/app.service';
|
||||
|
||||
@Module({
|
||||
imports: [InfraModule, DomainModule],
|
||||
providers: [AppService],
|
||||
providers: [MicroservicesService],
|
||||
})
|
||||
export class MicroservicesModule implements OnModuleInit {
|
||||
constructor(private appService: AppService) {}
|
||||
constructor(private appService: MicroservicesService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.appService.init();
|
||||
@@ -17,7 +17,7 @@ import { UserService } from 'src/domain/user/user.service';
|
||||
import { otelSDK } from 'src/infra/instrumentation';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
export class MicroservicesService {
|
||||
constructor(
|
||||
private auditService: AuditService,
|
||||
private assetService: AssetService,
|
||||
@@ -1,8 +0,0 @@
|
||||
import { CommandFactory } from 'nest-commander';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { AppModule } from 'src/immich-admin/app.module';
|
||||
|
||||
export async function bootstrap() {
|
||||
process.env.LOG_LEVEL = LogLevel.WARN;
|
||||
await CommandFactory.run(AppModule);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { bootstrap as admin } from 'src/immich-admin/main';
|
||||
import { bootstrap as server } from 'src/immich/main';
|
||||
import { bootstrap as microservices } from 'src/microservices/main';
|
||||
import { bootstrapApi } from 'src/apps/api.main';
|
||||
import { bootstrapImmichAdmin } from 'src/apps/immich-admin.main';
|
||||
import { bootstrapMicroservices } from 'src/apps/microservices.main';
|
||||
|
||||
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
||||
|
||||
@@ -12,15 +12,15 @@ function bootstrap() {
|
||||
switch (immichApp) {
|
||||
case 'immich': {
|
||||
process.title = 'immich_server';
|
||||
return server();
|
||||
return bootstrapApi();
|
||||
}
|
||||
case 'microservices': {
|
||||
process.title = 'immich_microservices';
|
||||
return microservices();
|
||||
return bootstrapMicroservices();
|
||||
}
|
||||
case 'immich-admin': {
|
||||
process.title = 'immich_admin_cli';
|
||||
return admin();
|
||||
return bootstrapImmichAdmin();
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DateTime } from 'luxon';
|
||||
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 { QueueName } from 'src/domain/job/job.constants';
|
||||
import { AppModule } from 'src/immich/app.module';
|
||||
import { AppService } from 'src/immich/app.service';
|
||||
import { dataSource } from 'src/infra/database.config';
|
||||
import { InfraModule, InfraTestModule } from 'src/infra/infra.module';
|
||||
import { IJobRepository, JobItem, JobItemHandler } from 'src/interfaces/job.repository';
|
||||
import { IMediaRepository } from 'src/interfaces/media.repository';
|
||||
import { StorageEventType } from 'src/interfaces/storage.repository';
|
||||
import { AppService as MicroAppService } from 'src/microservices/app.service';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string;
|
||||
export const IMMICH_TEST_ASSET_TEMP_PATH = join(tmpdir(), 'immich');
|
||||
|
||||
export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 });
|
||||
export const yesterday = today.minus({ days: 1 });
|
||||
|
||||
export interface ResetOptions {
|
||||
entities?: EntityTarget<ObjectLiteral>[];
|
||||
}
|
||||
export const db = {
|
||||
reset: async (options?: ResetOptions) => {
|
||||
if (!dataSource.isInitialized) {
|
||||
await dataSource.initialize();
|
||||
}
|
||||
await dataSource.transaction(async (em) => {
|
||||
const entities = options?.entities || [];
|
||||
const tableNames =
|
||||
entities.length > 0
|
||||
? entities.map((entity) => em.getRepository(entity).metadata.tableName)
|
||||
: dataSource.entityMetadatas
|
||||
.map((entity) => entity.tableName)
|
||||
.filter((tableName) => !tableName.startsWith('geodata'));
|
||||
|
||||
let deleteUsers = false;
|
||||
for (const tableName of tableNames) {
|
||||
if (tableName === 'users') {
|
||||
deleteUsers = true;
|
||||
continue;
|
||||
}
|
||||
await em.query(`DELETE FROM ${tableName} CASCADE;`);
|
||||
}
|
||||
if (deleteUsers) {
|
||||
await em.query(`DELETE FROM "users" CASCADE;`);
|
||||
}
|
||||
|
||||
// Release all locks
|
||||
await em.query('SELECT pg_advisory_unlock_all()');
|
||||
});
|
||||
},
|
||||
disconnect: async () => {
|
||||
if (dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class JobMock implements IJobRepository {
|
||||
private _handler: JobItemHandler = () => Promise.resolve();
|
||||
addHandler(_queueName: QueueName, _concurrency: number, handler: JobItemHandler) {
|
||||
this._handler = handler;
|
||||
}
|
||||
addCronJob() {}
|
||||
updateCronJob() {}
|
||||
deleteCronJob() {}
|
||||
validateCronExpression() {}
|
||||
queue(item: JobItem) {
|
||||
return this._handler(item);
|
||||
}
|
||||
queueAll(items: JobItem[]) {
|
||||
return Promise.all(items.map((arg) => this._handler(arg))).then(() => {});
|
||||
}
|
||||
async resume() {}
|
||||
async empty() {}
|
||||
async setConcurrency() {}
|
||||
getQueueStatus() {
|
||||
return Promise.resolve(null) as any;
|
||||
}
|
||||
getJobCounts() {
|
||||
return Promise.resolve(null) as any;
|
||||
}
|
||||
async pause() {}
|
||||
clear() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
async waitForQueueCompletion() {}
|
||||
}
|
||||
|
||||
class MediaMockRepository extends MediaRepository {
|
||||
generateThumbhash() {
|
||||
return Promise.resolve(Buffer.from('mock-thumbhash'));
|
||||
}
|
||||
}
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
export const testApp = {
|
||||
create: async (): Promise<INestApplication> => {
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
providers: [AppService, MicroAppService],
|
||||
})
|
||||
.overrideModule(InfraModule)
|
||||
.useModule(InfraTestModule)
|
||||
.overrideProvider(IJobRepository)
|
||||
.useClass(JobMock)
|
||||
.overrideProvider(IMediaRepository)
|
||||
.useClass(MediaMockRepository)
|
||||
.compile();
|
||||
|
||||
app = await moduleFixture.createNestApplication().init();
|
||||
await app.listen(0);
|
||||
await db.reset();
|
||||
await app.get(AppService).init();
|
||||
await app.get(MicroAppService).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) => {
|
||||
await db.reset(options);
|
||||
},
|
||||
get: (member: any) => app.get(member),
|
||||
teardown: async () => {
|
||||
if (app) {
|
||||
await app.get(MicroAppService).teardown();
|
||||
await app.close();
|
||||
}
|
||||
await db.disconnect();
|
||||
},
|
||||
};
|
||||
|
||||
export function waitForEvent(emitter: EventEmitter, event: string, times = 1): Promise<void[]> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 1; i <= times; i++) {
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
const success = (value: any) => {
|
||||
emitter.off(StorageEventType.ERROR, fail);
|
||||
resolve(value);
|
||||
};
|
||||
const fail = (error: Error) => {
|
||||
emitter.off(event, success);
|
||||
reject(error);
|
||||
};
|
||||
emitter.once(event, success);
|
||||
emitter.once(StorageEventType.ERROR, fail);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
const directoryExists = async (dirPath: string) =>
|
||||
await fs.promises
|
||||
.access(dirPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
export async function restoreTempFolder(): Promise<void> {
|
||||
if (await directoryExists(`${IMMICH_TEST_ASSET_TEMP_PATH}`)) {
|
||||
// Temp directory exists, delete all files inside it
|
||||
await fs.promises.rm(IMMICH_TEST_ASSET_TEMP_PATH, { recursive: true });
|
||||
}
|
||||
// Create temp folder
|
||||
await fs.promises.mkdir(IMMICH_TEST_ASSET_TEMP_PATH);
|
||||
}
|
||||
Reference in New Issue
Block a user