From b60e4225803d39b9af9d05ae8ca8deb328eeaa5a Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Thu, 20 Aug 2026 15:11:49 -0400 Subject: [PATCH] [rooms] fix bulk loading issue --- apps/rooms/src/rooms.app.ts | 37 +++++++++++++++------ apps/rooms/src/test/integration/api.test.ts | 11 ++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/apps/rooms/src/rooms.app.ts b/apps/rooms/src/rooms.app.ts index 9718a8b..bd6b02d 100644 --- a/apps/rooms/src/rooms.app.ts +++ b/apps/rooms/src/rooms.app.ts @@ -167,6 +167,14 @@ function tooManyIds(c: Context) { return c.json(`At most ${MAX_BULK_ROOM_IDS} room ids may be looked up at once`, 400) } +/** + * Whether a bulk lookup asked for the public-only filter. The client sends the C# spelling + * (`True`/`False`) and absent means False, so anything but a case-insensitive `true` is off. + */ +function excludePrivateRooms(value: string | undefined): boolean { + return (value ?? '').toLowerCase() === 'true' +} + function allIds(idParam: string): number[] { return idParam .split(',') @@ -939,13 +947,15 @@ const app = new Hono() tags: ['Rooms'], summary: 'Look up several rooms at once', description: [ - 'Rooms by a comma-separated `id` list, or a single `name`. Ids that aren’t in D1 are', - 'simply absent from the result rather than an error — the client reads an empty result', - 'as NoSuchRoom.', + 'Rooms by `id` — repeated `id` params, a comma-separated list, or both — or a single', + '`name`. Ids that aren’t in D1 are simply absent from the result rather than an error —', + 'the client reads an empty result as NoSuchRoom. `excludePrivateRooms=True` drops rooms', + 'that are not publicly visible, as on the POST.', ].join(' '), parameters: [ - stringQuery('id', 'Comma-separated room ids'), + stringQuery('id', 'Room ids — repeated `id` fields, comma-separated, or both'), stringQuery('name', 'A single room name. Ignored when `id` is given'), + stringQuery('excludePrivateRooms', 'True drops rooms that are not publicly visible'), ], responses: { 200: json(RoomDto.array(), 'The rooms that matched (missing ids are omitted)'), @@ -956,15 +966,22 @@ const app = new Hono() }, }), async (c) => { - const idParam = c.req.query('id') + // `id` repeats (`?id=641&id=657`) as often as it is comma-separated, and the client + // spells it both ways — read every occurrence, then split each on commas. + const idParams = c.req.queries('id') ?? [] const nameParam = c.req.query('name') - if (!idParam && !nameParam) { + if (idParams.length === 0 && !nameParam) { return c.json("Either 'id' or 'name' query parameter is required", 400) } - if (idParam) { - const ids = allIds(idParam) + if (idParams.length > 0) { + const ids = idParams.flatMap(allIds) if (ids.length > MAX_BULK_ROOM_IDS) return tooManyIds(c) - return c.json(await getRoomsByIds(c.env.DB, ids)) + const rooms = await getRoomsByIds(c.env.DB, ids) + return c.json( + excludePrivateRooms(c.req.query('excludePrivateRooms')) + ? rooms.filter((r) => r.Accessibility === 1) + : rooms + ) } const room = await getRoomByName(c.env.DB, nameParam ?? '') return c.json(room ? [room] : []) @@ -1018,7 +1035,7 @@ const app = new Hono() if (ids.length > MAX_BULK_ROOM_IDS) return tooManyIds(c) const rooms = await getRoomsByIds(c.env.DB, ids) - const excludePrivate = (field('excludePrivateRooms')[0] ?? '').toLowerCase() === 'true' + const excludePrivate = excludePrivateRooms(field('excludePrivateRooms')[0]) return c.json(excludePrivate ? rooms.filter((r) => r.Accessibility === 1) : rooms) } ) diff --git a/apps/rooms/src/test/integration/api.test.ts b/apps/rooms/src/test/integration/api.test.ts index 5e7309e..f825733 100644 --- a/apps/rooms/src/test/integration/api.test.ts +++ b/apps/rooms/src/test/integration/api.test.ts @@ -219,6 +219,17 @@ describe('rooms endpoints', () => { expect(body.map((r) => r.Name).sort()).toEqual(['DormRoom', 'RecCenter']) }) + it('GET /rooms/bulk takes repeated id params, like the POST form', async () => { + // The client spells the GET's id list the same way it spells the POST body's — one + // `id` per room. Reading only the first left it rendering a single room. + const res = await SELF.fetch( + `${ORIGIN}/rooms/bulk?id=1&id=2&id=999999&excludePrivateRooms=False` + ) + expect(res.status).toBe(200) + const body = (await res.json()) as Array<{ Name: string }> + expect(body.map((r) => r.Name).sort()).toEqual(['DormRoom', 'RecCenter']) + }) + it('GET /rooms/bulk?name=RecCenter returns [RecCenter]', async () => { const res = await SELF.fetch(`${ORIGIN}/rooms/bulk?name=RecCenter`) const body = (await res.json()) as Array<{ Name: string }>