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[];
};
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',

View File

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

View File

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

View File

@ -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<DB, 'activity'>) => {
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<DB>) {}
@ -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<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) {
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<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 { 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) {

View File

@ -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> = T extends object ? { [K in keyof T]?: DeepPartial<T
export type RepositoryInterface<T extends object> = Pick<T, keyof T>;
type IActivityRepository = RepositoryInterface<ActivityRepository>;
type ISearchRepository = RepositoryInterface<SearchRepository>;
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 SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0];

View File

@ -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> = {}) => ({
...asset,
});
const activityFactory = (activity: Partial<ActivityItem> = {}) => {
const activityFactory = (activity: Partial<Activity> = {}) => {
const userId = activity.userId || newUuid();
return {
id: newUuid(),