From b2c4b10e41421e1457ed58e0fe53b3277e87ad92 Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 24 Jul 2026 19:43:46 +0530 Subject: [PATCH] fix(db): guest_emails/lobby ALTERs ran before scheduled_meetings existed + e2e drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/db.js | 13 +++++++------ server/test/e2e.js | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/server/db.js b/server/db.js index 3ee6c67..904ce49 100644 --- a/server/db.js +++ b/server/db.js @@ -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_.webm, transcript text // in transcripts/m_.txt. Tied to a room (and group/scheduled meeting when applicable) so they diff --git a/server/test/e2e.js b/server/test/e2e.js index 29b8ade..5ba36eb 100644 --- a/server/test/e2e.js +++ b/server/test/e2e.js @@ -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);