[sql] improvements

This commit is contained in:
Devin Zuczek
2026-10-25 13:35:18 -04:00
parent 8397997234
commit b02a646892
8 changed files with 182 additions and 108 deletions
+34
View File
@@ -347,6 +347,40 @@ const app = new Hono<App>()
}
)
// Exact lookup by username — what the site's `/u/<name>` profile pages and the staff
// `@username` fields resolve a player through. An indexed equality on `username_lower`;
// before this existed they went through the prefix search above and filtered the
// exact hit client-side, which cost a 20-row index walk per profile view. Same path
// as the official API's `GET /account/username/{username}`.
.get(
'/account/username/:name',
describeRoute({
tags: ['Lookup'],
summary: 'A single public account, by exact username',
description:
'Case-insensitive exact match; a leading `@` is ignored. 404 when no account has the name.',
parameters: [
{
name: 'name',
in: 'path',
required: true,
description: 'The username, exactly (case-insensitive)',
schema: { type: 'string' },
},
],
responses: {
200: json(AccountDto, 'The public account'),
404: { description: 'No account has that username' },
},
}),
async (c) => {
const name = c.req.param('name').trim().replace(/^@/, '')
const account = name === '' ? null : await getAccountByUsername(c.env.DB, name)
if (!account) return c.json({ error: `There's no player called @${name}.` }, 404)
return c.json(toAccountDto(account))
}
)
// ---- Bulk / single lookup ------------------------------------------------
// Register the static `bulk` path before the `/account/:id` param route.
.get('/account/bulk', describeRoute(bulkRouteSpec('query')), bulkAccounts)
@@ -153,6 +153,24 @@ describe('public endpoints', () => {
expect(await empty.json()).toEqual([])
})
test('GET /account/username/:name is an exact, case-insensitive lookup', async () => {
const res = await exports.default.fetch(`${ORIGIN}/account/username/coach`)
expect(res.status).toBe(200)
const account = (await res.json()) as { accountId: number; username: string }
expect(account).toMatchObject({ accountId: 1, username: 'Coach' })
// A leading `@` is tolerated — staff fields take names in that form.
const at = await exports.default.fetch(
`${ORIGIN}/account/username/${encodeURIComponent('@Coach')}`
)
expect(((await at.json()) as { accountId: number }).accountId).toBe(1)
// A prefix is NOT a match: that's what `/account/search` is for.
const prefix = await exports.default.fetch(`${ORIGIN}/account/username/coa`)
expect(prefix.status).toBe(404)
const missing = await exports.default.fetch(`${ORIGIN}/account/username/zzzznope`)
expect(missing.status).toBe(404)
expect(await missing.json()).toEqual({ error: "There's no player called @zzzznope." })
})
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: '' })
@@ -689,6 +707,7 @@ describe('auth-gated endpoints', () => {
'GET /account/bulk',
'GET /account/me',
'GET /account/search',
'GET /account/username/{name}',
'GET /account/{id}',
'GET /account/{id}/bio',
'GET /accountprivacysettings/{id}',