diff --git a/backend/prisma/migrations/20260630120000_add_stream_event_stream_id_timestamp_index/migration.sql b/backend/prisma/migrations/20260630120000_add_stream_event_stream_id_timestamp_index/migration.sql new file mode 100644 index 00000000..ec2e12ec --- /dev/null +++ b/backend/prisma/migrations/20260630120000_add_stream_event_stream_id_timestamp_index/migration.sql @@ -0,0 +1,8 @@ +-- Replace the unused (streamId, createdAt) composite with (streamId, timestamp), +-- which matches streamId-scoped event listings that ORDER BY timestamp. + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "StreamEvent_streamId_timestamp_idx" ON "StreamEvent"("streamId", "timestamp"); + +-- DropIndex +DROP INDEX IF EXISTS "StreamEvent_streamId_createdAt_idx"; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index d0caba15..4ff22965 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -86,5 +86,6 @@ model StreamEvent { @@index([timestamp]) @@index([transactionHash]) @@index([createdAt]) - @@index([streamId, createdAt]) + @@index([streamId, timestamp]) + @@unique([transactionHash, eventType]) } diff --git a/backend/tests/integration/streams.test.ts b/backend/tests/integration/streams.test.ts index 0a8c82f1..c66e1e8b 100644 --- a/backend/tests/integration/streams.test.ts +++ b/backend/tests/integration/streams.test.ts @@ -231,6 +231,17 @@ describe('GET /v1/streams/:id/events — pagination and eventType filter', () => expect(res.body).toHaveProperty('hasMore'); expect(Array.isArray(res.body.data)).toBe(true); expect(res.body.hasMore).toBe(false); + + const callArgs = mockPrisma.streamEvent.findMany.mock.calls[0]![0] as { + where: { streamId: number }; + orderBy: { timestamp: string }; + take: number; + skip: number; + }; + expect(callArgs.where.streamId).toBe(1); + expect(callArgs.orderBy).toEqual({ timestamp: 'desc' }); + expect(callArgs.take).toBe(10); + expect(callArgs.skip).toBe(0); }); it('enforces default limit of 50', async () => { diff --git a/backend/tests/stream.controller.test.ts b/backend/tests/stream.controller.test.ts index 7a208ce1..0310ed67 100644 --- a/backend/tests/stream.controller.test.ts +++ b/backend/tests/stream.controller.test.ts @@ -1,24 +1,11 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - createStream, - listStreams, - getStream, - getStreamClaimableAmount, - pauseStream, - resumeStream, -} from "../src/controllers/stream.controller.js"; -import { prisma } from "../src/lib/prisma.js"; -import { claimableAmountService } from "../src/services/claimable.service.js"; -import * as sorobanService from "../src/services/sorobanService.js"; -import type { Request, Response } from "express"; - -type TestRequest = Partial & { - user?: { - publicKey: string; - }; -}; - -vi.mock("../src/lib/prisma.js", () => ({ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createStream, listStreams, getStream, getStreamEvents, getStreamClaimableAmount, pauseStream, resumeStream } from '../src/controllers/stream.controller.js'; +import { prisma } from '../src/lib/prisma.js'; +import { claimableAmountService } from '../src/services/claimable.service.js'; +import * as sorobanService from '../src/services/sorobanService.js'; +import type { Request, Response } from 'express'; + +vi.mock('../src/lib/prisma.js', () => ({ prisma: { stream: { upsert: vi.fn(), @@ -29,6 +16,8 @@ vi.mock("../src/lib/prisma.js", () => ({ }, streamEvent: { create: vi.fn(), + findMany: vi.fn(), + count: vi.fn(), }, }, })); @@ -235,8 +224,34 @@ describe("Stream Controller", () => { await getStream(req as Request, res as Response); expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith( - expect.objectContaining({ streamId: 123 }), + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ streamId: 123 })); + expect(prisma.stream.findUnique).toHaveBeenCalledWith( + expect.objectContaining({ + include: expect.objectContaining({ + events: { orderBy: { timestamp: 'desc' } }, + }), + }), + ); + }); + }); + + describe('getStreamEvents', () => { + it('should paginate stream events ordered by timestamp desc', async () => { + req.params = { streamId: '123' }; + req.query = { limit: '10', offset: '0' }; + (prisma.streamEvent.findMany as any).mockResolvedValue([]); + (prisma.streamEvent.count as any).mockResolvedValue(0); + + await getStreamEvents(req as Request, res as Response); + + expect(res.status).toHaveBeenCalledWith(200); + expect(prisma.streamEvent.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + where: { streamId: 123 }, + orderBy: { timestamp: 'desc' }, + take: 10, + skip: 0, + }), ); }); }); diff --git a/backend/tests/user.controller.test.ts b/backend/tests/user.controller.test.ts index 6439d07b..6efeda19 100644 --- a/backend/tests/user.controller.test.ts +++ b/backend/tests/user.controller.test.ts @@ -167,6 +167,11 @@ describe('User Controller', () => { await getUserEvents(req as Request, res as Response, next); expect(res.status).toHaveBeenCalledWith(200); + expect(prisma.streamEvent.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + orderBy: { timestamp: 'desc' }, + }), + ); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ data: [], total: 0,