From 8b479283e18f3d6f373f3b3cdfb74d1e0a789b54 Mon Sep 17 00:00:00 2001 From: Mark Rapson Date: Tue, 21 Jul 2026 21:14:41 +0100 Subject: [PATCH] Fix schema migration audit columns --- src/db.js | 125 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 43 deletions(-) diff --git a/src/db.js b/src/db.js index 550ea21..3e706cc 100644 --- a/src/db.js +++ b/src/db.js @@ -32,6 +32,69 @@ async function addColumnIfMissing(pool, tableName, columnName, columnDefinition) await pool.query(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${columnName}\` ${columnDefinition}`); } +async function dropColumnIfPresent(pool, tableName, columnName) { + const [rows] = await pool.query( + `SELECT COUNT(*) AS column_count + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = ? + AND column_name = ?`, + [tableName, columnName] + ); + + if (!rows.length || Number(rows[0].column_count) === 0) { + return; + } + + await pool.query(`ALTER TABLE \`${tableName}\` DROP COLUMN \`${columnName}\``); +} + +async function addForeignKeyIfMissing(pool, tableName, columnName, constraintName, referencedTable, referencedColumn, onDeleteAction) { + const [rows] = await pool.query( + `SELECT COUNT(*) AS constraint_count + FROM information_schema.table_constraints + WHERE table_schema = DATABASE() + AND table_name = ? + AND constraint_name = ?`, + [tableName, constraintName] + ); + + if (rows.length && Number(rows[0].constraint_count) > 0) { + return; + } + + await pool.query( + `ALTER TABLE \`${tableName}\` + ADD CONSTRAINT \`${constraintName}\` + FOREIGN KEY (\`${columnName}\`) REFERENCES \`${referencedTable}\`(\`${referencedColumn}\`) + ON DELETE ${onDeleteAction} + ON UPDATE CASCADE` + ); +} + +async function addUserAuditColumns(pool, tableName) { + await addColumnIfMissing(pool, tableName, 'created_by', 'INT NULL'); + await addColumnIfMissing(pool, tableName, 'modified_by', 'INT NULL'); + + await pool.query( + `UPDATE \`${tableName}\` t + LEFT JOIN users created_user ON created_user.id = t.created_by + SET t.created_by = NULL + WHERE t.created_by IS NOT NULL + AND created_user.id IS NULL` + ); + await pool.query( + `UPDATE \`${tableName}\` t + LEFT JOIN users modified_user ON modified_user.id = t.modified_by + SET t.modified_by = NULL + WHERE t.modified_by IS NOT NULL + AND modified_user.id IS NULL` + ); + + await addForeignKeyIfMissing(pool, tableName, 'created_by', `fk_${tableName}_created_by`, 'users', 'id', 'SET NULL'); + await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL'); +} + async function pruneStaleOnboardingDevices(pool) { await pool.query( `DELETE FROM player_onboarding_devices @@ -54,7 +117,7 @@ async function getTableColumnNames(pool, tableName) { } function getPermissionKey(row) { - return String((row && (row.permission_key || row.perm_key)) || '').trim().toLowerCase(); + return String((row && row.permission_key) || '').trim().toLowerCase(); } function getLegacyPermissionTargets(permissionKey) { @@ -81,9 +144,6 @@ function buildPermissionSeedColumns(columnNames) { if (columnNames.has('permission_key')) { columns.push('permission_key'); } - if (columnNames.has('perm_key')) { - columns.push('perm_key'); - } if (columnNames.has('name')) { columns.push('name'); } @@ -104,12 +164,10 @@ function buildPermissionSeedColumns(columnNames) { async function backfillLegacyRbacSchema(pool) { const permissionColumnNames = await getTableColumnNames(pool, 'permissions'); - const hasPermissionKeyColumn = permissionColumnNames.has('permission_key'); - const hasLegacyPermissionKeyColumn = permissionColumnNames.has('perm_key'); - const [permissionRows] = await pool.query('SELECT id, permission_key, perm_key, name, section_name FROM permissions ORDER BY id ASC'); + const [permissionRows] = await pool.query('SELECT id, permission_key, name, section_name FROM permissions ORDER BY id ASC'); const [rolePermissionRows] = await pool.query( - `SELECT rp.role_id, p.permission_key, p.perm_key + `SELECT rp.role_id, p.permission_key FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id` ); @@ -176,9 +234,6 @@ async function backfillLegacyRbacSchema(pool) { if (permissionColumnNames.has('permission_key')) { seedValues.push(permission.key); } - if (permissionColumnNames.has('perm_key')) { - seedValues.push(permission.key); - } if (permissionColumnNames.has('name')) { seedValues.push(permission.name); } @@ -205,9 +260,6 @@ async function backfillLegacyRbacSchema(pool) { if (permissionColumnNames.has('description')) { updateAssignments.push('description = VALUES(description)'); } - if (permissionColumnNames.has('perm_key')) { - updateAssignments.push('perm_key = VALUES(perm_key)'); - } if (permissionColumnNames.has('permission_key')) { updateAssignments.push('permission_key = VALUES(permission_key)'); } @@ -224,7 +276,7 @@ async function backfillLegacyRbacSchema(pool) { await pool.query('DELETE FROM permissions WHERE id IN (?)', [legacyPermissionRowIds]); } - const [currentPermissionRows] = await pool.query('SELECT id, permission_key, perm_key FROM permissions'); + const [currentPermissionRows] = await pool.query('SELECT id, permission_key FROM permissions'); const permissionIdByKey = new Map(); for (const row of currentPermissionRows || []) { const currentKey = getPermissionKey(row); @@ -275,8 +327,7 @@ async function ensureSchema(pool) { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'canvas_sizes', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'canvas_sizes', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'canvas_sizes', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'canvas_sizes'); await pool.query(` CREATE TABLE IF NOT EXISTS playlists ( @@ -289,8 +340,7 @@ async function ensureSchema(pool) { `); await addColumnIfMissing(pool, 'playlists', 'fade_between_slides', 'TINYINT(1) NOT NULL DEFAULT 0'); await addColumnIfMissing(pool, 'playlists', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'playlists', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'playlists', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'playlists'); await pool.query(` CREATE TABLE IF NOT EXISTS slide_templates ( @@ -308,8 +358,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'slide_templates', 'background_image_path', 'VARCHAR(512) NULL'); await addColumnIfMissing(pool, 'slide_templates', 'background_color', 'VARCHAR(32) NULL'); await addColumnIfMissing(pool, 'slide_templates', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'slide_templates', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'slide_templates', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'slide_templates'); await pool.query(` INSERT IGNORE INTO canvas_sizes (name, width, height) VALUES @@ -355,8 +404,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'slide_template_regions', 'font_family', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'slide_template_regions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'slide_template_regions', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'slide_template_regions', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'slide_template_regions'); await pool.query(` CREATE TABLE IF NOT EXISTS slides ( @@ -378,8 +426,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'slides', 'media_path', 'VARCHAR(512) NULL'); await addColumnIfMissing(pool, 'slides', 'media_type', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'slides', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'slides', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'slides'); await pool.query(` CREATE TABLE IF NOT EXISTS playlist_slides ( @@ -409,8 +456,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'playlist_slides', 'schedule_end_time', 'TIME NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'schedule_days_json', 'JSON NULL'); await addColumnIfMissing(pool, 'playlist_slides', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'playlist_slides', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'playlist_slides', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'playlist_slides'); await pool.query(` CREATE TABLE IF NOT EXISTS screens ( @@ -426,8 +472,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'screens', 'playlist_id', 'INT NULL'); await addColumnIfMissing(pool, 'screens', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'screens', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'screens', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'screens'); await pool.query(` CREATE TABLE IF NOT EXISTS player_onboarding_devices ( @@ -442,6 +487,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'player_onboarding_devices', 'client_name', 'VARCHAR(255) NULL'); await addColumnIfMissing(pool, 'player_onboarding_devices', 'screen_id', 'INT NULL'); await addColumnIfMissing(pool, 'player_onboarding_devices', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); + await addUserAuditColumns(pool, 'player_onboarding_devices'); await pool.query(` CREATE TABLE IF NOT EXISTS users ( @@ -460,8 +506,7 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'users', 'password_salt', 'VARCHAR(64) NOT NULL'); await addColumnIfMissing(pool, 'users', 'password_iterations', 'INT NOT NULL'); await addColumnIfMissing(pool, 'users', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'users', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'users', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'users'); await pool.query(` CREATE TABLE IF NOT EXISTS roles ( @@ -476,14 +521,12 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'roles', 'role_key', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'roles', 'description', 'TEXT NULL'); await addColumnIfMissing(pool, 'roles', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'roles', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'roles', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'roles'); await pool.query(` CREATE TABLE IF NOT EXISTS permissions ( id INT AUTO_INCREMENT PRIMARY KEY, permission_key VARCHAR(100) NOT NULL UNIQUE, - perm_key VARCHAR(100) NULL, name VARCHAR(255) NOT NULL, section_name VARCHAR(255) NOT NULL, description TEXT NULL, @@ -491,14 +534,13 @@ async function ensureSchema(pool) { modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await addColumnIfMissing(pool, 'permissions', 'perm_key', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'permissions', 'permission_key', 'VARCHAR(100) NULL'); await addColumnIfMissing(pool, 'permissions', 'name', 'VARCHAR(255) NULL'); await addColumnIfMissing(pool, 'permissions', 'section_name', 'VARCHAR(255) NULL'); await addColumnIfMissing(pool, 'permissions', 'description', 'TEXT NULL'); await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'permissions', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'permissions', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'permissions'); + await dropColumnIfPresent(pool, 'permissions', 'perm_key'); await pool.query(` CREATE TABLE IF NOT EXISTS role_permissions ( @@ -512,8 +554,7 @@ async function ensureSchema(pool) { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'role_permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'role_permissions', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'role_permissions', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'role_permissions'); await pool.query(` CREATE TABLE IF NOT EXISTS user_roles ( @@ -527,8 +568,7 @@ async function ensureSchema(pool) { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await addColumnIfMissing(pool, 'user_roles', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); - await addColumnIfMissing(pool, 'user_roles', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'user_roles', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'user_roles'); await pool.query(` CREATE TABLE IF NOT EXISTS auth_sessions ( @@ -540,8 +580,7 @@ async function ensureSchema(pool) { CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); - await addColumnIfMissing(pool, 'auth_sessions', 'created_by', 'INT NULL'); - await addColumnIfMissing(pool, 'auth_sessions', 'modified_by', 'INT NULL'); + await addUserAuditColumns(pool, 'auth_sessions'); const [userCountRows] = await pool.query('SELECT COUNT(*) AS user_count FROM users'); if (!userCountRows.length || Number(userCountRows[0].user_count) === 0) {