mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
rooms appearing in search
This commit is contained in:
@@ -70,3 +70,54 @@ export async function getRoomsByCreator(db: D1Database, accountId: number): Prom
|
||||
.all<RoomRow>()
|
||||
return parseAll(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search-tag aliases: a queried `#tag` also matches these stored tag names.
|
||||
* The client's pinned filters don't always match how rooms are tagged (e.g. it
|
||||
* searches `recroomoriginal`, but rooms are tagged `rro`).
|
||||
*/
|
||||
const TAG_ALIASES: Record<string, string[]> = {
|
||||
recroomoriginal: ['rro'],
|
||||
}
|
||||
|
||||
/** True if the room carries any of the given (lowercased) tags. */
|
||||
function roomHasAnyTag(room: Room, tags: Set<string>): boolean {
|
||||
const roomTags = room.Tags
|
||||
if (!Array.isArray(roomTags)) return false
|
||||
return roomTags.some((t) => {
|
||||
const value = (t as Record<string, unknown> | null)?.Tag
|
||||
return typeof value === 'string' && tags.has(value.toLowerCase())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Search public, non-dorm rooms. The query is split into terms (space/`+`):
|
||||
* `#tag` terms match the room's Tags; plain terms match the room name
|
||||
* (substring). All terms must match. Returns a paginated `{ Results, TotalResults }`.
|
||||
* The dataset is small, so this filters in memory rather than in SQL.
|
||||
*/
|
||||
export async function searchRooms(
|
||||
db: D1Database,
|
||||
query: string,
|
||||
skip: number,
|
||||
take: number
|
||||
): Promise<{ Results: Room[]; TotalResults: number }> {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (q === '') return { Results: [], TotalResults: 0 }
|
||||
const terms = q.split(/[\s+]+/).filter(Boolean)
|
||||
|
||||
const { results } = await db.prepare('SELECT data FROM rooms').all<RoomRow>()
|
||||
let rooms = parseAll(results).filter((r) => r.IsDorm !== true && r.Accessibility === 1)
|
||||
|
||||
for (const term of terms) {
|
||||
if (term.startsWith('#')) {
|
||||
const tag = term.slice(1)
|
||||
const accepted = new Set([tag, ...(TAG_ALIASES[tag] ?? [])])
|
||||
rooms = rooms.filter((r) => roomHasAnyTag(r, accepted))
|
||||
} else {
|
||||
rooms = rooms.filter((r) => typeof r.Name === 'string' && r.Name.toLowerCase().includes(term))
|
||||
}
|
||||
}
|
||||
|
||||
return { Results: rooms.slice(skip, skip + take), TotalResults: rooms.length }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import { getRoomById, getRoomByName, getRoomsByCreator, getRoomsByIds } from './rooms-db'
|
||||
import { getRoomById, getRoomByName, getRoomsByCreator, getRoomsByIds, searchRooms } from './rooms-db'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { App } from './context'
|
||||
@@ -108,6 +108,16 @@ const app = new Hono<App>()
|
||||
return c.json(room ?? {})
|
||||
})
|
||||
|
||||
// Room search: `query` is space/`+`-separated terms — `#tag` matches room tags,
|
||||
// plain terms match the name. Public, non-dorm rooms only. Paginated via
|
||||
// skip/take. Returns `{ Results, TotalResults }`.
|
||||
.get('/rooms/search', async (c) => {
|
||||
const query = c.req.query('query') ?? ''
|
||||
const skip = Number.parseInt(c.req.query('skip') ?? '0', 10) || 0
|
||||
const take = Number.parseInt(c.req.query('take') ?? '30', 10) || 30
|
||||
return c.json(await searchRooms(c.env.DB, query, skip, take))
|
||||
})
|
||||
|
||||
// Bulk room lookup by `id` or `name` — returns an array of matched rooms (the
|
||||
// client calls this bare on the rooms host). Rooms not in D1 are simply absent
|
||||
// from the result; the client treats an empty result as NoSuchRoom.
|
||||
|
||||
@@ -110,6 +110,43 @@ describe('rooms endpoints', () => {
|
||||
expect(other).toEqual([])
|
||||
})
|
||||
|
||||
it('GET /rooms/search returns a paginated { Results, TotalResults }', async () => {
|
||||
// Name-term search resolves a known public room.
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=reccenter&skip=0&take=100`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as { Results: Array<{ Name: string }>; TotalResults: number }
|
||||
expect(body.TotalResults).toBeGreaterThanOrEqual(1)
|
||||
expect(body.Results.some((r) => r.Name === 'RecCenter')).toBe(true)
|
||||
})
|
||||
|
||||
it('GET /rooms/search excludes dorms and respects pagination shape', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=dormroom`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as { Results: unknown[]; TotalResults: number }
|
||||
// The dorm is non-public/dorm, so a name search for it returns nothing.
|
||||
expect(body).toEqual({ Results: [], TotalResults: 0 })
|
||||
})
|
||||
|
||||
it('GET /rooms/search?query=#tag returns 200 (tag search)', async () => {
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=%23Quest+%23recroomoriginal`)
|
||||
expect(res.status).toBe(200)
|
||||
const body = (await res.json()) as { Results: unknown[]; TotalResults: number }
|
||||
expect(Array.isArray(body.Results)).toBe(true)
|
||||
expect(typeof body.TotalResults).toBe('number')
|
||||
})
|
||||
|
||||
it('GET /rooms/search aliases #recroomoriginal to the rro tag', async () => {
|
||||
// Rooms are tagged `rro`, not `recroomoriginal` — the alias bridges them.
|
||||
const aliased = (await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/search?query=%23recroomoriginal`)
|
||||
).json()) as { TotalResults: number }
|
||||
const direct = (await (await SELF.fetch(`${ORIGIN}/rooms/search?query=%23rro`)).json()) as {
|
||||
TotalResults: number
|
||||
}
|
||||
expect(aliased.TotalResults).toBe(direct.TotalResults)
|
||||
expect(aliased.TotalResults).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('GET /photon_access_token (bare + /roomserver) returns permissions', async () => {
|
||||
for (const path of ['/photon_access_token', '/roomserver/photon_access_token']) {
|
||||
const res = await SELF.fetch(`${ORIGIN}${path}`)
|
||||
|
||||
Reference in New Issue
Block a user