Skip to content

Commit d29a55f

Browse files
authored
Merge pull request #436 from iheomadev/feature/340-transactions-pagination
feat(transactions): add pagination metadata response (#340)
2 parents c7f69e1 + 06f8900 commit d29a55f

4 files changed

Lines changed: 94 additions & 29 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Transaction } from '../entities/transaction.entity';
2+
3+
export class PaginatedTransactionsDto {
4+
data: Transaction[];
5+
total: number;
6+
limit: number;
7+
offset: number;
8+
hasMore: boolean;
9+
}

src/transactions/transactions.controller.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,16 @@ describe('TransactionsController', () => {
8585

8686
it('should return the result from the service', async () => {
8787
const tx = { id: 'tx-1', status: TransactionStatus.PENDING };
88-
mockTransactionsService.findByWallet.mockResolvedValue([tx]);
88+
const paginated = { data: [tx], total: 1, limit: 20, offset: 0, hasMore: false };
89+
mockTransactionsService.findByWallet.mockResolvedValue(paginated);
8990

9091
const result = await controller.findByWallet(
9192
'wallet-1',
9293
undefined,
9394
undefined,
9495
);
9596

96-
expect(result).toEqual([tx]);
97+
expect(result).toEqual(paginated);
9798
});
9899
});
99100

src/transactions/transactions.service.spec.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const mockPrisma = {
4444
create: jest.fn(),
4545
findUnique: jest.fn(),
4646
findMany: jest.fn(),
47+
count: jest.fn(),
4748
update: jest.fn(),
4849
},
4950
};
@@ -171,30 +172,59 @@ describe('TransactionsService', () => {
171172
});
172173

