From 80846d60ee5499f9f6a9d0827eafbf037c0b7356 Mon Sep 17 00:00:00 2001 From: Stella01-code Date: Wed, 30 Sep 2026 00:48:10 +0000 Subject: [PATCH] feat(streams): add created-date range filter --- src/routes/streams.ts | 21 ++++++++++++++- test/routes/streams.test.ts | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/routes/streams.ts b/src/routes/streams.ts index bf87ac4..9bd42d3 100644 --- a/src/routes/streams.ts +++ b/src/routes/streams.ts @@ -16,6 +16,16 @@ const querySchema = z.object({ .regex(/^G[A-Z2-7]{55}$/) .optional(), status: z.enum(['ACTIVE', 'CANCELLED']).optional(), + createdAfter: z + .string() + .datetime({ offset: true }) + .transform((value) => new Date(value)) + .optional(), + createdBefore: z + .string() + .datetime({ offset: true }) + .transform((value) => new Date(value)) + .optional(), limit: z.coerce.number().int().min(1).max(100).default(100), // A stream id from a previous page's last item; results start right after it. cursor: z.string().uuid().optional(), @@ -52,7 +62,8 @@ export async function streamRoutes(app: FastifyInstance): Promise { // their own); `ngo` filters by the NGO's internal id, matching what // GET /ngos and /ngos/:id expose; `ngoAddress` filters by the NGO's // Stellar wallet address for clients that only have the on-chain key. - const { donor, ngo, ngoAddress, status, limit, cursor } = parsedQuery.data; + const { donor, ngo, ngoAddress, status, createdAfter, createdBefore, limit, cursor } = + parsedQuery.data; const rows = await prisma.stream.findMany({ where: { @@ -60,6 +71,14 @@ export async function streamRoutes(app: FastifyInstance): Promise { ...(ngo ? { ngoId: ngo } : {}), ...(ngoAddress ? { ngo: { ownerAddress: ngoAddress } } : {}), ...(status ? { status } : {}), + ...(createdAfter || createdBefore + ? { + createdAt: { + ...(createdAfter ? { gte: createdAfter } : {}), + ...(createdBefore ? { lte: createdBefore } : {}), + }, + } + : {}), }, orderBy: { createdAt: 'desc' }, take: limit + 1, diff --git a/test/routes/streams.test.ts b/test/routes/streams.test.ts index 9a66a95..60bc4fe 100644 --- a/test/routes/streams.test.ts +++ b/test/routes/streams.test.ts @@ -75,6 +75,59 @@ describe('GET /streams', () => { await app.close(); }); + it('filters by created-date range combined with donor', async () => { + const app = buildServer(); + + const donor = await prisma.donor.create({ data: { address: fakeAddress('A') } }); + const otherDonor = await prisma.donor.create({ data: { address: fakeAddress('B') } }); + const ngo = await prisma.ngo.create({ + data: { ownerAddress: fakeAddress('C'), name: 'NGO', verified: true }, + }); + const createdAfter = new Date('2025-03-01T00:00:00.000Z'); + const createdBefore = new Date('2025-03-31T23:59:59.999Z'); + + for (const stream of [ + { onChainId: 1n, donorId: donor.id, createdAt: new Date('2025-02-28T23:59:59.999Z') }, + { onChainId: 2n, donorId: donor.id, createdAt: createdAfter }, + { onChainId: 3n, donorId: donor.id, createdAt: new Date('2025-04-01T00:00:00.000Z') }, + { onChainId: 4n, donorId: otherDonor.id, createdAt: new Date('2025-03-15T12:00:00.000Z') }, + ]) { + await prisma.stream.create({ + data: { + ...stream, + ngoId: ngo.id, + tokenAddress: fakeAddress('D'), + rate: '1', + balance: '100', + withdrawn: '0', + }, + }); + } + + const response = await app.inject({ + method: 'GET', + url: `/streams?donor=${donor.address}&createdAfter=${createdAfter.toISOString()}&createdBefore=${createdBefore.toISOString()}`, + }); + expect(response.statusCode).toBe(200); + + const body = response.json(); + expect(body.streams.map((stream: { onChainId: string }) => stream.onChainId)).toEqual(['2']); + + await app.close(); + }); + + it('400s on a malformed created-date bound', async () => { + const app = buildServer(); + + const response = await app.inject({ + method: 'GET', + url: '/streams?createdAfter=not-a-date', + }); + expect(response.statusCode).toBe(400); + + await app.close(); + }); + it('pages through results with a filter applied', async () => { const app = buildServer();