mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -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([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user