173174
describe('findAll', () => {
174-
it('returns all transactions without filters', async () => {
175+
it('returns paginated transactions without filters', async () => {
175176
const txs = [makePrismaTransaction()];
176177
mockPrisma.transaction.findMany.mockResolvedValue(txs);
178+
mockPrisma.transaction.count.mockResolvedValue(1);
177179

178180
const result = await service.findAll();
179181

180182
expect(mockPrisma.transaction.findMany).toHaveBeenCalledWith({
181183
where: {},
182184
orderBy: { createdAt: 'desc' },
183-
take: undefined,
184-
skip: undefined,
185+
take: 20,
186+
skip: 0,
185187
});
186-
expect(result).toHaveLength(1);
188+
expect(result.data).toHaveLength(1);
189+
expect(result.total).toBe(1);
190+
expect(result.limit).toBe(20);
191+
expect(result.offset).toBe(0);
192+
expect(result.hasMore).toBe(false);
193+
});
194+
195+
it('uses provided limit and offset', async () => {
196+
mockPrisma.transaction.findMany.mockResolvedValue([]);
197+
mockPrisma.transaction.count.mockResolvedValue(10);
198+
199+
const result = await service.findAll({ limit: 5, offset: 5 });
200+
201+
expect(result.limit).toBe(5);
202+
expect(result.offset).toBe(5);
203+
expect(result.hasMore).toBe(false);
204+
});
205+
206+
it('sets hasMore=true when more results exist', async () => {
207+
const txs = [makePrismaTransaction()];
208+
mockPrisma.transaction.findMany.mockResolvedValue(txs);
209+
mockPrisma.transaction.count.mockResolvedValue(5);
210+
211+
const result = await service.findAll({ limit: 1, offset: 0 });
212+
213+
expect(result.hasMore).toBe(true);
187214
});
188215
});
189216

190217
describe('findByWallet', () => {
191-
it('returns transactions for a valid wallet', async () => {
218+
it('returns paginated transactions for a valid wallet', async () => {
192219
mockPrisma.wallet.findUnique.mockResolvedValue({ id: 'wallet-1' });
193220
mockPrisma.transaction.findMany.mockResolvedValue([makePrismaTransaction()]);
221+
mockPrisma.transaction.count.mockResolvedValue(1);
194222

195223
const result = await service.findByWallet('wallet-1');
196224

197-
expect(result).toHaveLength(1);
225+
expect(result.data).toHaveLength(1);
226+
expect(result.total).toBe(1);
227+
expect(result.hasMore).toBe(false);
198228
});
199229

200230
it('throws NotFoundException when wallet does not exist', async () => {

src/transactions/transactions.service.ts

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
Logger,
44
NotFoundException,
55
BadRequestException,
6-
Optional,
76
} from '@nestjs/common';
87
import { PrismaService } from '../prisma/prisma.service';
98
import { BalanceIndexerService } from '../balance-indexer/balance-indexer.service';
@@ -20,6 +19,7 @@ import {
2019
StellarNetworkReferences,
2120
} from './domain/transaction.model';
2221
import { Transaction as TransactionEntity } from './entities/transaction.entity';
22+
import { PaginatedTransactionsDto } from './dto/paginated-transactions.dto';
2323
import { InsufficientBalanceException } from './domain/insufficient-balance.exception';
2424
import { WebhookEventEmitterService } from '../webhooks/webhook-event-emitter.service';
2525
import { CacheService } from '../common/cache/cache.service';
@@ -140,15 +140,15 @@ export class TransactionsService {
140140
}
141141

142142
/**
143-
* Find all transactions with optional filters
143+
* Find all transactions with optional filters, returns paginated response
144144
*/
145145
async findAll(filters?: {
146146
senderWalletId?: string;
147147
receiverWalletId?: string;
148148
status?: TransactionStatus;
149149
limit?: number;
150150
offset?: number;
151-
}): Promise<TransactionEntity[]> {
151+
}): Promise<PaginatedTransactionsDto> {
152152
const where: any = {};
153153

154154
if (filters?.senderWalletId) {
@@ -163,14 +163,26 @@ export class TransactionsService {
163163
where.status = filters.status;
164164
}
165165

166-
const transactions = await this.prisma.transaction.findMany({
167-
where,
168-
orderBy: { createdAt: 'desc' },
169-
take: filters?.limit,
170-
skip: filters?.offset,
171-
});
166+
const limit = filters?.limit ?? 20;
167+
const offset = filters?.offset ?? 0;
168+
169+
const [transactions, total] = await Promise.all([
170+
this.prisma.transaction.findMany({
171+
where,
172+
orderBy: { createdAt: 'desc' },
173+
take: limit,
174+
skip: offset,
175+
}),
176+
this.prisma.transaction.count({ where }),
177+
]);
172178

173-
return transactions.map((t) => this.mapPrismaToEntity(t));
179+
return {
180+
data: transactions.map((t) => this.mapPrismaToEntity(t)),
181+
total,
182+
limit,
183+
offset,
184+
hasMore: offset + transactions.length < total,
185+
};
174186
}
175187

176188
/**
@@ -294,12 +306,12 @@ export class TransactionsService {
294306
}
295307

296308
/**
297-
* Find transactions by wallet ID with pagination
309+
* Find transactions by wallet ID with pagination metadata
298310
*/
299311
async findByWallet(
300312
walletId: string,
301313
pagination?: { limit?: number; offset?: number },
302-
): Promise<TransactionEntity[]> {
314+
): Promise<PaginatedTransactionsDto> {
303315
const wallet = await this.prisma.wallet.findUnique({
304316
where: { id: walletId },
305317
});
@@ -308,16 +320,29 @@ export class TransactionsService {
308320
throw new NotFoundException(`Wallet ${walletId} not found`);
309321
}
310322

311-
const transactions = await this.prisma.transaction.findMany({
312-
where: {
313-
OR: [{ senderWalletId: walletId }, { receiverWalletId: walletId }],
314-
},
315-
orderBy: { createdAt: 'desc' },
316-
take: pagination?.limit,
317-
skip: pagination?.offset,
318-
});
323+
const limit = pagination?.limit ?? 20;
324+
const offset = pagination?.offset ?? 0;
325+
const where = {
326+
OR: [{ senderWalletId: walletId }, { receiverWalletId: walletId }],
327+
};
328+
329+
const [transactions, total] = await Promise.all([
330+
this.prisma.transaction.findMany({
331+
where,
332+
orderBy: { createdAt: 'desc' },
333+
take: limit,
334+
skip: offset,
335+
}),
336+
this.prisma.transaction.count({ where }),
337+
]);
319338

320-
return transactions.map((t) => this.mapPrismaToEntity(t));
339+
return {
340+
data: transactions.map((t) => this.mapPrismaToEntity(t)),
341+
total,
342+
limit,
343+
offset,
344+
hasMore: offset + transactions.length < total,
345+
};
321346
}
322347

323348
/**

0 commit comments

Comments
 (0)