mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
misc rooms endpoints, friends
This commit is contained in:
@@ -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 []
|
||||
|
||||
@@ -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: '' })
|
||||
|
||||
Reference in New Issue
Block a user