Skip to content

Commit

Permalink
Feat/network activity (#100)
Browse files Browse the repository at this point in the history
* feat: enhance user activities with network activity retrieval

- Updated user activity schema to support multiple user IDs.
- Refactored selectUserActivities to handle user IDs and cursor for pagination.
- Introduced getUserNetworkActivitiesService to fetch activities of followed users.
- Added new controller and route for retrieving network activities.
- Improved SQL query structure for better readability and maintainability.
- Updated schemas to validate network activities query parameters.

* fix: correct parameter names in getUserNetworkActivitiesService

* chore: update CHANGELOG to include user network activities feature (#100)

* refactor: update user activity service to accept multiple user IDs
  • Loading branch information
lui7henrique authored Jan 23, 2025
1 parent 93be673 commit 633fa04
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 130 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## [Unreleased]

- Add user network activities [(#100)](https://github.com/plotwist-app/plotwist-backend/pull/100)

## 1.10.4

### Fixed
Expand Down
268 changes: 153 additions & 115 deletions src/db/repositories/user-activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,141 +4,179 @@ import type {
InsertUserActivity,
SelectUserActivities,
} from '@/domain/entities/user-activity'
import { and, desc, eq, getTableColumns, lte, sql } from 'drizzle-orm'
import { and, desc, eq, getTableColumns, inArray, lte, sql } from 'drizzle-orm'
import { db } from '..'
import { schema } from '../schema'
import { alias } from 'drizzle-orm/pg-core'

export async function insertUserActivity(values: InsertUserActivity) {
return db.insert(schema.userActivities).values(values)
}

export async function selectUserActivities({
userId,
userIds,
pageSize,
cursor,
}: SelectUserActivities) {
const additionalInfoCase = buildAdditionalInfoCase()

const owner = alias(schema.users, 'owner')

return db
.select({
...getTableColumns(schema.userActivities),
additionalInfo: sql`
CASE
WHEN ${schema.userActivities.activityType} IN ('FOLLOW_USER') THEN json_build_object(
'id', ${schema.users.id},
'username', ${schema.users.username},
'avatarUrl', ${schema.users.avatarUrl}
)
WHEN ${schema.userActivities.activityType} IN ('CREATE_LIST', 'LIKE_LIST') THEN json_build_object(
'id', ${schema.lists.id},
'title', ${schema.lists.title}
)
WHEN ${schema.userActivities.activityType} IN ('LIKE_REVIEW', 'CREATE_REVIEW') THEN json_build_object(
'id', ${schema.reviews.id},
'review', ${schema.reviews.review},
'rating', ${schema.reviews.rating},
'tmdbId', ${schema.reviews.tmdbId},
'mediaType', ${schema.reviews.mediaType},
'seasonNumber', ${schema.reviews.seasonNumber},
'episodeNumber', ${schema.reviews.episodeNumber},
'author', (
SELECT json_build_object(
'id', ${schema.users.id},
'username', ${schema.users.username},
'avatarUrl', ${schema.users.avatarUrl}
)
FROM ${schema.users}
WHERE ${schema.users.id} = ${schema.reviews.userId}
)
)
WHEN ${schema.userActivities.activityType} IN ('LIKE_REPLY', 'CREATE_REPLY') THEN json_build_object(
'id', ${schema.reviewReplies.id},
'reply', ${schema.reviewReplies.reply},
'review', (
SELECT json_build_object(
'id', ${schema.reviews.id},
'review', ${schema.reviews.review},
'rating', ${schema.reviews.rating},
'tmdbId', ${schema.reviews.tmdbId},
'mediaType', ${schema.reviews.mediaType},
'seasonNumber', ${schema.reviews.seasonNumber},
'episodeNumber', ${schema.reviews.episodeNumber},
'author', json_build_object(
'id', ${schema.users.id},
'username', ${schema.users.username},
'avatarUrl', ${schema.users.avatarUrl}
)
)
FROM ${schema.reviews}
LEFT JOIN ${schema.users} ON ${schema.users.id} = ${schema.reviews.userId}
WHERE ${schema.reviews.id} = ${schema.reviewReplies.reviewId}
)
)
WHEN ${schema.userActivities.activityType} IN ('ADD_ITEM', 'DELETE_ITEM') THEN json_build_object(
'tmdbId', ${sql`(metadata::jsonb->>'tmdbId')::integer`},
'mediaType', ${sql`(metadata::jsonb->>'mediaType')`},
'listId', ${schema.lists.id},
'listTitle', ${schema.lists.title}
)
WHEN ${schema.userActivities.activityType} IN ('WATCH_EPISODE') THEN json_build_object(
'episodes', metadata
)
WHEN ${schema.userActivities.activityType} IN ('CHANGE_STATUS') THEN json_build_object(
'tmdbId', ${sql`(metadata::jsonb->>'tmdbId')::integer`},
'mediaType', ${sql`(metadata::jsonb->>'mediaType')`},
'status', ${sql`(metadata::jsonb->>'status')`}
)
ELSE NULL
END
`.as('additionalInfo'),
additionalInfo: additionalInfoCase,
owner: {
id: owner.id,
username: owner.username,
avatarUrl: owner.avatarUrl,
},
})
.from(schema.userActivities)
.where(
and(
eq(schema.userActivities.userId, userId),
cursor
? lte(
sql`DATE_TRUNC('milliseconds', ${schema.userActivities.createdAt})`,
cursor
)
: undefined
)
)
.where(buildWhereClause(userIds, cursor))
.orderBy(desc(schema.userActivities.createdAt))
.limit(pageSize + 1)
.leftJoin(
schema.users,
sql`(${schema.userActivities.activityType} = 'FOLLOW_USER' OR ${schema.userActivities.activityType} = 'UNFOLLOW_USER')
AND ${sql`(metadata::jsonb->>'followedId')`} = ${schema.users.id}::text`
)
.leftJoin(
schema.lists,
sql`(
${schema.userActivities.activityType} = 'CREATE_LIST'
OR ${schema.userActivities.activityType} = 'LIKE_LIST'
OR ${schema.userActivities.activityType} = 'ADD_ITEM'
OR ${schema.userActivities.activityType} = 'DELETE_ITEM')
AND ${schema.userActivities.entityId} = ${schema.lists.id}`
)
.leftJoin(
schema.reviews,
sql`(
${schema.userActivities.activityType} = 'LIKE_REVIEW'
OR ${schema.userActivities.activityType} = 'CREATE_REVIEW')
AND ${schema.userActivities.entityId} = ${schema.reviews.id}`
.leftJoin(schema.users, buildUsersJoinCondition())
.leftJoin(schema.lists, buildListsJoinCondition())
.leftJoin(schema.reviews, buildReviewsJoinCondition())
.leftJoin(schema.reviewReplies, buildRepliesJoinCondition())
.leftJoin(owner, eq(schema.userActivities.userId, owner.id))
}

function buildAdditionalInfoCase() {
return sql`
CASE
WHEN ${schema.userActivities.activityType} IN ('FOLLOW_USER') THEN
${buildUserInfo()}
WHEN ${schema.userActivities.activityType} IN ('CREATE_LIST', 'LIKE_LIST') THEN
${buildListInfo()}
WHEN ${schema.userActivities.activityType} IN ('LIKE_REVIEW', 'CREATE_REVIEW') THEN
${buildReviewInfo()}
WHEN ${schema.userActivities.activityType} IN ('LIKE_REPLY', 'CREATE_REPLY') THEN
${buildReplyInfo()}
WHEN ${schema.userActivities.activityType} IN ('ADD_ITEM', 'DELETE_ITEM') THEN
${buildItemInfo()}
WHEN ${schema.userActivities.activityType} IN ('WATCH_EPISODE') THEN
json_build_object('episodes', metadata)
WHEN ${schema.userActivities.activityType} IN ('CHANGE_STATUS') THEN
${buildStatusInfo()}
ELSE NULL
END
`
}

function buildUserInfo() {
return sql`json_build_object(
'id', ${schema.users.id},
'username', ${schema.users.username},
'avatarUrl', ${schema.users.avatarUrl}
)`
}

function buildListInfo() {
return sql`json_build_object(
'id', ${schema.lists.id},
'title', ${schema.lists.title}
)`
}

function buildReviewInfo() {
return sql`json_build_object(
'id', ${schema.reviews.id},
'review', ${schema.reviews.review},
'rating', ${schema.reviews.rating},
'tmdbId', ${schema.reviews.tmdbId},
'mediaType', ${schema.reviews.mediaType},
'seasonNumber', ${schema.reviews.seasonNumber},
'episodeNumber', ${schema.reviews.episodeNumber},
'author', (
SELECT ${buildUserInfo()}
FROM ${schema.users}
WHERE ${schema.users.id} = ${schema.reviews.userId}
)
.leftJoin(
schema.reviewReplies,
sql`(
${schema.userActivities.activityType} = 'LIKE_REPLY'
OR ${schema.userActivities.activityType} = 'CREATE_REPLY')
AND ${schema.userActivities.entityId} = ${schema.reviewReplies.id}`
)`
}

function buildReplyInfo() {
return sql`json_build_object(
'id', ${schema.reviewReplies.id},
'reply', ${schema.reviewReplies.reply},
'review', (
SELECT json_build_object(
'id', ${schema.reviews.id},
'review', ${schema.reviews.review},
'rating', ${schema.reviews.rating},
'tmdbId', ${schema.reviews.tmdbId},
'mediaType', ${schema.reviews.mediaType},
'seasonNumber', ${schema.reviews.seasonNumber},
'episodeNumber', ${schema.reviews.episodeNumber},
'author', ${buildUserInfo()}
)
FROM ${schema.reviews}
LEFT JOIN ${schema.users} ON ${schema.users.id} = ${schema.reviews.userId}
WHERE ${schema.reviews.id} = ${schema.reviewReplies.reviewId}
)
)`
}

function buildItemInfo() {
return sql`json_build_object(
'tmdbId', ${sql`(metadata::jsonb->>'tmdbId')::integer`},
'mediaType', ${sql`(metadata::jsonb->>'mediaType')`},
'listId', ${schema.lists.id},
'listTitle', ${schema.lists.title}
)`
}

function buildStatusInfo() {
return sql`json_build_object(
'tmdbId', ${sql`(metadata::jsonb->>'tmdbId')::integer`},
'mediaType', ${sql`(metadata::jsonb->>'mediaType')`},
'status', ${sql`(metadata::jsonb->>'status')`}
)`
}

function buildWhereClause(
userIds: string[] | undefined,
cursor: string | undefined
) {
return and(
userIds ? inArray(schema.userActivities.userId, userIds) : undefined,
cursor
? lte(
sql`DATE_TRUNC('milliseconds', ${schema.userActivities.createdAt})`,
cursor
)
: undefined
)
}

function buildUsersJoinCondition() {
return sql`(${schema.userActivities.activityType} = 'FOLLOW_USER' OR ${schema.userActivities.activityType} = 'UNFOLLOW_USER')
AND ${sql`(metadata::jsonb->>'followedId')`} = ${schema.users.id}::text`
}

function buildListsJoinCondition() {
return sql`(
${schema.userActivities.activityType} = 'CREATE_LIST'
OR ${schema.userActivities.activityType} = 'LIKE_LIST'
OR ${schema.userActivities.activityType} = 'ADD_ITEM'
OR ${schema.userActivities.activityType} = 'DELETE_ITEM')
AND ${schema.userActivities.entityId} = ${schema.lists.id}`
}

function buildReviewsJoinCondition() {
return sql`(
${schema.userActivities.activityType} = 'LIKE_REVIEW'
OR ${schema.userActivities.activityType} = 'CREATE_REVIEW')
AND ${schema.userActivities.entityId} = ${schema.reviews.id}`
}

function buildRepliesJoinCondition() {
return sql`(
${schema.userActivities.activityType} = 'LIKE_REPLY'
OR ${schema.userActivities.activityType} = 'CREATE_REPLY')
AND ${schema.userActivities.entityId} = ${schema.reviewReplies.id}`
}

export async function deleteUserActivity({
Expand Down
5 changes: 3 additions & 2 deletions src/domain/entities/user-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type NonNullableRequired<T> = {
[K in keyof T]-?: NonNullable<T[K]>
}

export type UserActvity = InferSelectModel<typeof schema.userActivities>
export type UserActivity = InferSelectModel<typeof schema.userActivities>
export type InsertUserActivity = InferInsertModel<typeof schema.userActivities>
export type DeleteUserActivity = NonNullableRequired<
Pick<
Expand All @@ -20,7 +20,8 @@ export type DeleteFollowUserActivity = {
userId: string
}

export type SelectUserActivities = Pick<UserActvity, 'userId'> & {
export type SelectUserActivities = {
pageSize: number
cursor?: string
userIds?: string[]
}
10 changes: 5 additions & 5 deletions src/domain/services/user-activities/delete-user-activity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ describe('delete user activity', () => {

const { userActivities } = await getUserActivitiesService({
pageSize: 20,
userId: user.id,
userIds: [user.id],
})

await deleteUserActivityByIdService(userActivities[0].id)

const sut = await getUserActivitiesService({
pageSize: 20,
userId: user.id,
userIds: [user.id],
})

expect(sut.userActivities).toHaveLength(0)
Expand All @@ -51,7 +51,7 @@ describe('delete user activity by entity', () => {

const { userActivities } = await getUserActivitiesService({
pageSize: 20,
userId: user.id,
userIds: [user.id],
})

await deleteUserActivityByEntityService({
Expand All @@ -63,7 +63,7 @@ describe('delete user activity by entity', () => {

const sut = await getUserActivitiesService({
pageSize: 20,
userId: user.id,
userIds: [user.id],
})

expect(sut.userActivities).toHaveLength(0)
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('delete follow user activity', () => {

const sut = await getUserActivitiesService({
pageSize: 20,
userId: user.id,
userIds: [user.id],
})

expect(sut.userActivities).toHaveLength(0)
Expand Down
Loading

0 comments on commit 633fa04

Please # to comment.