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 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_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_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 INDEX IF NOT EXISTS idx_event_start ON event (start_time)`,
|
||||||
`CREATE TABLE IF NOT EXISTS event_attendee (
|
`CREATE TABLE IF NOT EXISTS event_attendee (
|
||||||
event_id INTEGER NOT NULL,
|
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)
|
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
|
* 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
|
* now" browse query. Both bounds compare lexicographically on the generated ISO-8601
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
getEventsByClubs,
|
getEventsByClubs,
|
||||||
getEventsByCreator,
|
getEventsByCreator,
|
||||||
getEventsByIds,
|
getEventsByIds,
|
||||||
|
getEventsByRoom,
|
||||||
getEventTags,
|
getEventTags,
|
||||||
getLiveEvents,
|
getLiveEvents,
|
||||||
inviteToEvent,
|
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
|
// 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.
|
// started and not yet finished. A bare array, like the multi-club feed.
|
||||||
.get(
|
.get(
|
||||||
|
|||||||
@@ -4075,6 +4075,53 @@ describe('player events', () => {
|
|||||||
expect(ids).not.toContain(pastEvent.PlayerEventId)
|
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 () => {
|
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
|
// The client deserializes the multi-club form as a list — an envelope here fails
|
||||||
// with "expected:'[', actual:'{'". Do not unify the two.
|
// with "expected:'[', actual:'{'". Do not unify the two.
|
||||||
@@ -4788,6 +4835,7 @@ describe('openapi', () => {
|
|||||||
'GET /api/playerevents/v1/bulk',
|
'GET /api/playerevents/v1/bulk',
|
||||||
'GET /api/playerevents/v1/club/{clubId}',
|
'GET /api/playerevents/v1/club/{clubId}',
|
||||||
'GET /api/playerevents/v1/clubs',
|
'GET /api/playerevents/v1/clubs',
|
||||||
|
'GET /api/playerevents/v1/room/{roomId}',
|
||||||
'GET /api/playerevents/v1/search',
|
'GET /api/playerevents/v1/search',
|
||||||
'GET /api/playerevents/v1/searchlive',
|
'GET /api/playerevents/v1/searchlive',
|
||||||
'GET /api/playerevents/v1/tagfilters',
|
'GET /api/playerevents/v1/tagfilters',
|
||||||
|
|||||||
Reference in New Issue
Block a user