diff --git a/package.json b/package.json index 0c84aa0..942715a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "1.4.1", + "version": "1.4.2", "private": false, "description": "Pulse Signage application with MySQL and media uploads", "repository": { diff --git a/src/db.js b/src/db.js index 3e706cc..258c185 100644 --- a/src/db.js +++ b/src/db.js @@ -95,6 +95,79 @@ async function addUserAuditColumns(pool, tableName) { await addForeignKeyIfMissing(pool, tableName, 'modified_by', `fk_${tableName}_modified_by`, 'users', 'id', 'SET NULL'); } +async function hasSingleColumnUniqueIndex(pool, tableName, columnName) { + const [rows] = await pool.query( + `SELECT INDEX_NAME, COUNT(*) AS column_count + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = ? + AND non_unique = 0 + AND column_name = ? + GROUP BY INDEX_NAME`, + [tableName, columnName] + ); + + return (rows || []).some(function (row) { + return Number(row.column_count) === 1; + }); +} + +async function addUniqueIndexIfMissing(pool, tableName, columnName, indexName) { + const hasUniqueIndex = await hasSingleColumnUniqueIndex(pool, tableName, columnName); + if (hasUniqueIndex) { + return; + } + + await pool.query(`ALTER TABLE \`${tableName}\` ADD UNIQUE KEY \`${indexName}\` (\`${columnName}\`)`); +} + +async function dedupePermissionRows(pool) { + const [rows] = await pool.query('SELECT id, permission_key FROM permissions ORDER BY id ASC'); + const canonicalIdByKey = new Map(); + const duplicateRowsByKey = new Map(); + + for (const row of rows || []) { + const permissionKey = getPermissionKey(row); + const permissionId = Number(row.id); + if (!permissionKey || !Number.isInteger(permissionId) || permissionId <= 0) { + continue; + } + + if (!canonicalIdByKey.has(permissionKey)) { + canonicalIdByKey.set(permissionKey, permissionId); + continue; + } + + if (!duplicateRowsByKey.has(permissionKey)) { + duplicateRowsByKey.set(permissionKey, []); + } + duplicateRowsByKey.get(permissionKey).push(permissionId); + } + + if (!duplicateRowsByKey.size) { + return; + } + + for (const [permissionKey, duplicateIds] of duplicateRowsByKey.entries()) { + const canonicalId = canonicalIdByKey.get(permissionKey); + for (const duplicateId of duplicateIds) { + await pool.query( + 'UPDATE IGNORE role_permissions SET permission_id = ? WHERE permission_id = ?', + [canonicalId, duplicateId] + ); + } + } + + const duplicateIds = []; + for (const duplicateList of duplicateRowsByKey.values()) { + duplicateIds.push.apply(duplicateIds, duplicateList); + } + + if (duplicateIds.length) { + await pool.query('DELETE FROM permissions WHERE id IN (?)', [duplicateIds]); + } +} + async function pruneStaleOnboardingDevices(pool) { await pool.query( `DELETE FROM player_onboarding_devices @@ -541,6 +614,8 @@ async function ensureSchema(pool) { await addColumnIfMissing(pool, 'permissions', 'modified_at', 'TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); await addUserAuditColumns(pool, 'permissions'); await dropColumnIfPresent(pool, 'permissions', 'perm_key'); + await dedupePermissionRows(pool); + await addUniqueIndexIfMissing(pool, 'permissions', 'permission_key', 'uq_permissions_permission_key'); await pool.query(` CREATE TABLE IF NOT EXISTS role_permissions (