mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
more interaction endpoints
This commit is contained in:
@@ -273,6 +273,43 @@ export async function toggleFavorite(
|
||||
return toggleInteraction(db, playerId, roomId, 'favorited')
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly clear a single interaction flag on a room (the DELETE counterpart to
|
||||
* the cheer/favorite toggles). Idempotent: only clears an existing interaction row
|
||||
* and never creates one, so clearing a flag on a room the player never interacted
|
||||
* with doesn't add a spurious visited/favorited entry. Returns the interaction.
|
||||
*/
|
||||
async function clearInteraction(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number,
|
||||
column: 'cheered' | 'favorited'
|
||||
): Promise<Interaction> {
|
||||
await db
|
||||
.prepare(`UPDATE interaction SET ${column} = 0 WHERE player_id = ?1 AND room_id = ?2`)
|
||||
.bind(playerId, roomId)
|
||||
.run()
|
||||
return getInteraction(db, playerId, roomId)
|
||||
}
|
||||
|
||||
/** Clear the player's cheer on a room (DELETE cheer), returning the interaction. */
|
||||
export async function removeCheer(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number
|
||||
): Promise<Interaction> {
|
||||
return clearInteraction(db, playerId, roomId, 'cheered')
|
||||
}
|
||||
|
||||
/** Clear the player's favorite on a room (DELETE favorite), returning the interaction. */
|
||||
export async function removeFavorite(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number
|
||||
): Promise<Interaction> {
|
||||
return clearInteraction(db, playerId, roomId, 'favorited')
|
||||
}
|
||||
|
||||
/**
|
||||
* Search-tag aliases: a queried `#tag` also matches these stored tag names.
|
||||
* The client's pinned filters don't always match how rooms are tagged (e.g. it
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
getRoomsByCreator,
|
||||
getRoomsByIds,
|
||||
getSimilarRooms,
|
||||
removeCheer,
|
||||
removeFavorite,
|
||||
getVisitedRooms,
|
||||
searchRooms,
|
||||
setRoomDescription,
|
||||
@@ -270,6 +272,18 @@ const app = new Hono<App>()
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
// Explicitly un-cheer a room (DELETE clears the cheer, vs the PUT toggle).
|
||||
// Auth-gated; idempotent — un-cheering when there's no cheer is a no-op.
|
||||
.delete('/rooms/:roomId{[0-9]+}/interactionby/me/cheer', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const interaction = await removeCheer(
|
||||
c.env.DB,
|
||||
accountId,
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
.put('/rooms/:roomId{[0-9]+}/interactionby/me/favorite', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
@@ -280,6 +294,18 @@ const app = new Hono<App>()
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
// Explicitly un-favorite a room (DELETE clears the favorite, vs the PUT toggle).
|
||||
// Auth-gated; idempotent — un-favoriting when there's no favorite is a no-op.
|
||||
.delete('/rooms/:roomId{[0-9]+}/interactionby/me/favorite', async (c) => {
|
||||
const accountId = await authedAccountId(c)
|
||||
if (accountId === null) return unauthorized(c)
|
||||
const interaction = await removeFavorite(
|
||||
c.env.DB,
|
||||
accountId,
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Clone a room into a new one owned by the caller, using the `name` form field
|
||||
// (also accepted as a query param). Auth is required — no valid token is a 401,
|
||||
|
||||
@@ -620,4 +620,72 @@ describe('rooms endpoints', () => {
|
||||
).json()) as Interaction
|
||||
expect(otherGet).toMatchObject({ Cheered: false, Favorited: false })
|
||||
})
|
||||
|
||||
it('DELETE /rooms/:id/interactionby/me/cheer clears the cheer (auth-gated, idempotent)', async () => {
|
||||
type Interaction = { Cheered: boolean; Favorited: boolean }
|
||||
const headers = await bearer('557')
|
||||
const del = () =>
|
||||
SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/cheer`, { method: 'DELETE', headers })
|
||||
|
||||
// No token → 401.
|
||||
expect(
|
||||
(await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/cheer`, { method: 'DELETE' })).status
|
||||
).toBe(401)
|
||||
|
||||
// Cheer + favorite on, then DELETE clears only the cheer (favorite untouched).
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/cheer`, { method: 'PUT', headers })
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/favorite`, { method: 'PUT', headers })
|
||||
expect(await (await del()).json()).toMatchObject({ Cheered: false, Favorited: true })
|
||||
|
||||
// Idempotent — a second DELETE stays cleared.
|
||||
expect(await (await del()).json()).toMatchObject({ Cheered: false, Favorited: true })
|
||||
|
||||
// Idempotent on a never-interacted room, and it doesn't create a visited row.
|
||||
const fresh = await bearer('558')
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/2/interactionby/me/cheer`, {
|
||||
method: 'DELETE',
|
||||
headers: fresh,
|
||||
})
|
||||
expect(await res.json()).toMatchObject({ Cheered: false, Favorited: false })
|
||||
const visited = (await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/visitedby/me`, { headers: fresh })
|
||||
).json()) as unknown[]
|
||||
expect(visited).toEqual([])
|
||||
})
|
||||
|
||||
it('DELETE /rooms/:id/interactionby/me/favorite clears the favorite (auth-gated, idempotent)', async () => {
|
||||
const headers = await bearer('559')
|
||||
const del = () =>
|
||||
SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/favorite`, { method: 'DELETE', headers })
|
||||
|
||||
// No token → 401.
|
||||
expect(
|
||||
(await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/favorite`, { method: 'DELETE' })).status
|
||||
).toBe(401)
|
||||
|
||||
// Favorite + cheer on, then DELETE clears only the favorite (cheer untouched).
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/favorite`, { method: 'PUT', headers })
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/cheer`, { method: 'PUT', headers })
|
||||
expect(await (await del()).json()).toMatchObject({ Cheered: true, Favorited: false })
|
||||
// It drops out of the caller's favorited list.
|
||||
const favs = (await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/favoritedby/me`, { headers })
|
||||
).json()) as unknown[]
|
||||
expect(favs).toEqual([])
|
||||
|
||||
// Idempotent — a second DELETE stays cleared.
|
||||
expect(await (await del()).json()).toMatchObject({ Cheered: true, Favorited: false })
|
||||
|
||||
// Idempotent on a never-interacted room, without creating a visited row.
|
||||
const fresh = await bearer('560')
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/2/interactionby/me/favorite`, {
|
||||
method: 'DELETE',
|
||||
headers: fresh,
|
||||
})
|
||||
expect(await res.json()).toMatchObject({ Cheered: false, Favorited: false })
|
||||
const visited = (await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/visitedby/me`, { headers: fresh })
|
||||
).json()) as unknown[]
|
||||
expect(visited).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Generated
-31
@@ -268,37 +268,6 @@ importers:
|
||||
specifier: 4.105.0
|
||||
version: 4.105.0(@cloudflare/workers-types@4.20260630.1)
|
||||
|
||||
apps/datacollection:
|
||||
dependencies:
|
||||
'@repo/hono-helpers':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/hono-helpers
|
||||
hono:
|
||||
specifier: 4.12.27
|
||||
version: 4.12.27
|
||||
workers-tagged-logger:
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
devDependencies:
|
||||
'@cloudflare/vitest-pool-workers':
|
||||
specifier: 0.16.20
|
||||
version: 0.16.20(@cloudflare/workers-types@4.20260630.1)(@vitest/runner@4.1.9)(@vitest/snapshot@4.1.9)(vitest@4.1.9(@types/node@26.0.1)(vite@6.3.4(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.29.2)(tsx@4.22.4)(yaml@2.9.0)))
|
||||
'@repo/tools':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/tools
|
||||
'@repo/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/typescript-config
|
||||
'@types/node':
|
||||
specifier: 26.0.1
|
||||
version: 26.0.1
|
||||
vitest:
|
||||
specifier: 4.1.9
|
||||
version: 4.1.9(@types/node@26.0.1)(vite@6.3.4(@types/node@26.0.1)(jiti@2.6.1)(lightningcss@1.29.2)(tsx@4.22.4)(yaml@2.9.0))
|
||||
wrangler:
|
||||
specifier: 4.105.0
|
||||
version: 4.105.0(@cloudflare/workers-types@4.20260630.1)
|
||||
|
||||
apps/econ:
|
||||
dependencies:
|
||||
'@repo/hono-helpers':
|
||||
|
||||
Reference in New Issue
Block a user