diff --git a/src/adapters/db.ts b/src/adapters/db.ts index dadd950..ae86f81 100644 --- a/src/adapters/db.ts +++ b/src/adapters/db.ts @@ -1,7 +1,15 @@ import SQL, { SQLStatement } from 'sql-template-strings' import { randomUUID } from 'node:crypto' import { PoolClient } from 'pg' -import { AppComponents, Friendship, FriendshipAction, FriendshipRequest, IDatabaseComponent, Friend } from '../types' +import { + AppComponents, + Friendship, + FriendshipAction, + FriendshipRequest, + IDatabaseComponent, + Friend, + Pagination +} from '../types' import { FRIENDSHIPS_PER_PAGE } from './rpc-server/constants' import { normalizeAddress } from '../utils/address' @@ -24,7 +32,13 @@ export function createDBComponent(components: Pick WHERE (LOWER(address_requester) = ${normalizedUserAddress} OR LOWER(address_requested) = ${normalizedUserAddress})` } - function getFriendshipRequestBaseQuery(userAddress: string, type: 'sent' | 'received'): SQLStatement { + function getFriendshipRequestBaseQuery( + userAddress: string, + type: 'sent' | 'received', + { onlyCount, pagination }: { onlyCount?: boolean; pagination?: Pagination } = { onlyCount: false } + ): SQLStatement { + const { limit, offset } = pagination || {} + const columnMapping = { sent: SQL` f.address_requested`, received: SQL` f.address_requester` @@ -34,9 +48,16 @@ export function createDBComponent(components: Pick received: SQL` LOWER(f.address_requested)` } - const baseQuery = SQL`SELECT fa.id,` - baseQuery.append(columnMapping[type]) - baseQuery.append(SQL` as address, fa.timestamp, fa.metadata`) + const baseQuery = SQL`SELECT` + + if (onlyCount) { + baseQuery.append(SQL` DISTINCT COUNT(1) as count`) + } else { + baseQuery.append(SQL` fa.id,`) + baseQuery.append(columnMapping[type]) + baseQuery.append(SQL` as address, fa.timestamp, fa.metadata`) + } + baseQuery.append(SQL` FROM friendships f`) baseQuery.append(SQL` INNER JOIN friendship_actions fa ON f.id = fa.friendship_id`) baseQuery.append(SQL` WHERE`) @@ -55,6 +76,14 @@ export function createDBComponent(components: Pick ORDER BY fa.timestamp DESC `) + if (limit) { + baseQuery.append(SQL` LIMIT ${limit}`) + } + + if (offset) { + baseQuery.append(SQL` OFFSET ${offset}`) + } + return baseQuery } @@ -274,38 +303,25 @@ export function createDBComponent(components: Pick return uuid }, async getReceivedFriendshipRequests(userAddress, pagination) { - const { limit, offset } = pagination || {} - - const query = getFriendshipRequestBaseQuery(userAddress, 'received') - - if (limit) { - query.append(SQL` LIMIT ${limit}`) - } - - if (offset) { - query.append(SQL` OFFSET ${offset}`) - } - + const query = getFriendshipRequestBaseQuery(userAddress, 'received', { pagination }) const results = await pg.query(query) - return results.rows }, + async getReceivedFriendshipRequestsCount(userAddress) { + const query = getFriendshipRequestBaseQuery(userAddress, 'received', { onlyCount: true }) + const results = await pg.query<{ count: number }>(query) + return results.rows[0].count + }, async getSentFriendshipRequests(userAddress, pagination) { - const { limit, offset } = pagination || {} - const query = getFriendshipRequestBaseQuery(userAddress, 'sent') - - if (limit) { - query.append(SQL` LIMIT ${limit}`) - } - - if (offset) { - query.append(SQL` OFFSET ${offset}`) - } - + const query = getFriendshipRequestBaseQuery(userAddress, 'sent', { pagination }) const results = await pg.query(query) - return results.rows }, + async getSentFriendshipRequestsCount(userAddress) { + const query = getFriendshipRequestBaseQuery(userAddress, 'sent', { onlyCount: true }) + const results = await pg.query<{ count: number }>(query) + return results.rows[0].count + }, async getOnlineFriends(userAddress: string, onlinePotentialFriends: string[]) { if (onlinePotentialFriends.length === 0) return [] diff --git a/src/adapters/rpc-server/services/get-pending-friendship-requests.ts b/src/adapters/rpc-server/services/get-pending-friendship-requests.ts index 78c3223..5d99737 100644 --- a/src/adapters/rpc-server/services/get-pending-friendship-requests.ts +++ b/src/adapters/rpc-server/services/get-pending-friendship-requests.ts @@ -4,6 +4,7 @@ import { PaginatedFriendshipRequestsResponse, GetFriendshipRequestsPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { getPage } from '../../../utils/pagination' export async function getPendingFriendshipRequestsService({ components: { logs, db, catalystClient, config } @@ -16,10 +17,14 @@ export async function getPendingFriendshipRequestsService({ context: RpcServerContext ): Promise { try { - const pendingRequests = await db.getReceivedFriendshipRequests(context.address, request.pagination) + const { limit, offset } = request.pagination || {} + const [pendingRequests, pendingRequestsCount] = await Promise.all([ + db.getReceivedFriendshipRequests(context.address, request.pagination), + db.getReceivedFriendshipRequestsCount(context.address) + ]) const pendingRequestsAddresses = pendingRequests.map(({ address }) => address) - const pendingRequesterProfiles = await catalystClient.getEntitiesByPointers(pendingRequestsAddresses) + const requests = parseFriendshipRequestsToFriendshipRequestResponses( pendingRequests, pendingRequesterProfiles, @@ -32,6 +37,10 @@ export async function getPendingFriendshipRequestsService({ requests: { requests } + }, + paginationData: { + total: pendingRequestsCount, + page: getPage(limit ?? pendingRequestsCount, offset) } } } catch (error: any) { diff --git a/src/adapters/rpc-server/services/get-sent-friendship-requests.ts b/src/adapters/rpc-server/services/get-sent-friendship-requests.ts index e3f7942..207acf0 100644 --- a/src/adapters/rpc-server/services/get-sent-friendship-requests.ts +++ b/src/adapters/rpc-server/services/get-sent-friendship-requests.ts @@ -4,6 +4,7 @@ import { PaginatedFriendshipRequestsResponse, GetFriendshipRequestsPayload } from '@dcl/protocol/out-js/decentraland/social_service/v2/social_service_v2.gen' +import { getPage } from '../../../utils/pagination' export async function getSentFriendshipRequestsService({ components: { logs, db, catalystClient, config } @@ -16,9 +17,13 @@ export async function getSentFriendshipRequestsService({ context: RpcServerContext ): Promise { try { - const sentRequests = await db.getSentFriendshipRequests(context.address, request.pagination) - const sentRequestsAddresses = sentRequests.map(({ address }) => address) + const { limit, offset } = request.pagination || {} + const [sentRequests, sentRequestsCount] = await Promise.all([ + db.getSentFriendshipRequests(context.address, request.pagination), + db.getSentFriendshipRequestsCount(context.address) + ]) + const sentRequestsAddresses = sentRequests.map(({ address }) => address) const sentRequestedProfiles = await catalystClient.getEntitiesByPointers(sentRequestsAddresses) const requests = parseFriendshipRequestsToFriendshipRequestResponses( @@ -33,6 +38,10 @@ export async function getSentFriendshipRequestsService({ requests: { requests } + }, + paginationData: { + total: sentRequestsCount, + page: getPage(limit ?? sentRequestsCount, offset) } } } catch (error: any) { diff --git a/src/types.ts b/src/types.ts index 74a48c9..020fe15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,7 +98,9 @@ export interface IDatabaseComponent { txClient?: PoolClient ): Promise getReceivedFriendshipRequests(userAddress: string, pagination?: Pagination): Promise + getReceivedFriendshipRequestsCount(userAddress: string): Promise getSentFriendshipRequests(userAddress: string, pagination?: Pagination): Promise + getSentFriendshipRequestsCount(userAddress: string): Promise getOnlineFriends(userAddress: string, potentialFriends: string[]): Promise executeTx(cb: (client: PoolClient) => Promise): Promise } diff --git a/test/mocks/components/db.ts b/test/mocks/components/db.ts index 10abe83..86f7c00 100644 --- a/test/mocks/components/db.ts +++ b/test/mocks/components/db.ts @@ -13,6 +13,8 @@ export const mockDb: jest.Mocked = { getLastFriendshipActionByUsers: jest.fn(), recordFriendshipAction: jest.fn(), getReceivedFriendshipRequests: jest.fn(), + getReceivedFriendshipRequestsCount: jest.fn(), getSentFriendshipRequests: jest.fn(), + getSentFriendshipRequestsCount: jest.fn(), executeTx: jest.fn() } diff --git a/test/unit/adapters/db.spec.ts b/test/unit/adapters/db.spec.ts index 928afd5..9f0847b 100644 --- a/test/unit/adapters/db.spec.ts +++ b/test/unit/adapters/db.spec.ts @@ -285,6 +285,17 @@ describe('db', () => { }) }) + describe('getReceivedFriendshipRequestsCount', () => { + it('should return the count of received friendship requests', async () => { + const mockCount = 5 + mockPg.query.mockResolvedValueOnce({ rows: [{ count: mockCount }], rowCount: 1 }) + + const result = await dbComponent.getReceivedFriendshipRequestsCount('0x456') + + expect(result).toBe(mockCount) + }) + }) + describe('getSentFriendshipRequests', () => { it('should retrieve sent friendship requests', async () => { const mockRequests = [ @@ -317,6 +328,17 @@ describe('db', () => { }) }) + describe('getSentFriendshipRequestsCount', () => { + it('should return the count of sent friendship requests', async () => { + const mockCount = 5 + mockPg.query.mockResolvedValueOnce({ rows: [{ count: mockCount }], rowCount: 1 }) + + const result = await dbComponent.getSentFriendshipRequestsCount('0x123') + + expect(result).toBe(mockCount) + }) + }) + describe('recordFriendshipAction', () => { it.each([false, true])('should record a friendship action', async (withTxClient: boolean) => { const mockClient = withTxClient ? await mockPg.getPool().connect() : undefined @@ -426,8 +448,7 @@ describe('db', () => { }) // Helpers - - function expectPaginatedQueryToHaveBeenCalledWithProperLimitAndOffset(limit, offset) { + function expectPaginatedQueryToHaveBeenCalledWithProperLimitAndOffset(limit: number, offset: number) { expect(mockPg.query).toHaveBeenCalledWith( expect.objectContaining({ text: expect.stringContaining('LIMIT'), diff --git a/test/unit/adapters/rpc-server/services/get-pending-friendship-requests.spec.ts b/test/unit/adapters/rpc-server/services/get-pending-friendship-requests.spec.ts index 7793795..f3f2100 100644 --- a/test/unit/adapters/rpc-server/services/get-pending-friendship-requests.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-pending-friendship-requests.spec.ts @@ -30,6 +30,7 @@ describe('getPendingFriendshipRequestsService', () => { const mockProfiles = mockPendingRequests.map(({ address }) => createMockProfile(address)) mockDb.getReceivedFriendshipRequests.mockResolvedValueOnce(mockPendingRequests) + mockDb.getReceivedFriendshipRequestsCount.mockResolvedValueOnce(mockPendingRequests.length) mockCatalystClient.getEntitiesByPointers.mockResolvedValueOnce(mockProfiles) const result: PaginatedFriendshipRequestsResponse = await getPendingRequests(emptyRequest, rpcContext) @@ -42,12 +43,19 @@ describe('getPendingFriendshipRequestsService', () => { createMockExpectedFriendshipRequest('id2', '0x789', mockProfiles[1], '2025-01-02T00:00:00Z') ] } + }, + paginationData: { + total: mockPendingRequests.length, + page: 1 } }) }) - it('should handle database errors gracefully', async () => { - mockDb.getReceivedFriendshipRequests.mockImplementationOnce(() => { + it.each([ + ['getReceivedFriendshipRequests', mockDb.getReceivedFriendshipRequests], + ['getReceivedFriendshipRequestsCount', mockDb.getReceivedFriendshipRequestsCount] + ])('should handle database errors in the %s method gracefully', async (_methodName, method) => { + method.mockImplementationOnce(() => { throw new Error('Database error') }) @@ -66,6 +74,7 @@ describe('getPendingFriendshipRequestsService', () => { const mockProfiles = mockPendingRequests.map(({ address }) => createMockProfile(address)) mockDb.getReceivedFriendshipRequests.mockResolvedValueOnce(mockPendingRequests) + mockDb.getReceivedFriendshipRequestsCount.mockResolvedValueOnce(mockPendingRequests.length) mockCatalystClient.getEntitiesByPointers.mockResolvedValueOnce(mockProfiles) const result: PaginatedFriendshipRequestsResponse = await getPendingRequests(emptyRequest, rpcContext) @@ -76,6 +85,10 @@ describe('getPendingFriendshipRequestsService', () => { requests: { requests: [createMockExpectedFriendshipRequest('id1', '0x456', mockProfiles[0], '2025-01-01T00:00:00Z', '')] } + }, + paginationData: { + total: mockPendingRequests.length, + page: 1 } }) }) diff --git a/test/unit/adapters/rpc-server/services/get-sent-friendship-requests.spec.ts b/test/unit/adapters/rpc-server/services/get-sent-friendship-requests.spec.ts index 4951d9f..dd9ff00 100644 --- a/test/unit/adapters/rpc-server/services/get-sent-friendship-requests.spec.ts +++ b/test/unit/adapters/rpc-server/services/get-sent-friendship-requests.spec.ts @@ -30,6 +30,7 @@ describe('getSentFriendshipRequestsService', () => { const mockProfiles = mockSentRequests.map(({ address }) => createMockProfile(address)) mockDb.getSentFriendshipRequests.mockResolvedValueOnce(mockSentRequests) + mockDb.getSentFriendshipRequestsCount.mockResolvedValueOnce(mockSentRequests.length) mockCatalystClient.getEntitiesByPointers.mockResolvedValueOnce(mockProfiles) const result: PaginatedFriendshipRequestsResponse = await getSentRequests(emptyRequest, rpcContext) @@ -43,12 +44,19 @@ describe('getSentFriendshipRequestsService', () => { createMockExpectedFriendshipRequest('id2', '0x789', mockProfiles[1], '2025-01-02T00:00:00Z') ] } + }, + paginationData: { + total: mockSentRequests.length, + page: 1 } }) }) - it('should handle database errors gracefully', async () => { - mockDb.getSentFriendshipRequests.mockImplementationOnce(() => { + it.each([ + ['getSentFriendshipRequests', mockDb.getSentFriendshipRequests], + ['getSentFriendshipRequestsCount', mockDb.getSentFriendshipRequestsCount] + ])('should handle database errors in the %s method gracefully', async (_methodName, method) => { + method.mockImplementationOnce(() => { throw new Error('Database error') }) @@ -67,6 +75,7 @@ describe('getSentFriendshipRequestsService', () => { const mockProfiles = mockSentRequests.map(({ address }) => createMockProfile(address)) mockDb.getSentFriendshipRequests.mockResolvedValueOnce(mockSentRequests) + mockDb.getSentFriendshipRequestsCount.mockResolvedValueOnce(mockSentRequests.length) mockCatalystClient.getEntitiesByPointers.mockResolvedValueOnce(mockProfiles) const result: PaginatedFriendshipRequestsResponse = await getSentRequests(emptyRequest, rpcContext) @@ -77,6 +86,10 @@ describe('getSentFriendshipRequestsService', () => { requests: { requests: [createMockExpectedFriendshipRequest('id1', '0x456', mockProfiles[0], '2025-01-01T00:00:00Z')] } + }, + paginationData: { + total: mockSentRequests.length, + page: 1 } }) })