mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 23:21:30 -07:00
add interactions (cheer/faves)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
-- Per-player interaction state with a room (cheered/favorited + last visit).
|
||||
-- One row per (player, room); cheer/favorite are toggled in place.
|
||||
CREATE TABLE IF NOT EXISTS interaction (
|
||||
player_id INTEGER NOT NULL,
|
||||
room_id INTEGER NOT NULL,
|
||||
cheered INTEGER NOT NULL DEFAULT 0,
|
||||
favorited INTEGER NOT NULL DEFAULT 0,
|
||||
last_visited_at TEXT,
|
||||
PRIMARY KEY (player_id, room_id)
|
||||
);
|
||||
@@ -22,6 +22,16 @@ export const SCHEMA_DDL: string[] = [
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_rooms_room_id ON rooms (room_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_rooms_name_lower ON rooms (name_lower)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_rooms_creator ON rooms (creator_account_id)`,
|
||||
// Per-player interaction state with a room (cheered/favorited + last visit).
|
||||
// One row per (player, room); cheer/favorite are toggled in place.
|
||||
`CREATE TABLE IF NOT EXISTS interaction (
|
||||
player_id INTEGER NOT NULL,
|
||||
room_id INTEGER NOT NULL,
|
||||
cheered INTEGER NOT NULL DEFAULT 0,
|
||||
favorited INTEGER NOT NULL DEFAULT 0,
|
||||
last_visited_at TEXT,
|
||||
PRIMARY KEY (player_id, room_id)
|
||||
)`,
|
||||
]
|
||||
|
||||
/** A stored room — the parsed JSON blob (full client-facing room response). */
|
||||
@@ -71,6 +81,77 @@ export async function getRoomsByCreator(db: D1Database, accountId: number): Prom
|
||||
return parseAll(results)
|
||||
}
|
||||
|
||||
/** A player's interaction state with a room. */
|
||||
export interface Interaction {
|
||||
Cheered: boolean
|
||||
Favorited: boolean
|
||||
}
|
||||
|
||||
interface InteractionRow {
|
||||
cheered: number
|
||||
favorited: number
|
||||
}
|
||||
|
||||
const toInteraction = (row: InteractionRow | null): Interaction => ({
|
||||
Cheered: row?.cheered === 1,
|
||||
Favorited: row?.favorited === 1,
|
||||
})
|
||||
|
||||
/** Read a player's interaction with a room (defaults to all-false if none). */
|
||||
export async function getInteraction(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number
|
||||
): Promise<Interaction> {
|
||||
return toInteraction(
|
||||
await db
|
||||
.prepare('SELECT cheered, favorited FROM interaction WHERE player_id = ?1 AND room_id = ?2')
|
||||
.bind(playerId, roomId)
|
||||
.first<InteractionRow>()
|
||||
)
|
||||
}
|
||||
|
||||
/** Upsert+toggle a single boolean column, returning the resulting interaction. */
|
||||
async function toggleInteraction(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number,
|
||||
column: 'cheered' | 'favorited'
|
||||
): Promise<Interaction> {
|
||||
const now = new Date().toISOString()
|
||||
// First interaction defaults the toggled column to 1; subsequent calls flip it.
|
||||
return toInteraction(
|
||||
await db
|
||||
.prepare(
|
||||
`INSERT INTO interaction (player_id, room_id, ${column}, last_visited_at)
|
||||
VALUES (?1, ?2, 1, ?3)
|
||||
ON CONFLICT(player_id, room_id)
|
||||
DO UPDATE SET ${column} = NOT ${column}, last_visited_at = ?3
|
||||
RETURNING cheered, favorited`
|
||||
)
|
||||
.bind(playerId, roomId, now)
|
||||
.first<InteractionRow>()
|
||||
)
|
||||
}
|
||||
|
||||
/** Toggle the player's cheer on a room, returning the resulting interaction. */
|
||||
export async function toggleCheer(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number
|
||||
): Promise<Interaction> {
|
||||
return toggleInteraction(db, playerId, roomId, 'cheered')
|
||||
}
|
||||
|
||||
/** Toggle the player's favorite on a room, returning the resulting interaction. */
|
||||
export async function toggleFavorite(
|
||||
db: D1Database,
|
||||
playerId: number,
|
||||
roomId: number
|
||||
): Promise<Interaction> {
|
||||
return toggleInteraction(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
|
||||
|
||||
@@ -4,7 +4,16 @@ import { useWorkersLogger } from 'workers-tagged-logger'
|
||||
import { withNotFound, withOnError } from '@repo/hono-helpers'
|
||||
|
||||
import { validateAndGetAccountId } from './jwt'
|
||||
import { getRoomById, getRoomByName, getRoomsByCreator, getRoomsByIds, searchRooms } from './rooms-db'
|
||||
import {
|
||||
getInteraction,
|
||||
getRoomById,
|
||||
getRoomByName,
|
||||
getRoomsByCreator,
|
||||
getRoomsByIds,
|
||||
searchRooms,
|
||||
toggleCheer,
|
||||
toggleFavorite,
|
||||
} from './rooms-db'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import type { App } from './context'
|
||||
@@ -141,6 +150,36 @@ const app = new Hono<App>()
|
||||
.get('/rooms/ownedby/me', async (c) => c.json(await getRoomsByCreator(c.env.DB, await ownerId(c))))
|
||||
.get('/rooms/createdby/me', async (c) => c.json(await getRoomsByCreator(c.env.DB, await ownerId(c))))
|
||||
|
||||
// The current player's interaction state with a room (cheered/favorited/last
|
||||
// visited), read from the `interaction` table.
|
||||
.get('/rooms/:roomId{[0-9]+}/interactionby/me', async (c) => {
|
||||
const interaction = await getInteraction(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Toggle the player's cheer/favorite on a room. Both are PUTs that flip the
|
||||
// stored flag and return the updated interaction (matches the C#).
|
||||
.put('/rooms/:roomId{[0-9]+}/interactionby/me/cheer', async (c) => {
|
||||
const interaction = await toggleCheer(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
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 interaction = await toggleFavorite(
|
||||
c.env.DB,
|
||||
await ownerId(c),
|
||||
Number.parseInt(c.req.param('roomId'), 10)
|
||||
)
|
||||
return c.json({ ...interaction, LastVisitedAt: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Single room by id. 404 when the room isn't in D1 (matches the C#). Ignores
|
||||
// the include/unityAsset* query params, same as the C#.
|
||||
.get('/rooms/:roomId{[0-9]+}', async (c) => {
|
||||
|
||||
@@ -155,4 +155,36 @@ describe('rooms endpoints', () => {
|
||||
expect(body.Permissions.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('interaction: defaults to false, cheer/favorite toggle and persist', async () => {
|
||||
type Interaction = { Cheered: boolean; Favorited: boolean; LastVisitedAt: string }
|
||||
const headers = await bearer('555')
|
||||
const get = async () =>
|
||||
(await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me`, { headers })
|
||||
).json()) as Interaction
|
||||
const put = async (action: 'cheer' | 'favorite') =>
|
||||
(await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me/${action}`, { method: 'PUT', headers })
|
||||
).json()) as Interaction
|
||||
|
||||
// No row yet → both false.
|
||||
expect(await get()).toMatchObject({ Cheered: false, Favorited: false })
|
||||
|
||||
// Cheer on, then favorite on.
|
||||
expect(await put('cheer')).toMatchObject({ Cheered: true, Favorited: false })
|
||||
expect(await put('favorite')).toMatchObject({ Cheered: true, Favorited: true })
|
||||
// Persisted across a fresh GET.
|
||||
expect(await get()).toMatchObject({ Cheered: true, Favorited: true })
|
||||
|
||||
// Toggling again flips back.
|
||||
expect(await put('cheer')).toMatchObject({ Cheered: false, Favorited: true })
|
||||
|
||||
// Scoped per player — a different account starts fresh.
|
||||
const other = await bearer('556')
|
||||
const otherGet = (await (
|
||||
await SELF.fetch(`${ORIGIN}/rooms/12/interactionby/me`, { headers: other })
|
||||
).json()) as Interaction
|
||||
expect(otherGet).toMatchObject({ Cheered: false, Favorited: false })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user