mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[api] another events endpoint
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
-- The room_id index behind a room's event shelf (`GET /api/playerevents/v1/room/{roomId}`).
|
||||
-- Owned by the `api` worker; generated from src/events-db.ts (SCHEMA_DDL) — keep in sync.
|
||||
--
|
||||
-- The column itself has been on the `event` table since 0006; only the index is new. The
|
||||
-- club feed has had one since that migration and the room feed now reads the same way, so
|
||||
-- without this a room's shelf scans every event in the database.
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_event_room ON event (room_id);
|
||||
@@ -41,6 +41,7 @@ export const SCHEMA_DDL: string[] = [
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_event_id ON event (id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_creator ON event (creator_player_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_club ON event (club_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_room ON event (room_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_start ON event (start_time)`,
|
||||
`CREATE TABLE IF NOT EXISTS event_attendee (
|
||||
event_id INTEGER NOT NULL,
|
||||
@@ -861,6 +862,30 @@ export async function getEventsByClubs(db: D1Database, clubIds: number[]): Promi
|
||||
return results.map((r) => JSON.parse(r.data) as PlayerEvent).sort(bySoonest)
|
||||
}
|
||||
|
||||
/**
|
||||
* A room's events — what is happening in this room and what is coming up, soonest first.
|
||||
* Backs the room's event shelf (`GET /api/playerevents/v1/room/{roomId}`).
|
||||
*
|
||||
* FINISHED events are left out, like the browse feed's: this answers "what can I still turn
|
||||
* up to in this room", and an event that ended last month is not that. Running events count
|
||||
* as current — the filter is on the END time, so an event stays listed until it is over
|
||||
* rather than disappearing the moment it starts.
|
||||
*
|
||||
* Selected on the indexed room_id column, with the time bound in SQL too: end_time is a
|
||||
* generated column of an ISO-8601 UTC string, so it compares lexicographically.
|
||||
*/
|
||||
export async function getEventsByRoom(
|
||||
db: D1Database,
|
||||
roomId: number,
|
||||
now = Date.now()
|
||||
): Promise<PlayerEvent[]> {
|
||||
const { results } = await db
|
||||
.prepare('SELECT data FROM event WHERE room_id = ?1 AND end_time >= ?2')
|
||||
.bind(roomId, eventTime(now))
|
||||
.all<EventRow>()
|
||||
return results.map((r) => JSON.parse(r.data) as PlayerEvent).sort(bySoonest)
|
||||
}
|
||||
|
||||
/**
|
||||
* The events happening right now — started and not yet finished. Backs the "happening
|
||||
* now" browse query. Both bounds compare lexicographically on the generated ISO-8601
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getEventsByClubs,
|
||||
getEventsByCreator,
|
||||
getEventsByIds,
|
||||
getEventsByRoom,
|
||||
getEventTags,
|
||||
getLiveEvents,
|
||||
inviteToEvent,
|
||||
@@ -358,6 +359,30 @@ export const eventRoutes = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// A room's event shelf (`/room/12`) — what is on in this room, current and upcoming.
|
||||
// A bare array of the stored record, like the multi-club shelf and `/searchlive`: the
|
||||
// single-club form's `{ ContinuationToken, Events }` envelope is the odd one out, and a
|
||||
// room's shelf is small enough that there is nothing to page.
|
||||
.get(
|
||||
'/api/playerevents/v1/room/:roomId{[0-9]+}',
|
||||
describeRoute({
|
||||
tags: ['Events'],
|
||||
summary: 'Player events in one room',
|
||||
description:
|
||||
'The events scheduled in a room — the shelf on the room’s page — soonest first. A ' +
|
||||
'bare array of the stored record, the same projection `/searchlive` and the ' +
|
||||
'multi-club shelf serve.\n\n' +
|
||||
'CURRENT and UPCOMING only: the filter is on the END time, so a running event stays ' +
|
||||
'listed until it is over rather than vanishing the moment it starts, and an event ' +
|
||||
'that has finished is dropped — this answers what someone can still turn up to. A ' +
|
||||
'room with nothing scheduled, and a room id that does not exist, both answer an ' +
|
||||
'empty array; the shelf is about events, not about whether the room is real.',
|
||||
parameters: [idParam('roomId', 'Room id')],
|
||||
responses: { 200: json(PlayerEventDto.array(), 'The room’s current and upcoming events') },
|
||||
}),
|
||||
async (c) => c.json(await getEventsByRoom(c.env.DB, Number.parseInt(c.req.param('roomId'), 10)))
|
||||
)
|
||||
|
||||
// Live player-event search (the "happening now" browse query) — events that have
|
||||
// started and not yet finished. A bare array, like the multi-club feed.
|
||||
.get(
|
||||
|
||||
@@ -4075,6 +4075,53 @@ describe('player events', () => {
|
||||
expect(ids).not.toContain(pastEvent.PlayerEventId)
|
||||
})
|
||||
|
||||
test('GET /api/playerevents/v1/room/:roomId serves that room’s current and upcoming events', async () => {
|
||||
// A room of this test's own, so events other tests create can't drift into the shelf.
|
||||
const soon = await create({
|
||||
RoomId: 12,
|
||||
Name: 'Room 12 Soon',
|
||||
StartTime: at(2 * HOUR),
|
||||
EndTime: at(3 * HOUR),
|
||||
})
|
||||
const running = await create({
|
||||
RoomId: 12,
|
||||
Name: 'Room 12 Running',
|
||||
StartTime: at(-HOUR),
|
||||
EndTime: at(HOUR),
|
||||
})
|
||||
const finished = await create({
|
||||
RoomId: 12,
|
||||
Name: 'Room 12 Finished',
|
||||
StartTime: at(-3 * HOUR),
|
||||
EndTime: at(-2 * HOUR),
|
||||
})
|
||||
const elsewhere = await create({
|
||||
RoomId: 13,
|
||||
Name: 'Room 13 Soon',
|
||||
StartTime: at(HOUR),
|
||||
EndTime: at(2 * HOUR),
|
||||
})
|
||||
|
||||
const res = await get('/api/playerevents/v1/room/12')
|
||||
expect(res.status).toBe(200)
|
||||
const events = (await res.json()) as PlayerEvent[]
|
||||
|
||||
// Soonest first, and RUNNING counts as current: the filter is on the end time, so an
|
||||
// event stays on the shelf until it is over rather than vanishing when it starts.
|
||||
expect(events.map((e) => e.PlayerEventId)).toEqual([running.PlayerEventId, soon.PlayerEventId])
|
||||
// A finished event is dropped — the shelf answers what you can still turn up to — and
|
||||
// another room's event is not this room's business.
|
||||
expect(events.map((e) => e.PlayerEventId)).not.toContain(finished.PlayerEventId)
|
||||
expect(events.map((e) => e.PlayerEventId)).not.toContain(elsewhere.PlayerEventId)
|
||||
|
||||
// A bare array of the STORED record, like `/searchlive` and the multi-club shelf —
|
||||
// not the base projection the browse feed serves, and not the single-club envelope.
|
||||
expect(events[0]).toEqual(asRecord(running, null))
|
||||
|
||||
// A room with nothing scheduled, and a room id nothing knows about, are both empty.
|
||||
expect(await (await get('/api/playerevents/v1/room/999999')).json()).toEqual([])
|
||||
})
|
||||
|
||||
test('GET /api/playerevents/v1/clubs is a bare array; /club/:id is a paged envelope', async () => {
|
||||
// The client deserializes the multi-club form as a list — an envelope here fails
|
||||
// with "expected:'[', actual:'{'". Do not unify the two.
|
||||
@@ -4788,6 +4835,7 @@ describe('openapi', () => {
|
||||
'GET /api/playerevents/v1/bulk',
|
||||
'GET /api/playerevents/v1/club/{clubId}',
|
||||
'GET /api/playerevents/v1/clubs',
|
||||
'GET /api/playerevents/v1/room/{roomId}',
|
||||
'GET /api/playerevents/v1/search',
|
||||
'GET /api/playerevents/v1/searchlive',
|
||||
'GET /api/playerevents/v1/tagfilters',
|
||||
|
||||
Reference in New Issue
Block a user