diff --git a/services/transactionService.ts b/services/transactionService.ts index 221905d2..c503a183 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -177,6 +177,57 @@ export async function fetchTransactionsByAddressList ( }) } +type TransactionOrderBy = Prisma.TransactionOrderByWithRelationInput + +// Repeated query keys arrive as arrays. Only known fields become Prisma orderBy +// objects; anything else, including arrays, sorts by timestamp. +function transactionOrderBy (orderBy: unknown, orderDesc: boolean): TransactionOrderBy { + const direction: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' + if (typeof orderBy !== 'string') { + return { timestamp: direction } + } + switch (orderBy) { + case 'id': + return { id: direction } + case 'hash': + return { hash: direction } + case 'amount': + return { amount: direction } + case 'confirmed': + return { confirmed: direction } + case 'createdAt': + return { createdAt: direction } + case 'address.address': + return { address: { address: direction } } + case 'address.networkId': + return { address: { networkId: direction } } + case 'timestamp': + default: + return { timestamp: direction } + } +} + +function paymentOrderBy (orderBy: unknown, orderDesc: boolean): TransactionOrderBy { + const direction: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' + if (typeof orderBy !== 'string') { + return { timestamp: direction } + } + switch (orderBy) { + case 'id': + return { id: direction } + case 'hash': + return { hash: direction } + case 'amount': + case 'values': + return { amount: direction } + case 'networkId': + return { address: { networkId: direction } } + case 'timestamp': + default: + return { timestamp: direction } + } +} + export async function fetchTransactionsByAddressListWithPagination ( addressIdList: string[], page: number, @@ -186,29 +237,7 @@ export async function fetchTransactionsByAddressListWithPagination ( networkIdsListFilter?: number[], includeInputs = false ): Promise { - const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' - - // Get query for orderBy that works with nested properties (e.g. `address.networkId`) - let orderByQuery - if (orderBy !== undefined && orderBy !== '') { - if (orderBy.includes('.')) { - const [relation, property] = orderBy.split('.') - orderByQuery = { - [relation]: { - [property]: orderDescString - } - } - } else { - orderByQuery = { - [orderBy]: orderDescString - } - } - } else { - // Default orderBy - orderByQuery = { - timestamp: orderDescString - } - } + const orderByQuery = transactionOrderBy(orderBy, orderDesc) // Build include conditionally - exclude inputs by default unless explicitly requested const include = includeInputs @@ -288,29 +317,7 @@ export async function * generateTransactionsWithPaybuttonsAndPricesForAddress (a } export async function fetchPaginatedAddressTransactions (addressString: string, page: number, pageSize: number, orderBy?: string, orderDesc = true): Promise { - const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' - - // Get query for orderBy that works with nested properties (e.g. `address.networkId`) - let orderByQuery - if (orderBy !== undefined && orderBy !== '') { - if (orderBy.includes('.')) { - const [relation, property] = orderBy.split('.') - orderByQuery = { - [relation]: { - [property]: orderDescString - } - } - } else { - orderByQuery = { - [orderBy]: orderDescString - } - } - } else { - // Default orderBy - orderByQuery = { - timestamp: orderDescString - } - } + const orderByQuery = transactionOrderBy(orderBy, orderDesc) const parsedAddress = parseAddress(addressString) await addressExists(parsedAddress, true) const txs = await prisma.transaction.findMany({ @@ -1161,36 +1168,12 @@ export async function fetchAllPaymentsByUserIdWithPagination ( endDate?: string, includeInputs = false ): Promise { - const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' - if (orderBy === 'buttonDisplayDataList') { return await getPaymentsByUserIdOrderedByButtonName( userId, page, pageSize, orderDesc, buttonIds ) } - // Get query for orderBy that works with nested properties (e.g. `address.networkId`) - let orderByQuery - if (orderBy !== undefined && orderBy !== '') { - if (orderBy === 'values') { - orderByQuery = { - amount: orderDescString - } - } else if (orderBy === 'networkId') { - orderByQuery = { - address: { - networkId: orderDescString - } - } - } else { - orderByQuery = { - [orderBy]: orderDescString - } - } - } else { - orderByQuery = { - timestamp: orderDescString - } - } + const orderByQuery = paymentOrderBy(orderBy, orderDesc) const where: Prisma.TransactionWhereInput = { address: { diff --git a/tests/unittests/transactionService.test.ts b/tests/unittests/transactionService.test.ts index e2e3f47a..7aac61a9 100644 --- a/tests/unittests/transactionService.test.ts +++ b/tests/unittests/transactionService.test.ts @@ -2,6 +2,7 @@ import prisma from 'prisma-local/clientInstance' import * as transactionService from 'services/transactionService' import { prismaMock } from 'prisma-local/mockedClient' import { mockedBCHAddress, mockedUSDPriceOnTransaction, mockedCADPriceOnTransaction, mockedTransaction, mockedUserProfile, mockedAddressIdList, mockedTransactionList } from '../mockedObjects' +import { exampleAddresses } from '../utils' import { CacheSet } from 'redis/index' import { Prisma } from '@prisma/client' import * as addressService from 'services/addressService' @@ -455,4 +456,71 @@ describe('Date and timezone filters for transactions', () => { expect(callArgs.where.timestamp).toBeUndefined() expect(callArgs.where.OR).toBeUndefined() }) + + it('maps payment sort fields and ignores values that are not on the blacklist', async () => { + prismaMock.transaction.findMany.mockResolvedValue([]) + prisma.transaction.findMany = prismaMock.transaction.findMany + + await transactionService.fetchAllPaymentsByUserIdWithPagination( + 'user-1', 0, 10, 'UTC', 'values', false + ) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { amount: 'asc' } + })) + + prismaMock.transaction.findMany.mockClear() + await transactionService.fetchAllPaymentsByUserIdWithPagination( + 'user-1', 0, 10, 'UTC', 'networkId', true + ) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { address: { networkId: 'desc' } } + })) + + prismaMock.transaction.findMany.mockClear() + await transactionService.fetchAllPaymentsByUserIdWithPagination( + 'user-1', 0, 10, 'UTC', '__proto__', true + ) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { timestamp: 'desc' } + })) + + prismaMock.transaction.findMany.mockClear() + await transactionService.fetchAllPaymentsByUserIdWithPagination( + 'user-1', 0, 10, 'UTC', ['amount', 'hash'] as unknown as string, true + ) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { timestamp: 'desc' } + })) + }) +}) + +describe('address transaction orderBy blacklist', () => { + const address = `ecash:${exampleAddresses.ecash}` + + beforeEach(() => { + prismaMock.address.findUniqueOrThrow.mockResolvedValue(mockedBCHAddress) + prisma.address.findUniqueOrThrow = prismaMock.address.findUniqueOrThrow + prismaMock.transaction.findMany.mockResolvedValue([]) + prisma.transaction.findMany = prismaMock.transaction.findMany + }) + + it('sorts by an allowed nested address field', async () => { + await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, 'address.networkId', false) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { address: { networkId: 'asc' } } + })) + }) + + it('falls back to timestamp when orderBy is an array or an unknown field', async () => { + await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, ['timestamp'] as unknown as string, true) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { timestamp: 'desc' } + })) + + prismaMock.transaction.findMany.mockClear() + await transactionService.fetchPaginatedAddressTransactions(address, 0, 10, 'constructor', true) + expect(prismaMock.transaction.findMany.mock.calls[0][0]).toEqual(expect.objectContaining({ + orderBy: { timestamp: 'desc' } + })) + }) })