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 |
|
| POST | `/api/challenge/v2/updateProgress` | ✓ | Report challenge progress |
|
||||||
| GET | `/api/gamerewards/v1/pending` | | Pending game rewards (stub `[]`) |
|
| GET | `/api/gamerewards/v1/pending` | | Pending game rewards (stub `[]`) |
|
||||||
| POST | `/api/gamerewards/v1/request` | ✓ | Claim a game reward → 10 XP + gift box |
|
| 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 |
|
| 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) |
|
| 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) |
|
| PUT | `/api/roomkeys/v1/updateAll` | ✓ | Edit one key (owner/co-owner) |
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ import {
|
|||||||
RoomCurrencyPurchaseOfferEnvelope,
|
RoomCurrencyPurchaseOfferEnvelope,
|
||||||
RoomCurrencyPurchaseOffersDto,
|
RoomCurrencyPurchaseOffersDto,
|
||||||
RoomEconConfig,
|
RoomEconConfig,
|
||||||
|
HeldRoomKeyDto,
|
||||||
RoomKeyDto,
|
RoomKeyDto,
|
||||||
RoomKeyEnvelope,
|
RoomKeyEnvelope,
|
||||||
RoomPurchasableList,
|
RoomPurchasableList,
|
||||||
@@ -195,6 +196,7 @@ import {
|
|||||||
claimRoomKey,
|
claimRoomKey,
|
||||||
createRoomKey,
|
createRoomKey,
|
||||||
getRoomKey,
|
getRoomKey,
|
||||||
|
getHeldRoomKeys,
|
||||||
getRoomKeys,
|
getRoomKeys,
|
||||||
getRoomOwnerId,
|
getRoomOwnerId,
|
||||||
ownsRoomKeys,
|
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.
|
// The caller's room keys: every key they hold (`room_key_player`), across all rooms.
|
||||||
.get('/api/roomkeys/v1/mine', listRoute('The player’s room keys', 'Empty for now'), (c) =>
|
.get(
|
||||||
c.json([])
|
'/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
|
// 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
|
// 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'),
|
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 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
|
* 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)
|
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
|
* 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
|
* 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)
|
expect(rows?.count).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('GET /api/roomkeys/v1/mine returns []', async () => {
|
test('GET /api/roomkeys/v1/mine lists the keys the caller holds, across rooms', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/roomkeys/v1/mine`)
|
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(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 () => {
|
test('GET /api/subscriptionseasons/v1/seasons/current returns []', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user