0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-28 00:59:18 -05:00

fix: activity types (#15368)

This commit is contained in:
Jason Rasmussen 2025-01-15 23:31:26 -05:00 committed by GitHub
parent 0ce62d8efd
commit 6ce1533117
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 75 additions and 57 deletions

View file

@ -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) {

View file

@ -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();

3
server/src/database.ts Normal file
View file

@ -0,0 +1,3 @@
export const columns = {
userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'],
} as const;

View file

@ -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),
};
}
};

View file

@ -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<ActivityEntity[]>;
create(activity: Insertable<Activity>): Promise<ActivityEntity>;
delete(id: string): Promise<void>;
getStatistics(options: { albumId: string; assetId?: string }): Promise<number>;
}

View file

@ -9,7 +9,11 @@ select
from
(
select
*
"id",
"name",
"email",
"profileImagePath",
"profileChangedAt"
from
"users"
where

View file

@ -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<DB, 'activity'>) => {
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<DB>) {}
@GenerateSql({ params: [{ albumId: DummyValue.UUID }] })
search(options: ActivitySearch): Promise<ActivityEntity[]> {
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<ActivityEntity[]>;
.execute();
}
async create(activity: Insertable<Activity>) {
return this.save(activity);
}
async delete(id: string): Promise<void> {
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<ActivityEntity>;
.executeTakeFirstOrThrow();
}
}

View file

@ -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 },

View file

@ -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';

View file

@ -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) {

View file

@ -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,

View file

@ -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<T extends object> = Pick<T, keyof T>;
export type IActivityRepository = RepositoryInterface<ActivityRepository>;
export type ActivityItem =
| Awaited<ReturnType<IActivityRepository['create']>>
| Awaited<ReturnType<IActivityRepository['search']>>[0];

View file

@ -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<ActivityEntity>({
oneComment: Object.freeze<ActivityItem>({
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<ActivityEntity>({
liked: Object.freeze<ActivityItem>({
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(),
}),

View file

@ -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<IActivityRepository> => {

View file

@ -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 = <T extends BaseService>(
const sut = new Service(
loggerMock,
accessMock,
activityMock,
activityMock as IActivityRepository as ActivityRepository,
auditMock,
albumMock,
albumUserMock,