diff --git a/server/src/app.module.ts b/server/src/app.module.ts index d0422756b6..5e6432408f 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -20,14 +20,14 @@ import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; -import { repositories } from 'src/repositories'; +import { providers, repositories } from 'src/repositories'; import { ConfigRepository } from 'src/repositories/config.repository'; import { teardownTelemetry } from 'src/repositories/telemetry.repository'; import { services } from 'src/services'; import { CliService } from 'src/services/cli.service'; import { DatabaseService } from 'src/services/database.service'; -const common = [...services, ...repositories]; +const common = [...services, ...providers, ...repositories]; const middleware = [ FileUploadInterceptor, @@ -73,7 +73,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy { } async onModuleInit() { - this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) }); + this.telemetryRepository.setup({ repositories: [...providers.map(({ useClass }) => useClass), ...repositories] }); this.jobRepository.setup({ services }); if (this.worker === ImmichWorker.MICROSERVICES) { diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 22dae63750..21eff3b306 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -12,7 +12,7 @@ import { format } from 'sql-formatter'; import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators'; import { entities } from 'src/entities'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { repositories } from 'src/repositories'; +import { providers, repositories } from 'src/repositories'; import { AccessRepository } from 'src/repositories/access.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { AuthService } from 'src/services/auth.service'; @@ -43,7 +43,7 @@ export class SqlLogger implements Logger { const reflector = new Reflector(); -type Repository = (typeof repositories)[0]['useClass']; +type Repository = (typeof providers)[0]['useClass']; type Provider = { provide: any; useClass: Repository }; type SqlGeneratorOptions = { targetDir: string }; @@ -57,7 +57,11 @@ class SqlGenerator { async run() { try { await this.setup(); - for (const repository of repositories) { + const targets = [ + ...providers, + ...repositories.map((repository) => ({ provide: repository, useClass: repository as any })), + ]; + for (const repository of targets) { if (repository.provide === ILoggerRepository) { continue; } @@ -99,7 +103,7 @@ class SqlGenerator { TypeOrmModule.forFeature(entities), OpenTelemetryModule.forRoot(otel), ], - providers: [...repositories, AuthService, SchedulerRegistry], + providers: [...providers, ...repositories, AuthService, SchedulerRegistry], }).compile(); this.app = await moduleFixture.createNestApplication().init(); diff --git a/server/src/database.ts b/server/src/database.ts new file mode 100644 index 0000000000..fce9ede561 --- /dev/null +++ b/server/src/database.ts @@ -0,0 +1,3 @@ +export const columns = { + userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'], +} as const; diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 4bc0065244..9a0307f46b 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { ActivityEntity } from 'src/entities/activity.entity'; +import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; +import { UserEntity } from 'src/entities/user.entity'; +import { ActivityItem } from 'src/types'; import { Optional, ValidateUUID } from 'src/validation'; export enum ReactionType { @@ -67,13 +68,13 @@ export class ActivityCreateDto extends ActivityDto { comment?: string; } -export function mapActivity(activity: ActivityEntity): ActivityResponseDto { +export const mapActivity = (activity: ActivityItem): ActivityResponseDto => { return { id: activity.id, assetId: activity.assetId, createdAt: activity.createdAt, comment: activity.comment, type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT, - user: mapUser(activity.user), + user: mapUser(activity.user as unknown as UserEntity), }; -} +}; diff --git a/server/src/interfaces/activity.interface.ts b/server/src/interfaces/activity.interface.ts deleted file mode 100644 index c42d3cc8aa..0000000000 --- a/server/src/interfaces/activity.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Insertable } from 'kysely'; -import { Activity } from 'src/db'; -import { ActivityEntity } from 'src/entities/activity.entity'; -import { ActivitySearch } from 'src/repositories/activity.repository'; - -export const IActivityRepository = 'IActivityRepository'; - -export interface IActivityRepository { - search(options: ActivitySearch): Promise; - create(activity: Insertable): Promise; - delete(id: string): Promise; - getStatistics(options: { albumId: string; assetId?: string }): Promise; -} diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 5f25a7dcbd..8e9bb11f25 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -9,7 +9,11 @@ select from ( select - * + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" from "users" where diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 6ed82abdfc..99d3192341 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -2,10 +2,9 @@ import { Injectable } from '@nestjs/common'; import { ExpressionBuilder, Insertable, Kysely } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; import { Activity, DB } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { ActivityEntity } from 'src/entities/activity.entity'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { asUuid } from 'src/utils/database'; export interface ActivitySearch { @@ -19,18 +18,18 @@ const withUser = (eb: ExpressionBuilder) => { return jsonObjectFrom( eb .selectFrom('users') - .selectAll() + .select(columns.userDto) .whereRef('users.id', '=', 'activity.userId') .where('users.deletedAt', 'is', null), ).as('user'); }; @Injectable() -export class ActivityRepository implements IActivityRepository { +export class ActivityRepository { constructor(@InjectKysely() private db: Kysely) {} @GenerateSql({ params: [{ albumId: DummyValue.UUID }] }) - search(options: ActivitySearch): Promise { + search(options: ActivitySearch) { const { userId, assetId, albumId, isLiked } = options; return this.db @@ -44,14 +43,14 @@ export class ActivityRepository implements IActivityRepository { .$if(!!albumId, (qb) => qb.where('activity.albumId', '=', albumId!)) .$if(isLiked !== undefined, (qb) => qb.where('activity.isLiked', '=', isLiked!)) .orderBy('activity.createdAt', 'asc') - .execute() as unknown as Promise; + .execute(); } async create(activity: Insertable) { return this.save(activity); } - async delete(id: string): Promise { + async delete(id: string) { await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute(); } @@ -79,6 +78,6 @@ export class ActivityRepository implements IActivityRepository { .selectAll('activity') .select(withUser) .where('activity.id', '=', asUuid(id)) - .executeTakeFirstOrThrow() as unknown as Promise; + .executeTakeFirstOrThrow(); } } diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index eb6a5d6f71..c48233f08f 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -1,5 +1,4 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -78,8 +77,12 @@ import { VersionHistoryRepository } from 'src/repositories/version-history.repos import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ + // + ActivityRepository, +]; + +export const providers = [ { provide: IAccessRepository, useClass: AccessRepository }, - { provide: IActivityRepository, useClass: ActivityRepository }, { provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index f9a8e6ce47..4ee656abe5 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { ReactionType } from 'src/dtos/activity.dto'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { ActivityService } from 'src/services/activity.service'; +import { IActivityRepository } from 'src/types'; import { activityStub } from 'test/fixtures/activity.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index ea7f8b5c0a..feb1074fb2 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -5,15 +5,15 @@ import { ActivityResponseDto, ActivitySearchDto, ActivityStatisticsResponseDto, + mapActivity, MaybeDuplicate, ReactionLevel, ReactionType, - mapActivity, } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ActivityEntity } from 'src/entities/activity.entity'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { ActivityItem } from 'src/types'; @Injectable() export class ActivityService extends BaseService { @@ -43,7 +43,7 @@ export class ActivityService extends BaseService { albumId: dto.albumId, }; - let activity: ActivityEntity | null = null; + let activity: ActivityItem | undefined; let duplicate = false; if (dto.type === ReactionType.LIKE) { diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 82852c27e2..9e024daacd 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -7,7 +7,6 @@ import { StorageCore } from 'src/cores/storage.core'; import { Users } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; @@ -45,6 +44,7 @@ import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { IViewRepository } from 'src/interfaces/view.interface'; +import { ActivityRepository } from 'src/repositories/activity.repository'; import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access'; import { getConfig, updateConfig } from 'src/utils/config'; @@ -54,7 +54,7 @@ export class BaseService { constructor( @Inject(ILoggerRepository) protected logger: ILoggerRepository, @Inject(IAccessRepository) protected accessRepository: IAccessRepository, - @Inject(IActivityRepository) protected activityRepository: IActivityRepository, + protected activityRepository: ActivityRepository, @Inject(IAuditRepository) protected auditRepository: IAuditRepository, @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository, diff --git a/server/src/types.ts b/server/src/types.ts index c55de4160d..0d3b037f9e 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,5 +1,6 @@ import { UserEntity } from 'src/entities/user.entity'; import { Permission } from 'src/enum'; +import { ActivityRepository } from 'src/repositories/activity.repository'; export type AuthApiKey = { id: string; @@ -7,3 +8,11 @@ export type AuthApiKey = { user: UserEntity; permissions: Permission[]; }; + +export type RepositoryInterface = Pick; + +export type IActivityRepository = RepositoryInterface; + +export type ActivityItem = + | Awaited> + | Awaited>[0]; diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts index 4805f6604d..9578bcd4a1 100644 --- a/server/test/fixtures/activity.stub.ts +++ b/server/test/fixtures/activity.stub.ts @@ -1,33 +1,39 @@ -import { ActivityEntity } from 'src/entities/activity.entity'; +import { ActivityItem } from 'src/types'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; export const activityStub = { - oneComment: Object.freeze({ + oneComment: Object.freeze({ id: 'activity-1', comment: 'comment', isLiked: false, - userId: authStub.admin.user.id, - user: userStub.admin, + userId: 'admin_id', + user: { + id: 'admin_id', + name: 'admin', + email: 'admin@test.com', + profileImagePath: '', + profileChangedAt: new Date('2021-01-01'), + }, assetId: assetStub.image.id, - asset: assetStub.image, albumId: albumStub.oneAsset.id, - album: albumStub.oneAsset, createdAt: new Date(), updatedAt: new Date(), }), - liked: Object.freeze({ + liked: Object.freeze({ id: 'activity-2', comment: null, isLiked: true, - userId: authStub.admin.user.id, - user: userStub.admin, + userId: 'admin_id', + user: { + id: 'admin_id', + name: 'admin', + email: 'admin@test.com', + profileImagePath: '', + profileChangedAt: new Date('2021-01-01'), + }, assetId: assetStub.image.id, - asset: assetStub.image, albumId: albumStub.oneAsset.id, - album: albumStub.oneAsset, createdAt: new Date(), updatedAt: new Date(), }), diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts index 9d29d90ab8..bcc27774e3 100644 --- a/server/test/repositories/activity.repository.mock.ts +++ b/server/test/repositories/activity.repository.mock.ts @@ -1,4 +1,4 @@ -import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { IActivityRepository } from 'src/types'; import { Mocked, vitest } from 'vitest'; export const newActivityRepositoryMock = (): Mocked => { diff --git a/server/test/utils.ts b/server/test/utils.ts index 7f5b75020c..bc0ada3259 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -3,7 +3,9 @@ import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import { ImmichWorker } from 'src/enum'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { ActivityRepository } from 'src/repositories/activity.repository'; import { BaseService } from 'src/services/base.service'; +import { IActivityRepository } from 'src/types'; import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; @@ -104,7 +106,7 @@ export const newTestService = ( const sut = new Service( loggerMock, accessMock, - activityMock, + activityMock as IActivityRepository as ActivityRepository, auditMock, albumMock, albumUserMock,