fix(db): guest_emails/lobby ALTERs ran before scheduled_meetings existed + e2e drift
Migration prep. Two real fixes surfaced while building a regression harness:
1. db.js: the `guest_emails` and `lobby` ALTER TABLEs sat at lines 241/244, BEFORE
scheduled_meetings is CREATEd (line 299). On a FRESH database the ALTER fails
(no table yet), is swallowed by the try/catch, and the columns are never added —
so a brand-new deploy is missing them and scheduling with guests crashes. Prod
escaped it only by incremental deploy history. Moved both ALTERs to after the
CREATE. (The upcoming Postgres schema defines every column up front, so this
whole class of ordering bug goes away there.)
2. test/e2e.js: /api/meetings returns paginated `{list, pastTotal, page, pageSize}`
now, not a bare array — updated three `.data.find` → `.data.list.find`.
No prod behaviour change (prod already has the columns; ALTERs are idempotent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-6
@@ -236,12 +236,6 @@ try {
|
||||
SELECT MAX(created_at) FROM messages WHERE messages.sender_id = users.id
|
||||
) WHERE last_seen IS NULL AND EXISTS (SELECT 1 FROM messages WHERE messages.sender_id = users.id)`);
|
||||
} catch (e) { /* messages table may not exist yet on a fresh db */ }
|
||||
// External (non-Connect) invitee emails on a scheduled meeting (#4) — JSON array. They get an emailed
|
||||
// guest join link instead of an in-app invite.
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN guest_emails TEXT'); } catch (e) { /* exists */ }
|
||||
// Lobby (#4): 1 = guests joining by link must be admitted by the host; 0 = they join directly. NULL is
|
||||
// treated as "require approval" (safe default) by the signaling layer.
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN lobby INTEGER'); } catch (e) { /* exists */ }
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_users_bizgaze_user_id ON users(bizgaze_user_id)'); } catch (e) { /* exists */ }
|
||||
// When two accounts merge (#2), the merged-away row is deleted. This records old_id -> survivor so
|
||||
// any lingering reference to the old id (a cached contact, an in-flight DM) resolves to the survivor
|
||||
@@ -320,6 +314,13 @@ try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN cancelled INTEGER NOT N
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN duration_mins INTEGER'); } catch (e) { /* exists */ }
|
||||
// Weekly recurrence: JSON array of weekdays (0=Sun..6=Sat), or null for a one-off.
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN recurrence TEXT'); } catch (e) { /* exists */ }
|
||||
// External (non-Connect) invitee emails on a scheduled meeting (#4) — JSON array. They get an emailed
|
||||
// guest join link instead of an in-app invite. (Must come AFTER the CREATE above — on a fresh DB these
|
||||
// ALTERs previously ran before the table existed and were silently lost.)
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN guest_emails TEXT'); } catch (e) { /* exists */ }
|
||||
// Lobby (#4): 1 = guests joining by link must be admitted by the host; 0 = they join directly. NULL is
|
||||
// treated as "require approval" (safe default) by the signaling layer.
|
||||
try { db.exec('ALTER TABLE scheduled_meetings ADD COLUMN lobby INTEGER'); } catch (e) { /* exists */ }
|
||||
|
||||
// Meeting recordings & transcripts. Video bytes live in recordings/m_<id>.webm, transcript text
|
||||
// in transcripts/m_<id>.txt. Tied to a room (and group/scheduled meeting when applicable) so they
|
||||
|
||||
+3
-3
@@ -289,21 +289,21 @@ function nextMsg(ws, type, timeout = 3000) {
|
||||
const schP = await call('/api/v1/meetings/schedule', { title: 'Synced', scheduledAt: Date.now() + 3600000, participants: [bobId] }, cookie);
|
||||
check('meeting scheduled with participants', schP.status === 200 && Array.isArray(schP.data.participants) && schP.data.participants.includes(bobId));
|
||||
const bobMeetings = await get('/api/v1/meetings', bobCookie);
|
||||
const bm = bobMeetings.data.find((m) => m.id === schP.data.id);
|
||||
const bm = bobMeetings.data.list.find((m) => m.id === schP.data.id);
|
||||
check('invited participant sees the meeting', !!bm && bm.isHost === false);
|
||||
const schPush = await nextMsg(bobWs, 'chat-message');
|
||||
check('schedule announced in the group chat', schPush.message && /Scheduled a call/.test(schPush.message.body));
|
||||
const pastM = await call('/api/v1/meetings/schedule', { group: gid, title: 'Old Standup', scheduledAt: Date.now() - 2 * 60 * 60 * 1000 }, cookie);
|
||||
check('past meeting scheduling is rejected', pastM.status === 400); // can't schedule in the past (#1)
|
||||
const mlist = await get('/api/v1/meetings', cookie);
|
||||
const schUp = mlist.data.find((m) => m.id === sched.data.id);
|
||||
const schUp = mlist.data.list.find((m) => m.id === sched.data.id);
|
||||
check('meetings list buckets upcoming', mlist.status === 200 && schUp && schUp.status === 'upcoming');
|
||||
const sm = wsClient(); await new Promise((r) => sm.on('open', r));
|
||||
sm.send(JSON.stringify({ type: 'meeting-join', room: sched.data.roomCode, name: 'Admin' }));
|
||||
const smJoined = await nextMsg(sm, 'meeting-joined');
|
||||
check('scheduled meeting joinable by code (room created lazily)', !!smJoined.peerId && smJoined.room === sched.data.roomCode);
|
||||
const mlist2 = await get('/api/v1/meetings', cookie);
|
||||
const upRun = mlist2.data.find((m) => m.id === sched.data.id);
|
||||
const upRun = mlist2.data.list.find((m) => m.id === sched.data.id);
|
||||
check('scheduled meeting shows running while a peer is connected', !!upRun && upRun.status === 'running' && upRun.inCall >= 1);
|
||||
sm.close();
|
||||
const cancelBob = await call('/api/v1/meetings/cancel', { id: sched.data.id }, bobCookie);
|
||||
|
||||
Reference in New Issue
Block a user