screentinker/scripts/parity-multitenancy.js

101 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
// Phase 1 multitenancy parity check.
//
// Compares per-user resource counts between the pre-migration snapshot and
// the current DB. Every row must end up in exactly one workspace owned by
// the original user. Drift = bug.
//
// Usage:
// node scripts/parity-multitenancy.js
//
// Exits non-zero on any drift.
'use strict';
const path = require('path');
const SERVER_DIR = path.resolve(__dirname, '..', 'server');
process.chdir(SERVER_DIR);
const Database = require(require.resolve('better-sqlite3', { paths: [SERVER_DIR] }));
const config = require(path.join(SERVER_DIR, 'config'));
const PRE = path.resolve(SERVER_DIR, 'db', 'remote_display.pre-multitenancy.db');
const POST = config.dbPath;
console.log(`[parity] pre = ${PRE}`);
console.log(`[parity] post = ${POST}`);
const pre = new Database(PRE, { readonly: true });
const post = new Database(POST, { readonly: true });
const TABLES = ['devices','content','playlists','layouts','widgets','schedules','video_walls','device_groups','white_labels','kiosk_pages','alert_configs'];
const users = pre.prepare('SELECT id, email FROM users').all();
let pass = 0, fail = 0;
console.log('\n--- per-user, per-table row counts ---');
for (const u of users) {
for (const t of TABLES) {
const preN = pre.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id = ?`).get(u.id).n;
// Post: rows belonging to any workspace owned by an org owned by this user.
let postN;
try {
postN = post.prepare(`
SELECT COUNT(*) AS n FROM ${t} r
WHERE r.workspace_id IN (
SELECT w.id FROM workspaces w
JOIN organizations o ON w.organization_id = o.id
WHERE o.owner_user_id = ?
)
`).get(u.id).n;
} catch (e) {
postN = '<no workspace_id col>';
}
const ok = preN === postN;
if (preN === 0 && postN === 0) continue;
console.log(` ${u.email.padEnd(30)} ${t.padEnd(16)} pre=${String(preN).padStart(4)} post=${String(postN).padStart(4)} ${ok ? 'PASS' : 'FAIL'}`);
if (ok) pass++; else fail++;
}
}
console.log('\n--- platform-wide totals ---');
const totalsPre = {};
const totalsPost = {};
for (const t of TABLES) {
totalsPre[t] = pre .prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
totalsPost[t] = post.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
const ok = totalsPre[t] === totalsPost[t];
console.log(` ${t.padEnd(16)} pre=${String(totalsPre[t]).padStart(4)} post=${String(totalsPost[t]).padStart(4)} ${ok ? 'PASS' : 'FAIL'}`);
if (ok) pass++; else fail++;
}
console.log('\n--- new tables populated ---');
const newTables = ['organizations', 'organization_members', 'workspaces', 'workspace_members'];
for (const t of newTables) {
const n = post.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get().n;
console.log(` ${t.padEnd(24)} rows=${n}`);
}
console.log('\n--- orphan check: rows with NON-NULL user_id but NULL workspace_id ---');
console.log('(rows with NULL user_id are unclaimed devices or platform templates - expected NULL workspace_id)');
for (const t of TABLES) {
try {
const realOrphans = post.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id IS NOT NULL AND workspace_id IS NULL`).get().n;
const expectedNulls = post.prepare(`SELECT COUNT(*) AS n FROM ${t} WHERE user_id IS NULL`).get().n;
const tag = realOrphans > 0 ? 'FAIL' : 'PASS';
console.log(` ${t.padEnd(16)} bug_orphans=${realOrphans} expected_nulls(user_id IS NULL)=${expectedNulls} ${tag}`);
if (realOrphans > 0) fail++; else pass++;
} catch { /* no column */ }
}
console.log('\n--- role migration ---');
const sa = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'superadmin'`).get().n;
const pa = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'platform_admin'`).get().n;
const adm = post.prepare(`SELECT COUNT(*) AS n FROM users WHERE role = 'admin'`).get().n;
console.log(` superadmin (should be 0) : ${sa}`);
console.log(` platform_admin (should be > 0 if any pre had superadmin): ${pa}`);
console.log(` admin (should be 0) : ${adm}`);
if (sa === 0 && adm === 0) pass++; else fail++;
console.log(`\n--- summary: ${pass} pass, ${fail} fail ---`);
pre.close(); post.close();
process.exit(fail === 0 ? 0 : 1);