mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	refactor: migrate activity repo to kysely (#15203)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
		
							parent
							
								
									2e12c46980
								
							
						
					
					
						commit
						1fb2b3f899
					
				@ -1,3 +1,5 @@
 | 
			
		||||
import { Insertable } from 'kysely';
 | 
			
		||||
import { Activity } from 'src/db';
 | 
			
		||||
import { ActivityEntity } from 'src/entities/activity.entity';
 | 
			
		||||
import { ActivitySearch } from 'src/repositories/activity.repository';
 | 
			
		||||
 | 
			
		||||
@ -5,7 +7,7 @@ export const IActivityRepository = 'IActivityRepository';
 | 
			
		||||
 | 
			
		||||
export interface IActivityRepository {
 | 
			
		||||
  search(options: ActivitySearch): Promise<ActivityEntity[]>;
 | 
			
		||||
  create(activity: Partial<ActivityEntity>): Promise<ActivityEntity>;
 | 
			
		||||
  create(activity: Insertable<Activity>): Promise<ActivityEntity>;
 | 
			
		||||
  delete(id: string): Promise<void>;
 | 
			
		||||
  getStatistics(assetId: string | undefined, albumId: string): Promise<number>;
 | 
			
		||||
  getStatistics(options: { albumId: string; assetId?: string }): Promise<number>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,85 +1,41 @@
 | 
			
		||||
-- NOTE: This file is auto generated by ./sql-generator
 | 
			
		||||
 | 
			
		||||
-- ActivityRepository.search
 | 
			
		||||
SELECT
 | 
			
		||||
  "ActivityEntity"."id" AS "ActivityEntity_id",
 | 
			
		||||
  "ActivityEntity"."createdAt" AS "ActivityEntity_createdAt",
 | 
			
		||||
  "ActivityEntity"."updatedAt" AS "ActivityEntity_updatedAt",
 | 
			
		||||
  "ActivityEntity"."albumId" AS "ActivityEntity_albumId",
 | 
			
		||||
  "ActivityEntity"."userId" AS "ActivityEntity_userId",
 | 
			
		||||
  "ActivityEntity"."assetId" AS "ActivityEntity_assetId",
 | 
			
		||||
  "ActivityEntity"."comment" AS "ActivityEntity_comment",
 | 
			
		||||
  "ActivityEntity"."isLiked" AS "ActivityEntity_isLiked",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."id" AS "ActivityEntity__ActivityEntity_user_id",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."name" AS "ActivityEntity__ActivityEntity_user_name",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."isAdmin" AS "ActivityEntity__ActivityEntity_user_isAdmin",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."email" AS "ActivityEntity__ActivityEntity_user_email",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."storageLabel" AS "ActivityEntity__ActivityEntity_user_storageLabel",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."oauthId" AS "ActivityEntity__ActivityEntity_user_oauthId",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."profileImagePath" AS "ActivityEntity__ActivityEntity_user_profileImagePath",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."shouldChangePassword" AS "ActivityEntity__ActivityEntity_user_shouldChangePassword",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."createdAt" AS "ActivityEntity__ActivityEntity_user_createdAt",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."deletedAt" AS "ActivityEntity__ActivityEntity_user_deletedAt",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes",
 | 
			
		||||
  "ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt"
 | 
			
		||||
FROM
 | 
			
		||||
  "activity" "ActivityEntity"
 | 
			
		||||
  LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
 | 
			
		||||
  AND (
 | 
			
		||||
    "ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL
 | 
			
		||||
  )
 | 
			
		||||
  LEFT JOIN "assets" "ActivityEntity__ActivityEntity_asset" ON "ActivityEntity__ActivityEntity_asset"."id" = "ActivityEntity"."assetId"
 | 
			
		||||
  AND (
 | 
			
		||||
    "ActivityEntity__ActivityEntity_asset"."deletedAt" IS NULL
 | 
			
		||||
  )
 | 
			
		||||
WHERE
 | 
			
		||||
select
 | 
			
		||||
  "activity".*,
 | 
			
		||||
  (
 | 
			
		||||
    ("ActivityEntity"."albumId" = $1)
 | 
			
		||||
    AND (
 | 
			
		||||
    select
 | 
			
		||||
      to_json(obj)
 | 
			
		||||
    from
 | 
			
		||||
      (
 | 
			
		||||
        (
 | 
			
		||||
          "ActivityEntity__ActivityEntity_asset"."deletedAt" IS NULL
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    AND (
 | 
			
		||||
      (
 | 
			
		||||
        (
 | 
			
		||||
          "ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
ORDER BY
 | 
			
		||||
  "ActivityEntity"."createdAt" ASC
 | 
			
		||||
        select
 | 
			
		||||
          *
 | 
			
		||||
        from
 | 
			
		||||
          "users"
 | 
			
		||||
        where
 | 
			
		||||
          "users"."id" = "activity"."userId"
 | 
			
		||||
          and "users"."deletedAt" is null
 | 
			
		||||
      ) as obj
 | 
			
		||||
  ) as "user"
 | 
			
		||||
from
 | 
			
		||||
  "activity"
 | 
			
		||||
  left join "assets" on "assets"."id" = "activity"."assetId"
 | 
			
		||||
  and "assets"."deletedAt" is null
 | 
			
		||||
where
 | 
			
		||||
  "activity"."albumId" = $1
 | 
			
		||||
order by
 | 
			
		||||
  "activity"."createdAt" asc
 | 
			
		||||
 | 
			
		||||
-- ActivityRepository.getStatistics
 | 
			
		||||
SELECT
 | 
			
		||||
  COUNT(DISTINCT ("ActivityEntity"."id")) AS "cnt"
 | 
			
		||||
FROM
 | 
			
		||||
  "activity" "ActivityEntity"
 | 
			
		||||
  LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
 | 
			
		||||
  LEFT JOIN "assets" "ActivityEntity__ActivityEntity_asset" ON "ActivityEntity__ActivityEntity_asset"."id" = "ActivityEntity"."assetId"
 | 
			
		||||
WHERE
 | 
			
		||||
  (
 | 
			
		||||
    ("ActivityEntity"."assetId" = $1)
 | 
			
		||||
    AND ("ActivityEntity"."albumId" = $2)
 | 
			
		||||
    AND ("ActivityEntity"."isLiked" = $3)
 | 
			
		||||
    AND (
 | 
			
		||||
      (
 | 
			
		||||
        (
 | 
			
		||||
          "ActivityEntity__ActivityEntity_asset"."deletedAt" IS NULL
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
    AND (
 | 
			
		||||
      (
 | 
			
		||||
        (
 | 
			
		||||
          "ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
select
 | 
			
		||||
  count(*) as "count"
 | 
			
		||||
from
 | 
			
		||||
  "activity"
 | 
			
		||||
  left join "users" on "users"."id" = "activity"."userId"
 | 
			
		||||
  left join "assets" on "assets"."id" = "activity"."assetId"
 | 
			
		||||
where
 | 
			
		||||
  "activity"."assetId" = $1
 | 
			
		||||
  and "activity"."albumId" = $2
 | 
			
		||||
  and "activity"."isLiked" = $3
 | 
			
		||||
  and "users"."deletedAt" is null
 | 
			
		||||
  and "assets"."deletedAt" is null
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,12 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { ExpressionBuilder, Insertable, Kysely } from 'kysely';
 | 
			
		||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
 | 
			
		||||
import { InjectKysely } from 'nestjs-kysely';
 | 
			
		||||
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 { IsNull, Repository } from 'typeorm';
 | 
			
		||||
import { asUuid } from 'src/utils/database';
 | 
			
		||||
 | 
			
		||||
export interface ActivitySearch {
 | 
			
		||||
  albumId?: string;
 | 
			
		||||
@ -12,73 +15,71 @@ export interface ActivitySearch {
 | 
			
		||||
  isLiked?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const withUser = (eb: ExpressionBuilder<DB, 'activity'>) => {
 | 
			
		||||
  return jsonObjectFrom(
 | 
			
		||||
    eb
 | 
			
		||||
      .selectFrom('users')
 | 
			
		||||
      .selectAll()
 | 
			
		||||
      .whereRef('users.id', '=', 'activity.userId')
 | 
			
		||||
      .where('users.deletedAt', 'is', null),
 | 
			
		||||
  ).as('user');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ActivityRepository implements IActivityRepository {
 | 
			
		||||
  constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
 | 
			
		||||
  constructor(@InjectKysely() private db: Kysely<DB>) {}
 | 
			
		||||
 | 
			
		||||
  @GenerateSql({ params: [{ albumId: DummyValue.UUID }] })
 | 
			
		||||
  search(options: ActivitySearch): Promise<ActivityEntity[]> {
 | 
			
		||||
    const { userId, assetId, albumId, isLiked } = options;
 | 
			
		||||
    return this.repository.find({
 | 
			
		||||
      where: {
 | 
			
		||||
        userId,
 | 
			
		||||
        assetId: assetId === null ? IsNull() : assetId,
 | 
			
		||||
        albumId,
 | 
			
		||||
        isLiked,
 | 
			
		||||
        asset: {
 | 
			
		||||
          deletedAt: IsNull(),
 | 
			
		||||
        },
 | 
			
		||||
        user: {
 | 
			
		||||
          deletedAt: IsNull(),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      relations: {
 | 
			
		||||
        user: true,
 | 
			
		||||
      },
 | 
			
		||||
      order: {
 | 
			
		||||
        createdAt: 'ASC',
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return this.db
 | 
			
		||||
      .selectFrom('activity')
 | 
			
		||||
      .selectAll('activity')
 | 
			
		||||
      .select(withUser)
 | 
			
		||||
      .leftJoin('assets', (join) => join.onRef('assets.id', '=', 'activity.assetId').on('assets.deletedAt', 'is', null))
 | 
			
		||||
      .$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!))
 | 
			
		||||
      .$if(assetId === null, (qb) => qb.where('assetId', 'is', null))
 | 
			
		||||
      .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!))
 | 
			
		||||
      .$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[]>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  create(entity: Partial<ActivityEntity>): Promise<ActivityEntity> {
 | 
			
		||||
    return this.save(entity);
 | 
			
		||||
  async create(activity: Insertable<Activity>) {
 | 
			
		||||
    return this.save(activity);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(id: string): Promise<void> {
 | 
			
		||||
    await this.repository.delete(id);
 | 
			
		||||
    await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
 | 
			
		||||
  getStatistics(assetId: string, albumId: string): Promise<number> {
 | 
			
		||||
    return this.repository.count({
 | 
			
		||||
      where: {
 | 
			
		||||
        assetId,
 | 
			
		||||
        albumId,
 | 
			
		||||
        isLiked: false,
 | 
			
		||||
        asset: {
 | 
			
		||||
          deletedAt: IsNull(),
 | 
			
		||||
        },
 | 
			
		||||
        user: {
 | 
			
		||||
          deletedAt: IsNull(),
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      relations: {
 | 
			
		||||
        user: true,
 | 
			
		||||
      },
 | 
			
		||||
      withDeleted: true,
 | 
			
		||||
    });
 | 
			
		||||
  @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] })
 | 
			
		||||
  async getStatistics({ albumId, assetId }: { albumId: string; assetId?: string }): Promise<number> {
 | 
			
		||||
    const { count } = await this.db
 | 
			
		||||
      .selectFrom('activity')
 | 
			
		||||
      .select((eb) => eb.fn.countAll().as('count'))
 | 
			
		||||
      .leftJoin('users', 'users.id', 'activity.userId')
 | 
			
		||||
      .leftJoin('assets', 'assets.id', 'activity.assetId')
 | 
			
		||||
      .$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!))
 | 
			
		||||
      .where('activity.albumId', '=', albumId)
 | 
			
		||||
      .where('activity.isLiked', '=', false)
 | 
			
		||||
      .where('users.deletedAt', 'is', null)
 | 
			
		||||
      .where('assets.deletedAt', 'is', null)
 | 
			
		||||
      .executeTakeFirstOrThrow();
 | 
			
		||||
 | 
			
		||||
    return count as number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async save(entity: Partial<ActivityEntity>) {
 | 
			
		||||
    const { id } = await this.repository.save(entity);
 | 
			
		||||
    return this.repository.findOneOrFail({
 | 
			
		||||
      where: {
 | 
			
		||||
        id,
 | 
			
		||||
      },
 | 
			
		||||
      relations: {
 | 
			
		||||
        user: true,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  private async save(entity: Insertable<Activity>) {
 | 
			
		||||
    const { id } = await this.db.insertInto('activity').values(entity).returning('id').executeTakeFirstOrThrow();
 | 
			
		||||
 | 
			
		||||
    return this.db
 | 
			
		||||
      .selectFrom('activity')
 | 
			
		||||
      .selectAll('activity')
 | 
			
		||||
      .select(withUser)
 | 
			
		||||
      .where('activity.id', '=', asUuid(id))
 | 
			
		||||
      .executeTakeFirstOrThrow() as unknown as Promise<ActivityEntity>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ export class ActivityService extends BaseService {
 | 
			
		||||
 | 
			
		||||
  async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
 | 
			
		||||
    await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
 | 
			
		||||
    return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
 | 
			
		||||
    return { comments: await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }) };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user