diff --git a/CHANGELOG.md b/CHANGELOG.md index c53e473..f3065d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## 2.17.5 - 2026-10-23 + +### Changed + +- Added query-aligned database indexes for playlist loading, template and slide lookups, session cleanup, background task claiming, and data-source list ordering. +- Audit log filtering, sorting, and pagination run in SQL; CSV exports still include all matching events. + ## 2.17.4 - 2026-10-23 ### Added diff --git a/build/package.player.json b/build/package.player.json index 3531d50..e46ad91 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.17.4", + "version": "2.17.5", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index a7ceec9..8a32ee8 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.17.4", + "version": "2.17.5", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 93c4322..bbd5923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.17.4", + "version": "2.17.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.17.4", + "version": "2.17.5", "dependencies": { "animate.css": "^4.1.1", "bootstrap-icons": "1.13.1", diff --git a/package.json b/package.json index f035f1f..a9e5578 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.17.4", + "version": "2.17.5", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/db/index.js b/src/db/index.js index 71e5827..776b851 100644 --- a/src/db/index.js +++ b/src/db/index.js @@ -64,7 +64,8 @@ async function ensureSchema(pool, options) { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - modified_by INT NULL + modified_by INT NULL, + INDEX idx_templates_canvas_size (canvas_size_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -86,7 +87,8 @@ async function ensureSchema(pool, options) { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - modified_by INT NULL + modified_by INT NULL, + INDEX idx_template_regions_template_z (template_id, z_index) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -100,7 +102,8 @@ async function ensureSchema(pool, options) { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - modified_by INT NULL + modified_by INT NULL, + INDEX idx_slides_template (template_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -143,7 +146,8 @@ async function ensureSchema(pool, options) { modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, CONSTRAINT fk_playlist_slides_playlist FOREIGN KEY (playlist_id) REFERENCES c_playlists(id) ON DELETE CASCADE, - CONSTRAINT fk_playlist_slides_slide FOREIGN KEY (slide_id) REFERENCES c_slides(id) ON DELETE CASCADE + CONSTRAINT fk_playlist_slides_slide FOREIGN KEY (slide_id) REFERENCES c_slides(id) ON DELETE CASCADE, + INDEX idx_playlist_slides_playlist_position (playlist_id, position) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -240,7 +244,8 @@ async function ensureSchema(pool, options) { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - modified_by INT NULL + modified_by INT NULL, + INDEX idx_rss_feeds_modified_at (modified_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -281,7 +286,8 @@ async function ensureSchema(pool, options) { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by INT NULL, modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - modified_by INT NULL + modified_by INT NULL, + INDEX idx_api_source_groups_modified_at (modified_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` @@ -520,7 +526,8 @@ async function ensureSchema(pool, options) { created_by INT NULL, last_used_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, modified_by INT NULL, - CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES a_users(id) ON DELETE CASCADE + CONSTRAINT fk_auth_sessions_user FOREIGN KEY (user_id) REFERENCES a_users(id) ON DELETE CASCADE, + INDEX idx_sessions_expires_at (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); @@ -551,7 +558,7 @@ async function ensureSchema(pool, options) { started_at TIMESTAMP NULL, finished_at TIMESTAMP NULL, error_message MEDIUMTEXT NULL, - INDEX idx_background_tasks_status (status), + INDEX idx_background_tasks_status_created_at (status, created_at), INDEX idx_background_tasks_key (task_key), INDEX idx_background_tasks_type (task_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci diff --git a/src/db/migrations.js b/src/db/migrations.js index 5aeca72..3564e2c 100644 --- a/src/db/migrations.js +++ b/src/db/migrations.js @@ -746,6 +746,21 @@ const VERSIONED_MIGRATIONS = [ ); await pool.query('DELETE FROM a_permissions WHERE permission_key = ?', ['dashboard.allow']); } + }, + { + version: '2.17.5', + label: 'v2.17.5 query indexes', + run: async function (pool) { + await ensureIndex(pool, 'c_playlist_slides', 'idx_playlist_slides_playlist_position', 'playlist_id, position'); + await ensureIndex(pool, 'c_template_regions', 'idx_template_regions_template_z', 'template_id, z_index'); + await ensureIndex(pool, 'c_slides', 'idx_slides_template', 'template_id'); + await ensureIndex(pool, 'c_templates', 'idx_templates_canvas_size', 'canvas_size_id'); + await ensureIndex(pool, 'a_sessions', 'idx_sessions_expires_at', 'expires_at'); + await ensureIndex(pool, 'o_background_tasks', 'idx_background_tasks_status_created_at', 'status, created_at'); + await ensureIndex(pool, 'i_rss_feeds', 'idx_rss_feeds_modified_at', 'modified_at'); + await ensureIndex(pool, 'i_api_source_groups', 'idx_api_source_groups_modified_at', 'modified_at'); + await dropIndexIfExists(pool, 'o_background_tasks', 'idx_background_tasks_status'); + } } ]; @@ -896,6 +911,38 @@ async function ensureForeignKey(pool, tableName, constraintName, columnName, ref } } +async function ensureIndex(pool, tableName, indexName, columns) { + const [rows] = await pool.query( + `SELECT COUNT(*) AS index_count + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND INDEX_NAME = ?`, + [tableName, indexName] + ); + + if (Number(rows && rows[0] && rows[0].index_count) > 0) { + return; + } + + await pool.query('ALTER TABLE ' + tableName + ' ADD INDEX ' + indexName + ' (' + columns + ')'); +} + +async function dropIndexIfExists(pool, tableName, indexName) { + const [rows] = await pool.query( + `SELECT COUNT(*) AS index_count + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = ? + AND INDEX_NAME = ?`, + [tableName, indexName] + ); + + if (Number(rows && rows[0] && rows[0].index_count) > 0) { + await pool.query('ALTER TABLE ' + tableName + ' DROP INDEX ' + indexName); + } +} + async function dropForeignKeyIfExists(pool, tableName, columnName) { const [rows] = await pool.query( `SELECT CONSTRAINT_NAME AS constraint_name diff --git a/src/web/routes/settings/audit-log.js b/src/web/routes/settings/audit-log.js index 742c025..ac640c8 100644 --- a/src/web/routes/settings/audit-log.js +++ b/src/web/routes/settings/audit-log.js @@ -1,10 +1,29 @@ const { renderView } = require('#src/web/view'); const { hasPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac'); const { AUDIT_CATEGORY_KEYS, formatUserAgentLabel } = require('#src/data/audit-log'); +const { buildSearchFilter } = require('#src/data/utils'); const { buildPagination } = require('#src/web/lib/pagination'); -const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query'); +const { getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber } = require('#src/web/lib/list-query'); const PAGE_SIZE = 25; +const AUDIT_FROM_SQL = `FROM o_audit_events e + LEFT JOIN a_users u ON u.id = e.actor_user_id`; +const AUDIT_TARGET_LABEL_SQL = "CASE WHEN COALESCE(e.target_label, '') <> '' THEN e.target_label WHEN e.target_type IS NOT NULL AND e.target_id IS NOT NULL THEN CONCAT(e.target_type, ' #', e.target_id) ELSE '' END"; +const AUDIT_SEARCH_COLUMNS = [ + 'e.category', + 'e.event_type', + "COALESCE(NULLIF(u.name, ''), u.username, 'System')", + AUDIT_TARGET_LABEL_SQL, + 'e.ip_address', + 'e.user_agent' +]; +const AUDIT_SORT_COLUMNS = { + time: 'e.occurred_at', + event: "REPLACE(REPLACE(REPLACE(e.event_type, '.', ' '), '_', ' '), '-', ' ')", + actor: "COALESCE(NULLIF(u.name, ''), u.username, 'System')", + target: AUDIT_TARGET_LABEL_SQL, + category: 'e.category' +}; const SORTED_AUDIT_CATEGORY_KEYS = AUDIT_CATEGORY_KEYS.slice().sort(function (left, right) { return String(left).localeCompare(String(right)); @@ -168,35 +187,78 @@ function mapAuditRow(row, formatDashboardDate) { }); } -function filterAuditRows(rows, req, formatDashboardDate) { +function buildAuditFilters(req) { const category = String(req.query.category || '').trim().toLowerCase(); const eventType = String(req.query.event_type || '').trim().toLowerCase(); const search = getSearchQuery(req); const requestedSort = getSortQuery(req); - const sort = requestedSort || 'time'; + const sort = Object.prototype.hasOwnProperty.call(AUDIT_SORT_COLUMNS, requestedSort) ? requestedSort : 'time'; const direction = requestedSort ? getSortDirectionQuery(req) : 'desc'; - const mappedRows = (rows || []).map(function (row) { return mapAuditRow(row, formatDashboardDate); }); - const filteredRows = sortRows(mappedRows.filter(function (event) { - return (!category || event.category === category) && (!eventType || event.event_type === eventType); - }).filter(createSearchMatcher(search, ['category', 'event_type', 'actorLabel', 'targetLabelDisplay', 'ip_address', 'user_agent'])), function (event) { - if (sort === 'event') return event.eventLabel; - if (sort === 'actor') return event.actorLabel; - if (sort === 'target') return event.targetLabelDisplay; - if (sort === 'category') return event.category; - return event.occurred_at; - }, direction); - return { rows: filteredRows, category: category, eventType: eventType, search: search, sort: sort, direction: direction }; + const whereClauses = []; + const params = []; + + if (category) { + whereClauses.push('e.category = ?'); + params.push(category); + } + if (eventType) { + whereClauses.push('e.event_type = ?'); + params.push(eventType); + } + + const searchFilter = buildSearchFilter(AUDIT_SEARCH_COLUMNS, search); + if (searchFilter.clause) { + whereClauses.push(searchFilter.clause.replace(/^\s*WHERE\s+/i, '')); + params.push(...searchFilter.params); + } + + return { + category: category, + eventType: eventType, + search: search, + sort: sort, + direction: direction, + whereSql: whereClauses.length ? ' WHERE ' + whereClauses.join(' AND ') : '', + params: params + }; } -async function fetchAuditRows(pool) { +function buildAuditOrderBy(filters) { + const sortExpression = AUDIT_SORT_COLUMNS[filters.sort] || AUDIT_SORT_COLUMNS.time; + const tieBreakers = filters.sort === 'time' ? 'e.id DESC' : 'e.occurred_at DESC, e.id DESC'; + return ' ORDER BY ' + sortExpression + ' ' + filters.direction.toUpperCase() + ', ' + tieBreakers; +} + +async function fetchAuditCount(pool, filters) { const [rows] = await pool.query( - `SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id, + 'SELECT COUNT(*) AS count ' + AUDIT_FROM_SQL + filters.whereSql, + filters.params + ); + return Number(rows && rows[0] && rows[0].count) || 0; +} + +async function fetchAuditEventTypes(pool, filters) { + const [rows] = await pool.query( + 'SELECT DISTINCT e.event_type AS event_type ' + AUDIT_FROM_SQL + filters.whereSql + (filters.whereSql ? ' AND ' : ' WHERE ') + "e.event_type IS NOT NULL AND e.event_type <> '' ORDER BY e.event_type ASC", + filters.params + ); + return (rows || []).map(function (row) { + return String(row.event_type || '').trim(); + }).filter(Boolean); +} + +async function fetchAuditRows(pool, filters, page) { + let sql = `SELECT e.id, e.occurred_at, e.category, e.event_type, e.target_type, e.target_id, e.target_label, e.ip_address, e.user_agent, e.details_json, u.name AS actor_name, u.username AS actor_username - FROM o_audit_events e - LEFT JOIN a_users u ON u.id = e.actor_user_id - ORDER BY e.occurred_at DESC, e.id DESC`, [] - ); + ${AUDIT_FROM_SQL}${filters.whereSql}${buildAuditOrderBy(filters)}`; + const params = filters.params.slice(); + if (page) { + sql += ' LIMIT ? OFFSET ?'; + params.push(page.limit, page.offset); + } + + const [rows] = await pool.query(sql, params); return rows || []; } @@ -214,27 +276,28 @@ module.exports = function registerAuditLogRoutes(app, deps) { const search = getSearchQuery(req); const sort = getSortQuery(req); const direction = getSortDirectionQuery(req); - const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate); - const filteredEvents = filtered.rows; - const eventTypes = Array.from(new Set(filtered.rows.map(function (event) { - return String(event.event_type || '').trim(); - }).filter(Boolean))).sort(); - const exportQuery = new URLSearchParams({ - category: filtered.category, - event_type: filtered.eventType, - search: filtered.search, - sort: filtered.sort, - direction: filtered.direction - }).toString(); - const pagination = buildPagination(filteredEvents.length, parsePageNumber(req.query.page), 'page', { + const filters = buildAuditFilters(req); + const totalEvents = await fetchAuditCount(pool, filters); + const pagination = buildPagination(totalEvents, parsePageNumber(req.query.page), 'page', { search: search, sort: sort, direction: direction, category: category, event_type: eventType }, PAGE_SIZE, 'events', 'Audit event pages'); - const startIndex = (pagination.currentPage - 1) * PAGE_SIZE; - const events = filteredEvents.slice(startIndex, startIndex + PAGE_SIZE); + const rawEvents = await fetchAuditRows(pool, filters, { + limit: PAGE_SIZE, + offset: (pagination.currentPage - 1) * PAGE_SIZE + }); + const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); }); + const eventTypes = await fetchAuditEventTypes(pool, filters); + const exportQuery = new URLSearchParams({ + category: filters.category, + event_type: filters.eventType, + search: filters.search, + sort: filters.sort, + direction: filters.direction + }).toString(); res.send(renderView('settings/audit-log/index', { title: 'Audit log', active: 'audit-log', @@ -257,11 +320,13 @@ module.exports = function registerAuditLogRoutes(app, deps) { app.get('/settings/audit-log/export', requireAuditExportAccess(deps.setAuthMessageCookie), async function (req, res, next) { try { - const filtered = filterAuditRows(await fetchAuditRows(pool), req, formatDashboardDate); + const filters = buildAuditFilters(req); + const rawEvents = await fetchAuditRows(pool, filters); + const events = rawEvents.map(function (row) { return mapAuditRow(row, formatDashboardDate); }); const lines = [ ['Occurred At (UTC)', 'Category', 'Event Type', 'Actor', 'Target Type', 'Target ID', 'Target', 'IP Address', 'User Agent', 'Details'].map(csvCell).join(',') ]; - filtered.rows.forEach(function (event) { + events.forEach(function (event) { lines.push([ event.occurredAtValue, event.category, diff --git a/test/audit-log-route.test.js b/test/audit-log-route.test.js index fed743a..44b14d4 100644 --- a/test/audit-log-route.test.js +++ b/test/audit-log-route.test.js @@ -7,6 +7,7 @@ const registerAuditLogRoutes = require('../src/web/routes/settings/audit-log'); test('audit log uses its own read permission and shared pagination partial', async () => { const handlers = {}; + const queries = []; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; @@ -15,8 +16,16 @@ test('audit log uses its own read permission and shared pagination partial', asy registerAuditLogRoutes(app, { pool: { - async query(sql) { - if (String(sql).includes('FROM o_audit_events')) { + async query(sql, params) { + const query = String(sql); + queries.push([query, params || []]); + if (query.includes('SELECT COUNT(*) AS count')) { + return [[{ count: 2 }]]; + } + if (query.includes('SELECT DISTINCT e.event_type')) { + return [[{ event_type: 'login.failed' }, { event_type: 'login.success' }]]; + } + if (query.includes('FROM o_audit_events')) { return [[ { id: 1, @@ -102,6 +111,12 @@ test('audit log uses its own read permission and shared pagination partial', asy assert.ok(response.body.indexOf('login.success') < response.body.indexOf('login.failed')); assert.match(response.body, />Export sql.includes('LIMIT ? OFFSET ?')); + assert.ok(pageQuery); + assert.match(pageQuery[0], /ORDER BY e\.occurred_at DESC, e\.id DESC LIMIT \? OFFSET \?/); + assert.deepEqual(pageQuery[1].slice(-2), [25, 0]); + assert.ok(queries.some(([sql]) => sql.includes('SELECT COUNT(*) AS count'))); + assert.ok(queries.some(([sql]) => sql.includes('SELECT DISTINCT e.event_type') && /WHERE e\.event_type IS NOT NULL/.test(sql))); const exportResponse = { headers: {}, @@ -114,4 +129,59 @@ test('audit log uses its own read permission and shared pagination partial', asy assert.match(exportResponse.headers['Content-Type'], /text\/csv/); assert.match(exportResponse.body, /Occurred At \(UTC\)/); assert.match(exportResponse.body, /login.success/); + const exportQuery = queries.filter(([sql]) => sql.includes('FROM o_audit_events') && !sql.includes('SELECT COUNT(*)') && !sql.includes('SELECT DISTINCT')).at(-1); + assert.ok(exportQuery); + assert.doesNotMatch(exportQuery[0], /LIMIT \?/); +}); + +test('audit log applies SQL filters, escaped search, and selected sort before pagination', async () => { + const handlers = {}; + const queries = []; + const app = { + get(path, ...routeHandlers) { + handlers[path] = routeHandlers; + } + }; + + registerAuditLogRoutes(app, { + pool: { + async query(sql, params) { + const query = String(sql); + queries.push([query, params || []]); + if (query.includes('SELECT COUNT(*) AS count')) return [[{ count: 40 }]]; + return [[]]; + } + }, + formatDashboardDate() { + return ''; + } + }); + + await handlers['/settings/audit-log'][1]({ + currentUser: { id: 7, permissions: ['audit-log.read'] }, + query: { + category: 'Authentication', + event_type: 'Login.Success', + search: ' Alice%_ ', + sort: 'actor', + direction: 'desc', + page: '2' + } + }, { send() {} }, function (error) { + throw error; + }); + + const countQuery = queries.find(([sql]) => sql.includes('SELECT COUNT(*) AS count')); + const pageQuery = queries.find(([sql]) => sql.includes('LIMIT ? OFFSET ?')); + const eventTypesQuery = queries.find(([sql]) => sql.includes('SELECT DISTINCT e.event_type')); + assert.ok(countQuery); + assert.ok(pageQuery); + assert.ok(eventTypesQuery); + assert.match(countQuery[0], /e\.category = \? AND e\.event_type = \?/); + assert.match(countQuery[0], /LOWER\(COALESCE\(CAST\(e\.category AS CHAR\)/); + assert.deepEqual(countQuery[1].slice(0, 2), ['authentication', 'login.success']); + assert.ok(countQuery[1].includes('%alice\\%\\_%')); + assert.match(pageQuery[0], /ORDER BY COALESCE\(NULLIF\(u\.name, ''\), u\.username, 'System'\) DESC, e\.occurred_at DESC, e\.id DESC/); + assert.deepEqual(pageQuery[1].slice(-2), [25, 25]); + assert.match(eventTypesQuery[0], /AND e\.event_type IS NOT NULL/); }); \ No newline at end of file diff --git a/test/schema-update-log.test.js b/test/schema-update-log.test.js index 1fa0f3c..15e1325 100644 --- a/test/schema-update-log.test.js +++ b/test/schema-update-log.test.js @@ -49,6 +49,37 @@ test('pending migrations are empty when the schema already matches the app versi assert.equal(pendingMigrations.length, 0); }); +test('query index migration upgrades a 2.17.4 installation', async () => { + const queries = []; + const pool = { + queries, + async query(sql, params) { + queries.push([sql, params]); + if (String(sql).includes('FROM information_schema.STATISTICS')) { + return [[{ index_count: params[1] === 'idx_background_tasks_status' ? 1 : 0 }]]; + } + return [[]]; + } + }; + const pending = await getPendingMigrations(pool, { currentVersion: '2.17.4' }); + const indexMigration = pending.find(migration => migration.version === '2.17.5'); + + assert.ok(indexMigration); + await indexMigration.run(pool); + + const alterations = queries.map(([sql]) => String(sql)).filter(sql => sql.startsWith('ALTER TABLE')); + assert.equal(alterations.length, 9); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_playlist_slides_playlist_position'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_template_regions_template_z'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_slides_template'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_templates_canvas_size'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_sessions_expires_at'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_background_tasks_status_created_at'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_rss_feeds_modified_at'))); + assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_api_source_groups_modified_at'))); + assert.ok(alterations.some(sql => sql.includes('DROP INDEX idx_background_tasks_status'))); +}); + test('playlist shuffle migration upgrades an existing 2.15.0 installation', async () => { const pool = createPool([]); const pending = await getPendingMigrations(pool, { currentVersion: '2.15.0' }); diff --git a/test/timetable-migrations.test.js b/test/timetable-migrations.test.js index c4b951c..f2cae1a 100644 --- a/test/timetable-migrations.test.js +++ b/test/timetable-migrations.test.js @@ -161,8 +161,9 @@ test('2.17.0 migration adds the expiring view-as session columns', async () => { ]); const pending = await getPendingMigrations(pool, { currentVersion: '2.16.2' }); - assert.deepEqual(pending.map(function (migration) { return migration.version; }), ['2.17.0', '2.17.1']); - await pending[0].run(pool); + const viewAsMigration = pending.find(function (migration) { return migration.version === '2.17.0'; }); + assert.ok(viewAsMigration); + await viewAsMigration.run(pool); assert.ok(pool.queries.some(function ([sql]) { return String(sql).includes('ALTER TABLE a_sessions ADD COLUMN view_as_user_id INT NULL AFTER user_id'); @@ -189,8 +190,9 @@ test('2.17.1 migration removes obsolete dashboard.allow grants and permission', ]); const pending = await getPendingMigrations(pool, { currentVersion: '2.17.0' }); - assert.deepEqual(pending.map(function (migration) { return migration.version; }), ['2.17.1']); - await pending[0].run(pool); + const obsoletePermissionMigration = pending.find(function (migration) { return migration.version === '2.17.1'; }); + assert.ok(obsoletePermissionMigration); + await obsoletePermissionMigration.run(pool); assert.ok(pool.queries.some(function ([sql, params]) { return String(sql).includes('DELETE role_permissions') && params[0] === 'dashboard.allow';