misc rooms endpoints, friends

This commit is contained in:
Devin Zuczek
2026-07-08 13:22:06 -04:00
parent 390843c679
commit a1d9fc17f0
22 changed files with 823 additions and 123 deletions
+27
View File
@@ -137,6 +137,33 @@ export async function getAccountByUsername(
)
}
/** Default cap on how many matches `searchAccounts` returns. */
export const SEARCH_LIMIT = 20
/** Escape LIKE wildcards so user input is matched literally (using `\` as the escape char). */
const escapeLike = (s: string): string => s.replace(/[\\%_]/g, '\\$&')
/**
* Prefix-search accounts by username (case-insensitive, "begins with"), ordered
* alphabetically. Backed by the indexed `username_lower` generated column, so the
* `name%` LIKE stays index-friendly. Returns up to `limit` matches.
*/
export async function searchAccounts(
db: D1Database,
name: string,
limit = SEARCH_LIMIT
): Promise<Account[]> {
const q = name.trim().toLowerCase()
if (q === '') return []
const { results } = await db
.prepare(
`SELECT data FROM accounts WHERE username_lower LIKE ?1 ESCAPE '\\' ORDER BY username_lower LIMIT ?2`
)
.bind(`${escapeLike(q)}%`, limit)
.all<AccountRow>()
return parseAll(results)
}
/** Look up multiple accounts by AccountId (order not guaranteed). */
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
if (ids.length === 0) return []
+11
View File
@@ -9,6 +9,7 @@ import {
getAccount,
getAccountByUsername,
getAccountsByIds,
searchAccounts,
updateAccount,
} from './accounts-db'
import { validateAndGetAccountId } from './jwt'
@@ -151,6 +152,16 @@ const app = new Hono<App>()
return c.json(toSelfAccountDto(account))
})
// ---- Search --------------------------------------------------------------
// Prefix-search accounts by username (`?name=`). Returns a bare array of public
// account DTOs, ordered alphabetically. Registered before `/account/:id` so the
// static `search` path wins over the param route.
.get('/account/search', async (c) => {
const name = c.req.query('name') ?? ''
const accounts = await searchAccounts(c.env.DB, name)
return c.json(accounts.map(toAccountDto))
})
// ---- Bulk / single lookup ------------------------------------------------
// Register the static `bulk` path before the `/account/:id` param route.
.get('/account/bulk', async (c) => {
@@ -95,6 +95,20 @@ describe('public endpoints', () => {
expect(accounts[1].username).toBe('Player2')
})
test('GET /account/search prefix-matches usernames, returns public DTOs', async () => {
const res = await exports.default.fetch(`${ORIGIN}/account/search?name=coa`)
expect(res.status).toBe(200)
const accounts = (await res.json()) as Array<{ accountId: number; username: string }>
// "Coach" (seeded uid 1) matches the "coa" prefix, case-insensitively.
expect(accounts.some((a) => a.accountId === 1 && a.username === 'Coach')).toBe(true)
// A non-matching prefix yields nothing.
const none = await exports.default.fetch(`${ORIGIN}/account/search?name=zzzznope`)
expect(await none.json()).toEqual([])
// An empty query yields nothing (no full-table dump).
const empty = await exports.default.fetch(`${ORIGIN}/account/search?name=`)
expect(await empty.json()).toEqual([])
})
test('GET /account/:id/bio returns an empty bio', async () => {
const res = await exports.default.fetch(`${ORIGIN}/account/7/bio`)
expect(await res.json()).toEqual({ accountId: 7, bio: '' })