mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[econ] fix missing owned keys endpoint
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@ missing/invalid). `~` = optional auth: served to anyone, personalised for a vali
|
||||
| POST | `/api/challenge/v2/updateProgress` | ✓ | Report challenge progress |
|
||||
| GET | `/api/gamerewards/v1/pending` | | Pending game rewards (stub `[]`) |
|
||||
| POST | `/api/gamerewards/v1/request` | ✓ | Claim a game reward → 10 XP + gift box |
|
||||
| GET | `/api/roomkeys/v1/mine` | | The player's room keys (stub `[]`) |
|
||||
| GET | `/api/roomkeys/v1/mine` | ✓ | The player's held room keys |
|
||||
| GET | `/api/roomkeys/v1/room` | | The keys a room has listed |
|
||||
| POST | `/api/roomkeys/v1/create` | ✓ | List a key for a room (owner/co-owner) |
|
||||
| PUT | `/api/roomkeys/v1/updateAll` | ✓ | Edit one key (owner/co-owner) |
|
||||
|
||||
@@ -147,6 +147,7 @@ import {
|
||||
RoomCurrencyPurchaseOfferEnvelope,
|
||||
RoomCurrencyPurchaseOffersDto,
|
||||
RoomEconConfig,
|
||||
HeldRoomKeyDto,
|
||||
RoomKeyDto,
|
||||
RoomKeyEnvelope,
|
||||
RoomPurchasableList,
|
||||
@@ -195,6 +196,7 @@ import {
|
||||
claimRoomKey,
|
||||
createRoomKey,
|
||||
getRoomKey,
|
||||
getHeldRoomKeys,
|
||||
getRoomKeys,
|
||||
getRoomOwnerId,
|
||||
ownsRoomKeys,
|
||||
@@ -6391,9 +6393,31 @@ const app = new Hono<App>({ strict: false })
|
||||
}
|
||||
)
|
||||
|
||||
// The player's room keys. Returns "[]": nothing sells a key yet, so nobody holds one.
|
||||
.get('/api/roomkeys/v1/mine', listRoute('The player’s room keys', 'Empty for now'), (c) =>
|
||||
c.json([])
|
||||
// The caller's room keys: every key they hold (`room_key_player`), across all rooms.
|
||||
.get(
|
||||
'/api/roomkeys/v1/mine',
|
||||
describeRoute({
|
||||
tags: ['Econ'],
|
||||
summary: 'The player’s room keys',
|
||||
description: [
|
||||
'Every key the caller holds — bought, awarded, or taken free as a room manager —',
|
||||
'across every room, oldest key first. No parameters.',
|
||||
'',
|
||||
'Each entry is the room key plus `IsGiftable` (false) and `Visibility` (0), with',
|
||||
'`ImageName` as "" rather than null when the key has no art — the shape observed',
|
||||
'from the live client, which differs from `/api/roomkeys/v1/room`’s.',
|
||||
].join('\n'),
|
||||
security: AUTHED,
|
||||
responses: {
|
||||
200: json(HeldRoomKeyDto.array(), 'The caller’s keys, oldest first'),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const id = await authedId(c)
|
||||
if (id === null) return unauthorized(c)
|
||||
return c.json(await getHeldRoomKeys(c.env.DB, id))
|
||||
}
|
||||
)
|
||||
// The keys a room has listed. Public, like the room's currencies: a key is offered to
|
||||
// whoever walks up to the door it opens. A room with none, an unknown room and a missing
|
||||
|
||||
@@ -779,6 +779,13 @@ export const RoomKeyDto = z.object({
|
||||
Type: z.int().describe('The key type’s ordinal: 0 `Key`, the only one seen'),
|
||||
})
|
||||
|
||||
/** A held key as `GET /api/roomkeys/v1/mine` serves it — the room key plus two fields. */
|
||||
export const HeldRoomKeyDto = RoomKeyDto.extend({
|
||||
ImageName: z.string().describe('The key’s art; "" when it has none (NOT null, unlike the list)'),
|
||||
IsGiftable: z.boolean().describe('Always false — gifting keys is not modelled'),
|
||||
Visibility: z.int().describe('Always 0 — not modelled'),
|
||||
})
|
||||
|
||||
/**
|
||||
* The envelope `POST /api/roomkeys/v1/create` answers in: `{ Status, RoomKey }`, observed from
|
||||
* the live client — NOT the `{ Value, Success, Error, error_id }` the room-currency and
|
||||
|
||||
@@ -213,6 +213,38 @@ export async function getRoomKeys(db: D1Database, roomId: number): Promise<RoomK
|
||||
return results.map(toRoomKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* A key as `GET /api/roomkeys/v1/mine` serves it — observed from the live client: the
|
||||
* {@link RoomKey} plus `IsGiftable` and `Visibility`, and `ImageName` as '' (not null) on a
|
||||
* key without art. Neither extra field is modelled here, so both are served as their 0/false.
|
||||
*/
|
||||
export interface HeldRoomKey extends Omit<RoomKey, 'ImageName'> {
|
||||
ImageName: string
|
||||
IsGiftable: boolean
|
||||
Visibility: number
|
||||
}
|
||||
|
||||
/** Every key `accountId` holds, across all rooms, in creation order. */
|
||||
export async function getHeldRoomKeys(db: D1Database, accountId: number): Promise<HeldRoomKey[]> {
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
`SELECT ${SELECT_COLUMNS.split(', ')
|
||||
.map((col) => `k.${col}`)
|
||||
.join(', ')}
|
||||
FROM room_key_player p JOIN room_key k ON k.room_key_id = p.room_key_id
|
||||
WHERE p.account_id = ?1 ORDER BY k.room_key_id`
|
||||
)
|
||||
.bind(accountId)
|
||||
.all<RoomKeyRow>()
|
||||
|
||||
return results.map((row) => ({
|
||||
...toRoomKey(row),
|
||||
ImageName: row.image_name,
|
||||
IsGiftable: false,
|
||||
Visibility: 0,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a player a room key, answering when they came to hold it — now, or the first award's
|
||||
* time when they already did (a key is held once; re-awarding is a no-op). Null when
|
||||
|
||||
@@ -7211,10 +7211,77 @@ describe('econ endpoints', () => {
|
||||
expect(rows?.count).toBe(0)
|
||||
})
|
||||
|
||||
test('GET /api/roomkeys/v1/mine returns []', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/roomkeys/v1/mine`)
|
||||
test('GET /api/roomkeys/v1/mine lists the keys the caller holds, across rooms', async () => {
|
||||
const mine = async (sub: string | null) =>
|
||||
exports.default.fetch(
|
||||
`${ORIGIN}/api/roomkeys/v1/mine`,
|
||||
sub === null ? undefined : { headers: await bearer(sub) }
|
||||
)
|
||||
expect((await mine(null)).status).toBe(401)
|
||||
expect(await (await mine('7601')).json()).toEqual([])
|
||||
|
||||
const insert = async (roomId: number, name: string, imageName: string) =>
|
||||
(await env.DB.prepare(
|
||||
`INSERT INTO room_key
|
||||
(replication_id, room_id, key_type, name, description, price, purchase_currency_id, image_name, created_at)
|
||||
VALUES ('0f8c3a7e-1d42-4b90-9c11-7a6e5d2b8f03', ?1, 'Key', ?2, '', 0, NULL, ?3, '2026-10-14T12:00:00Z')
|
||||
RETURNING room_key_id`
|
||||
)
|
||||
.bind(roomId, name, imageName)
|
||||
.first<{ room_key_id: number }>())!.room_key_id
|
||||
const a = await insert(12, 'VIP Key', '')
|
||||
const b = await insert(2511, 'Art Key', 'key.png')
|
||||
const notHeld = await insert(12, 'Other', '')
|
||||
for (const [key, account] of [
|
||||
[b, 7601],
|
||||
[a, 7601],
|
||||
[notHeld, 7602],
|
||||
]) {
|
||||
await env.DB.prepare(
|
||||
"INSERT INTO room_key_player (room_key_id, account_id, awarded_at) VALUES (?1, ?2, '')"
|
||||
)
|
||||
.bind(key, account)
|
||||
.run()
|
||||
}
|
||||
|
||||
const res = await mine('7601')
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual([])
|
||||
const keys = (await res.json()) as Array<Record<string, unknown>>
|
||||
expect(keys).toEqual([
|
||||
{
|
||||
RoomKeyId: a,
|
||||
ReplicationId: '0f8c3a7e-1d42-4b90-9c11-7a6e5d2b8f03',
|
||||
RoomId: 12,
|
||||
Name: 'VIP Key',
|
||||
Description: '',
|
||||
Price: 0,
|
||||
PurchaseCurrencyId: null,
|
||||
CreatedAt: '2026-10-14T12:00:00Z',
|
||||
ImageName: '',
|
||||
Type: 0,
|
||||
IsGiftable: false,
|
||||
Visibility: 0,
|
||||
},
|
||||
expect.objectContaining({ RoomKeyId: b, RoomId: 2511, ImageName: 'key.png' }),
|
||||
])
|
||||
// Member order as the client was observed receiving it.
|
||||
expect(Object.keys(keys[0]!)).toEqual([
|
||||
'RoomKeyId',
|
||||
'ReplicationId',
|
||||
'RoomId',
|
||||
'Name',
|
||||
'Description',
|
||||
'Price',
|
||||
'PurchaseCurrencyId',
|
||||
'CreatedAt',
|
||||
'ImageName',
|
||||
'Type',
|
||||
'IsGiftable',
|
||||
'Visibility',
|
||||
])
|
||||
|
||||
await env.DB.prepare('DELETE FROM room_key_player').run()
|
||||
await env.DB.prepare('DELETE FROM room_key').run()
|
||||
})
|
||||
|
||||
test('GET /api/subscriptionseasons/v1/seasons/current returns []', async () => {
|
||||
|
||||
Reference in New Issue
Block a user