mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
[rooms] role refactor
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
-- Room roles as their own table, mirroring 0013's move of tags into `room_tag`. One row
|
||||
-- per (room, account). Generated from packages/domain/src/rooms-db.ts (ROOM_SCHEMA_DDL) —
|
||||
-- keep in sync.
|
||||
--
|
||||
-- The row is the client's `Roles` entry shape:
|
||||
-- { "AccountId": …, "Role": …, "LastChangedByAccountId": …, "InvitedRole": … }
|
||||
-- `role` is the member's CURRENT role tier (10 Host, 20 Moderator, 30 CoOwner,
|
||||
-- 255 Creator); `invited_role` is the tier they have been OFFERED but not yet accepted —
|
||||
-- usually higher than `role`, and 0 when no invitation is pending. `last_changed_by` is
|
||||
-- who last touched the row; NULL for creator entries, matching the blob's
|
||||
-- `LastChangedByAccountId: null`.
|
||||
--
|
||||
-- The table is AUTHORITATIVE and the blob's `Roles` key is removed below, the same
|
||||
-- arrangement 0013 gave tags: `serializeRoom` drops `Roles` on write and the reads
|
||||
-- re-attach it (attachRoles), so the room DTO the client sees is unchanged and the two
|
||||
-- copies can't drift. `getContributedRooms` now matches on this table instead of running
|
||||
-- `json_each` over every blob.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_role (
|
||||
room_id INTEGER NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
role INTEGER NOT NULL DEFAULT 0,
|
||||
last_changed_by INTEGER,
|
||||
invited_role INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (room_id, account_id)
|
||||
);
|
||||
|
||||
-- Backfill from the blobs, the way 0013 backfilled `room_tag`. `json_each` walks the
|
||||
-- `Roles` array; a room with no array (or a null one) contributes nothing, which is why
|
||||
-- this is a join rather than a correlated subquery. `INSERT OR IGNORE` collapses a blob
|
||||
-- that somehow carries the same account twice — the primary key is (room, account).
|
||||
INSERT OR IGNORE INTO room_role (room_id, account_id, role, last_changed_by, invited_role)
|
||||
SELECT
|
||||
r.room_id,
|
||||
json_extract(t.value, '$.AccountId'),
|
||||
COALESCE(json_extract(t.value, '$.Role'), 0),
|
||||
json_extract(t.value, '$.LastChangedByAccountId'),
|
||||
COALESCE(json_extract(t.value, '$.InvitedRole'), 0)
|
||||
FROM room r, json_each(r.data, '$.Roles') t
|
||||
WHERE json_extract(t.value, '$.AccountId') IS NOT NULL;
|
||||
|
||||
-- Single source of truth: the roles now live in `room_role`, so the copy in the blob
|
||||
-- goes. Leaving it would be a second answer to "who has a role in this room" that only
|
||||
-- the writes through setRoomRole keep current.
|
||||
UPDATE room SET data = json_remove(data, '$.Roles');
|
||||
@@ -425,6 +425,11 @@ describe('rooms endpoints', () => {
|
||||
it('GET /rooms/contributedby/me lists rooms the caller owns or has a role in', async () => {
|
||||
const seed = (data: Record<string, unknown>) =>
|
||||
env.DB.prepare('INSERT INTO room (data) VALUES (?1)').bind(JSON.stringify(data)).run()
|
||||
// Roles live in `room_role`, not the blob — the table the query's EXISTS matches on.
|
||||
const grant = (roomId: number, accountId: number, role: number) =>
|
||||
env.DB.prepare('INSERT INTO room_role (room_id, account_id, role) VALUES (?1, ?2, ?3)')
|
||||
.bind(roomId, accountId, role)
|
||||
.run()
|
||||
|
||||
// A room somebody else made, where 820 is a co-owner...
|
||||
await seed({
|
||||
@@ -433,11 +438,9 @@ describe('rooms endpoints', () => {
|
||||
CreatorAccountId: 821,
|
||||
Accessibility: 1,
|
||||
SubRooms: [],
|
||||
Roles: [
|
||||
{ AccountId: 821, Role: 255 },
|
||||
{ AccountId: 820, Role: 30 },
|
||||
],
|
||||
})
|
||||
await grant(30401, 821, 255)
|
||||
await grant(30401, 820, 30)
|
||||
// ...one where they're only a host (every tier counts, not just owner-level)...
|
||||
await seed({
|
||||
RoomId: 30402,
|
||||
@@ -447,20 +450,20 @@ describe('rooms endpoints', () => {
|
||||
// accessibility is not filtered here.
|
||||
Accessibility: 0,
|
||||
SubRooms: [],
|
||||
Roles: [{ AccountId: 820, Role: 10 }],
|
||||
})
|
||||
// ...one they created themselves, whose Roles name them as Creator (matched by BOTH
|
||||
// halves of the query, so it must still appear exactly once)...
|
||||
await grant(30402, 820, 10)
|
||||
// ...one they created themselves, whose role rows name them as Creator (matched by
|
||||
// BOTH halves of the query, so it must still appear exactly once)...
|
||||
await seed({
|
||||
RoomId: 30403,
|
||||
Name: 'ContribOwn',
|
||||
CreatorAccountId: 820,
|
||||
Accessibility: 1,
|
||||
SubRooms: [],
|
||||
Roles: [{ AccountId: 820, Role: 255 }],
|
||||
})
|
||||
// ...one they created that names nobody in Roles at all — the older rooms have no
|
||||
// Roles key, and those reach the list on the creator half alone...
|
||||
await grant(30403, 820, 255)
|
||||
// ...one they created that has no role rows at all — the older rooms never got
|
||||
// any, and those reach the list on the creator half alone...
|
||||
await seed({
|
||||
RoomId: 30406,
|
||||
Name: 'ContribOwnNoRoles',
|
||||
@@ -476,18 +479,17 @@ describe('rooms endpoints', () => {
|
||||
IsDorm: true,
|
||||
Accessibility: 2,
|
||||
SubRooms: [],
|
||||
Roles: [{ AccountId: 820, Role: 255 }],
|
||||
})
|
||||
// ...one they have nothing to do with, and one with no Roles key at all (the older
|
||||
// seeded rooms have none — json_each must drop them, not error).
|
||||
await grant(30407, 820, 255)
|
||||
// ...one they have nothing to do with, and one with no role rows at all.
|
||||
await seed({
|
||||
RoomId: 30404,
|
||||
Name: 'ContribOther',
|
||||
CreatorAccountId: 821,
|
||||
Accessibility: 1,
|
||||
SubRooms: [],
|
||||
Roles: [{ AccountId: 822, Role: 30 }],
|
||||
})
|
||||
await grant(30404, 822, 30)
|
||||
await seed({ RoomId: 30405, Name: 'ContribNoRoles', CreatorAccountId: 821, SubRooms: [] })
|
||||
|
||||
const res = await SELF.fetch(`${ORIGIN}/rooms/contributedby/me`, {
|
||||
@@ -515,6 +517,7 @@ describe('rooms endpoints', () => {
|
||||
// The DB is shared across this file, and these are the only player-made public rooms
|
||||
// in it — leaving them behind changes what the `new`/`community` room feeds serve.
|
||||
await env.DB.prepare('DELETE FROM room WHERE room_id BETWEEN 30401 AND 30407').run()
|
||||
await env.DB.prepare('DELETE FROM room_role WHERE room_id BETWEEN 30401 AND 30407').run()
|
||||
})
|
||||
|
||||
it('GET /rooms/:roomId/experience serves the fixed XP settings, no auth', async () => {
|
||||
|
||||
Reference in New Issue
Block a user