Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/routes/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -52,14 +62,23 @@ export async function streamRoutes(app: FastifyInstance): Promise<void> {
// 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: {
...(donor ? { donor: { address: donor } } : {}),
...(ngo ? { ngoId: ngo } : {}),
...(ngoAddress ? { ngo: { ownerAddress: ngoAddress } } : {}),
...(status ? { status } : {}),
...(createdAfter || createdBefore
? {
createdAt: {
...(createdAfter ? { gte: createdAfter } : {}),
...(createdBefore ? { lte: createdBefore } : {}),
},
}
: {}),
},
orderBy: { createdAt: 'desc' },
take: limit + 1,
Expand Down
53 changes: 53 additions & 0 deletions test/routes/streams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading