diff --git a/server/src/database.ts b/server/src/database.ts index 5f2d5c5123..f8af8438a0 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -30,6 +30,19 @@ export type AuthApiKey = { permissions: Permission[]; }; +export type Activity = { + id: string; + createdAt: Date; + updatedAt: Date; + albumId: string; + userId: string; + user: User; + assetId: string | null; + comment: string | null; + isLiked: boolean; + updateId: string; +}; + export type ApiKey = { id: string; name: string; @@ -173,6 +186,7 @@ export const columns = { 'shared_links.password', ], user: userColumns, + userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'], userAdmin: [ ...userColumns, 'createdAt', diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 9a0307f46b..98216147b7 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { Activity } from 'src/database'; 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 { @@ -68,7 +68,7 @@ export class ActivityCreateDto extends ActivityDto { comment?: string; } -export const mapActivity = (activity: ActivityItem): ActivityResponseDto => { +export const mapActivity = (activity: Activity): ActivityResponseDto => { return { id: activity.id, assetId: activity.assetId, diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 0ddb91c692..3d4d667de6 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -3,6 +3,38 @@ -- ActivityRepository.search select "activity".*, + to_json("user") as "user" +from + "activity" + inner join "users" on "users"."id" = "activity"."userId" + and "users"."deletedAt" is null + inner join lateral ( + select + "users"."id", + "users"."name", + "users"."email", + "users"."profileImagePath", + "users"."profileChangedAt" + from + ( + select + 1 + ) as "dummy" + ) as "user" on true + left join "assets" on "assets"."id" = "activity"."assetId" + and "assets"."deletedAt" is null +where + "activity"."albumId" = $1 +order by + "activity"."createdAt" asc + +-- ActivityRepository.create +insert into + "activity" ("albumId", "userId") +values + ($1, $2) +returning + *, ( select to_json(obj) @@ -18,17 +50,13 @@ select "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 + +-- ActivityRepository.delete +delete from "activity" where - "activity"."albumId" = $1 -order by - "activity"."createdAt" asc + "id" = $1::uuid -- ActivityRepository.getStatistics select diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 48def82f49..e266022b05 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { ExpressionBuilder, Insertable, Kysely } from 'kysely'; +import { Insertable, Kysely, NotNull, sql } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; import { columns } from 'src/database'; @@ -14,16 +14,6 @@ export interface ActivitySearch { isLiked?: boolean; } -const withUser = (eb: ExpressionBuilder) => { - return jsonObjectFrom( - eb - .selectFrom('users') - .select(columns.user) - .whereRef('users.id', '=', 'activity.userId') - .where('users.deletedAt', 'is', null), - ).as('user'); -}; - @Injectable() export class ActivityRepository { constructor(@InjectKysely() private db: Kysely) {} @@ -35,7 +25,16 @@ export class ActivityRepository { return this.db .selectFrom('activity') .selectAll('activity') - .select(withUser) + .innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null)) + .innerJoinLateral( + (eb) => + eb + .selectFrom(sql`(select 1)`.as('dummy')) + .select(columns.userWithPrefix) + .as('user'), + (join) => join.onTrue(), + ) + .select((eb) => eb.fn.toJson('user').as('user')) .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)) @@ -46,10 +45,22 @@ export class ActivityRepository { .execute(); } + @GenerateSql({ params: [{ albumId: DummyValue.UUID, userId: DummyValue.UUID }] }) async create(activity: Insertable) { - return this.save(activity); + return this.db + .insertInto('activity') + .values(activity) + .returningAll() + .returning((eb) => + jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'activity.userId').select(columns.user)).as( + 'user', + ), + ) + .$narrowType<{ user: NotNull }>() + .executeTakeFirstOrThrow(); } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string) { await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute(); } @@ -72,15 +83,4 @@ export class ActivityRepository { return count as number; } - - private async save(entity: Insertable) { - 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(); - } } diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index feb1074fb2..6e3c3d7083 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import { Activity } from 'src/database'; import { ActivityCreateDto, ActivityDto, @@ -13,7 +14,6 @@ import { import { AuthDto } from 'src/dtos/auth.dto'; 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: ActivityItem | undefined; + let activity: Activity | undefined; let duplicate = false; if (dto.type === ReactionType.LIKE) { diff --git a/server/src/types.ts b/server/src/types.ts index fdbcc990e7..3e74a39730 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -13,7 +13,6 @@ import { TranscodeTarget, VideoCodec, } from 'src/enum'; -import { ActivityRepository } from 'src/repositories/activity.repository'; import { SearchRepository } from 'src/repositories/search.repository'; import { SessionRepository } from 'src/repositories/session.repository'; @@ -21,14 +20,9 @@ export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial = Pick; -type IActivityRepository = RepositoryInterface; type ISearchRepository = RepositoryInterface; type ISessionRepository = RepositoryInterface; -export type ActivityItem = - | Awaited> - | Awaited>[0]; - export type SearchPlacesItem = Awaited>[0]; export type SessionItem = Awaited>[0]; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index ea8c95e375..d2a7ba6e8f 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -1,5 +1,6 @@ import { randomUUID } from 'node:crypto'; import { + Activity, ApiKey, Asset, AuthApiKey, @@ -13,7 +14,7 @@ import { } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; -import { ActivityItem, OnThisDayData } from 'src/types'; +import { OnThisDayData } from 'src/types'; export const newUuid = () => randomUUID() as string; export const newUuids = () => @@ -154,7 +155,7 @@ const assetFactory = (asset: Partial = {}) => ({ ...asset, }); -const activityFactory = (activity: Partial = {}) => { +const activityFactory = (activity: Partial = {}) => { const userId = activity.userId || newUuid(); return { id: newUuid(),