#!/usr/bin/env node /** * One-shot, idempotent migration for. * * Adds the `executive_meetings.access` permission, grants it to admin + all * executive_* roles, and links it to the `executive-meetings` app via * `app_permissions` so the home-screen icon is hidden from users who don't * have an executive role. * * Safe to run multiple times — every insert uses ON CONFLICT DO NOTHING. * * Usage (from repo root): * DATABASE_URL=... node artifacts/api-server/scripts/gate-executive-meetings.mjs */ import pg from "pg"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error("DATABASE_URL must be set"); process.exit(1); } const pool = new pg.Pool({ connectionString: DATABASE_URL }); const EXEC_ROLE_NAMES = [ "admin", "executive_ceo", "executive_office_manager", "executive_coord_lead", "executive_coordinator", "executive_viewer", ]; async function main() { const client = await pool.connect(); try { await client.query("BEGIN"); // 1) Permission row. await client.query( `INSERT INTO permissions (name, description_ar, description_en) VALUES ($1, $2, $3) ON CONFLICT (name) DO NOTHING`, [ "executive_meetings.access", "الوصول إلى وحدة الاجتماعات التنفيذية", "Access the Executive Meetings module", ], ); const permRes = await client.query( `SELECT id FROM permissions WHERE name = $1`, ["executive_meetings.access"], ); if (permRes.rowCount === 0) { throw new Error("Failed to find executive_meetings.access permission"); } const permId = permRes.rows[0].id; // 2) Role grants. const grantRes = await client.query( `INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, $1 FROM roles r WHERE r.name = ANY($2::text[]) ON CONFLICT DO NOTHING RETURNING role_id`, [permId, EXEC_ROLE_NAMES], ); // 3) App link. const appRes = await client.query( `SELECT id FROM apps WHERE slug = $1`, ["executive-meetings"], ); if (appRes.rowCount === 0) { throw new Error( "executive-meetings app not found in the apps table; aborting", ); } const appId = appRes.rows[0].id; const appLinkRes = await client.query( `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING app_id`, [appId, permId], ); await client.query("COMMIT"); // Final state for the operator. const totalGrants = await client.query( `SELECT COUNT(*)::int AS c FROM role_permissions rp JOIN roles r ON r.id = rp.role_id WHERE rp.permission_id = $1 AND r.name = ANY($2::text[])`, [permId, EXEC_ROLE_NAMES], ); const totalLinks = await client.query( `SELECT COUNT(*)::int AS c FROM app_permissions WHERE permission_id = $1`, [permId], ); console.log( JSON.stringify( { ok: true, permissionId: permId, executiveAppId: appId, newRoleGrantsThisRun: grantRes.rowCount ?? 0, newAppLinksThisRun: appLinkRes.rowCount ?? 0, executiveRoleGrantsTotal: totalGrants.rows[0].c, appPermissionLinksTotal: totalLinks.rows[0].c, }, null, 2, ), ); } catch (err) { await client.query("ROLLBACK").catch(() => {}); throw err; } finally { client.release(); await pool.end(); } } main().catch((err) => { console.error(err); process.exit(1); });