refactor: activity item (#17470)

* refactor: activity item

* fix query

* qualified columns

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
This commit is contained in:
Jason Rasmussen 2025-04-09 08:35:20 -04:00 committed by GitHub
parent ae8af84101
commit cf2c0260a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 81 additions and 44 deletions

View File

@ -30,6 +30,19 @@ export type AuthApiKey = {
permissions: Permission[]; 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 = { export type ApiKey = {
id: string; id: string;
name: string; name: string;
@ -173,6 +186,7 @@ export const columns = {
'shared_links.password', 'shared_links.password',
], ],
user: userColumns, user: userColumns,
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'],
userAdmin: [ userAdmin: [
...userColumns, ...userColumns,
'createdAt', 'createdAt',

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { Activity } from 'src/database';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { ActivityItem } from 'src/types';
import { Optional, ValidateUUID } from 'src/validation'; import { Optional, ValidateUUID } from 'src/validation';
export enum ReactionType { export enum ReactionType {
@ -68,7 +68,7 @@ export class ActivityCreateDto extends ActivityDto {
comment?: string; comment?: string;
} }
export const mapActivity = (activity: ActivityItem): ActivityResponseDto => { export const mapActivity = (activity: Activity): ActivityResponseDto => {
return { return {
id: activity.id, id: activity.id,
assetId: activity.assetId, assetId: activity.assetId,

View File

@ -3,6 +3,38 @@
-- ActivityRepository.search -- ActivityRepository.search
select select
"activity".*, "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 select
to_json(obj) to_json(obj)
@ -18,17 +50,13 @@ select
"users" "users"
where where
"users"."id" = "activity"."userId" "users"."id" = "activity"."userId"
and "users"."deletedAt" is null
) as obj ) as obj
) as "user" ) as "user"
from
"activity" -- ActivityRepository.delete
left join "assets" on "assets"."id" = "activity"."assetId" delete from "activity"
and "assets"."deletedAt" is null
where where
"activity"."albumId" = $1 "id" = $1::uuid
order by
"activity"."createdAt" asc
-- ActivityRepository.getStatistics -- ActivityRepository.getStatistics
select select

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; 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 { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database'; import { columns } from 'src/database';
@ -14,16 +14,6 @@ export interface ActivitySearch {
isLiked?: boolean; isLiked?: boolean;
} }
const withUser = (eb: ExpressionBuilder<DB, 'activity'>) => {
return jsonObjectFrom(
eb
.selectFrom('users')
.select(columns.user)
.whereRef('users.id', '=', 'activity.userId')
.where('users.deletedAt', 'is', null),
).as('user');
};
@Injectable() @Injectable()
export class ActivityRepository { export class ActivityRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@ -35,7 +25,16 @@ export class ActivityRepository {
return this.db return this.db
.selectFrom('activity') .selectFrom('activity')
.selectAll('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)) .leftJoin('assets', (join) => join.onRef('assets.id', '=', 'activity.assetId').on('assets.deletedAt', 'is', null))
.$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!)) .$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!))
.$if(assetId === null, (qb) => qb.where('assetId', 'is', null)) .$if(assetId === null, (qb) => qb.where('assetId', 'is', null))
@ -46,10 +45,22 @@ export class ActivityRepository {
.execute(); .execute();
} }
@GenerateSql({ params: [{ albumId: DummyValue.UUID, userId: DummyValue.UUID }] })
async create(activity: Insertable<Activity>) { async create(activity: Insertable<Activity>) {
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) { async delete(id: string) {
await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute(); await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute();
} }
@ -72,15 +83,4 @@ export class ActivityRepository {
return count as number; return count as number;
} }
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();
}
} }

View File

@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Activity } from 'src/database';
import { import {
ActivityCreateDto, ActivityCreateDto,
ActivityDto, ActivityDto,
@ -13,7 +14,6 @@ import {
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { ActivityItem } from 'src/types';
@Injectable() @Injectable()
export class ActivityService extends BaseService { export class ActivityService extends BaseService {
@ -43,7 +43,7 @@ export class ActivityService extends BaseService {
albumId: dto.albumId, albumId: dto.albumId,
}; };
let activity: ActivityItem | undefined; let activity: Activity | undefined;
let duplicate = false; let duplicate = false;
if (dto.type === ReactionType.LIKE) { if (dto.type === ReactionType.LIKE) {

View File

@ -13,7 +13,6 @@ import {
TranscodeTarget, TranscodeTarget,
VideoCodec, VideoCodec,
} from 'src/enum'; } from 'src/enum';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { SearchRepository } from 'src/repositories/search.repository'; import { SearchRepository } from 'src/repositories/search.repository';
import { SessionRepository } from 'src/repositories/session.repository'; import { SessionRepository } from 'src/repositories/session.repository';
@ -21,14 +20,9 @@ export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T
export type RepositoryInterface<T extends object> = Pick<T, keyof T>; export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
type IActivityRepository = RepositoryInterface<ActivityRepository>;
type ISearchRepository = RepositoryInterface<SearchRepository>; type ISearchRepository = RepositoryInterface<SearchRepository>;
type ISessionRepository = RepositoryInterface<SessionRepository>; type ISessionRepository = RepositoryInterface<SessionRepository>;
export type ActivityItem =
| Awaited<ReturnType<IActivityRepository['create']>>
| Awaited<ReturnType<IActivityRepository['search']>>[0];
export type SearchPlacesItem = Awaited<ReturnType<ISearchRepository['searchPlaces']>>[0]; export type SearchPlacesItem = Awaited<ReturnType<ISearchRepository['searchPlaces']>>[0];
export type SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0]; export type SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0];

View File

@ -1,5 +1,6 @@
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { import {
Activity,
ApiKey, ApiKey,
Asset, Asset,
AuthApiKey, AuthApiKey,
@ -13,7 +14,7 @@ import {
} from 'src/database'; } from 'src/database';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum'; 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 newUuid = () => randomUUID() as string;
export const newUuids = () => export const newUuids = () =>
@ -154,7 +155,7 @@ const assetFactory = (asset: Partial<Asset> = {}) => ({
...asset, ...asset,
}); });
const activityFactory = (activity: Partial<ActivityItem> = {}) => { const activityFactory = (activity: Partial<Activity> = {}) => {
const userId = activity.userId || newUuid(); const userId = activity.userId || newUuid();
return { return {
id: newUuid(), id: newUuid(),