refactor: repositories (#16038)

This commit is contained in:
Jason Rasmussen
2025-02-11 15:12:31 -05:00
committed by GitHub
parent 9d85272c2b
commit 5f3a42a132
26 changed files with 216 additions and 242 deletions

View File

@@ -10,21 +10,15 @@ import {
import { ClassConstructor } from 'class-transformer';
import _ from 'lodash';
import { Server, Socket } from 'socket.io';
import { SystemConfig } from 'src/config';
import { EventConfig } from 'src/decorators';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { ImmichWorker, MetadataKey } from 'src/enum';
import {
ArgsOf,
ClientEventMap,
EmitEvent,
EmitHandler,
EventItem,
IEventRepository,
serverEvents,
ServerEvents,
} from 'src/interfaces/event.interface';
import { JobItem, QueueName } from 'src/interfaces/job.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { AuthService } from 'src/services/auth.service';
import { handlePromiseError } from 'src/utils/misc';
type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
@@ -37,14 +31,99 @@ type Item<T extends EmitEvent> = {
label: string;
};
type EventMap = {
// app events
'app.bootstrap': [];
'app.shutdown': [];
'config.init': [{ newConfig: SystemConfig }];
// config events
'config.update': [
{
newConfig: SystemConfig;
oldConfig: SystemConfig;
},
];
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
// album events
'album.update': [{ id: string; recipientIds: string[] }];
'album.invite': [{ id: string; userId: string }];
// asset events
'asset.tag': [{ assetId: string }];
'asset.untag': [{ assetId: string }];
'asset.hide': [{ assetId: string; userId: string }];
'asset.show': [{ assetId: string; userId: string }];
'asset.trash': [{ assetId: string; userId: string }];
'asset.delete': [{ assetId: string; userId: string }];
// asset bulk events
'assets.trash': [{ assetIds: string[]; userId: string }];
'assets.delete': [{ assetIds: string[]; userId: string }];
'assets.restore': [{ assetIds: string[]; userId: string }];
'job.start': [QueueName, JobItem];
// session events
'session.delete': [{ sessionId: string }];
// stack events
'stack.create': [{ stackId: string; userId: string }];
'stack.update': [{ stackId: string; userId: string }];
'stack.delete': [{ stackId: string; userId: string }];
// stack bulk events
'stacks.delete': [{ stackIds: string[]; userId: string }];
// user events
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
// websocket events
'websocket.connect': [{ userId: string }];
};
export const serverEvents = ['config.update'] as const;
export type ServerEvents = (typeof serverEvents)[number];
export type EmitEvent = keyof EventMap;
export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
export type ArgOf<T extends EmitEvent> = EventMap[T][0];
export type ArgsOf<T extends EmitEvent> = EventMap[T];
export interface ClientEventMap {
on_upload_success: [AssetResponseDto];
on_user_delete: [string];
on_asset_delete: [string];
on_asset_trash: [string[]];
on_asset_update: [AssetResponseDto];
on_asset_hidden: [string];
on_asset_restore: [string[]];
on_asset_stack_update: string[];
on_person_thumbnail: [string];
on_server_version: [ServerVersionResponseDto];
on_config_update: [];
on_new_release: [ReleaseNotification];
on_session_delete: [string];
}
export type EventItem<T extends EmitEvent> = {
event: T;
handler: EmitHandler<T>;
server: boolean;
};
export type AuthFn = (client: Socket) => Promise<AuthDto>;
@WebSocketGateway({
cors: true,
path: '/api/socket.io',
transports: ['websocket'],
})
@Injectable()
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
private emitHandlers: EmitHandlers = {};
private authFn?: AuthFn;
@WebSocketServer()
private server?: Server;
@@ -122,11 +201,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
async handleConnection(client: Socket) {
try {
this.logger.log(`Websocket Connect: ${client.id}`);
const auth = await this.moduleRef.get(AuthService).authenticate({
headers: client.request.headers,
queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
});
const auth = await this.authenticate(client);
await client.join(auth.user.id);
if (auth.session) {
await client.join(auth.session.id);
@@ -182,4 +257,16 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
this.logger.debug(`Server event: ${event} (send)`);
this.server?.serverSideEmit(event, ...args);
}
setAuthFn(fn: (client: Socket) => Promise<AuthDto>) {
this.authFn = fn;
}
private async authenticate(client: Socket) {
if (!this.authFn) {
throw new Error('Auth function not set');
}
return this.authFn(client);
}
}

View File

@@ -1,6 +1,4 @@
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@@ -53,8 +51,10 @@ export const repositories = [
CronRepository,
CryptoRepository,
DatabaseRepository,
EventRepository,
LibraryRepository,
LoggingRepository,
MachineLearningRepository,
MapRepository,
MediaRepository,
MemoryRepository,
@@ -80,8 +80,4 @@ export const repositories = [
VersionHistoryRepository,
];
export const providers = [
{ provide: IEventRepository, useClass: EventRepository },
{ provide: IJobRepository, useClass: JobRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
];
export const providers = [{ provide: IJobRepository, useClass: JobRepository }];

View File

@@ -1,12 +1,11 @@
import { getQueueToken } from '@nestjs/bullmq';
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { ModuleRef, Reflector } from '@nestjs/core';
import { JobsOptions, Queue, Worker } from 'bullmq';
import { ClassConstructor } from 'class-transformer';
import { setTimeout } from 'node:timers/promises';
import { JobConfig } from 'src/decorators';
import { MetadataKey } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
IEntityJob,
IJobRepository,
@@ -20,6 +19,7 @@ import {
QueueStatus,
} from 'src/interfaces/job.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
@@ -38,7 +38,7 @@ export class JobRepository implements IJobRepository {
constructor(
private moduleRef: ModuleRef,
private configRepository: ConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
private eventRepository: EventRepository,
private logger: LoggingRepository,
) {
this.logger.setContext(JobRepository.name);

View File

@@ -1,21 +1,60 @@
import { Injectable } from '@nestjs/common';
import { readFile } from 'node:fs/promises';
import { CLIPConfig } from 'src/dtos/model-config.dto';
import {
ClipTextualResponse,
ClipVisualResponse,
FaceDetectionOptions,
FacialRecognitionResponse,
IMachineLearningRepository,
MachineLearningRequest,
ModelPayload,
ModelTask,
ModelType,
} from 'src/interfaces/machine-learning.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
export interface BoundingBox {
x1: number;
y1: number;
x2: number;
y2: number;
}
export enum ModelTask {
FACIAL_RECOGNITION = 'facial-recognition',
SEARCH = 'clip',
}
export enum ModelType {
DETECTION = 'detection',
PIPELINE = 'pipeline',
RECOGNITION = 'recognition',
TEXTUAL = 'textual',
VISUAL = 'visual',
}
export type ModelPayload = { imagePath: string } | { text: string };
type ModelOptions = { modelName: string };
export type FaceDetectionOptions = ModelOptions & { minScore: number };
type VisualResponse = { imageHeight: number; imageWidth: number };
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
export type FacialRecognitionRequest = {
[ModelTask.FACIAL_RECOGNITION]: {
[ModelType.DETECTION]: ModelOptions & { options: { minScore: number } };
[ModelType.RECOGNITION]: ModelOptions;
};
};
export interface Face {
boundingBox: BoundingBox;
embedding: string;
score: number;
}
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
export type DetectedFaces = { faces: Face[] } & VisualResponse;
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
@Injectable()
export class MachineLearningRepository implements IMachineLearningRepository {
export class MachineLearningRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(MachineLearningRepository.name);
}