Release v2.17.5
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m11s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-23 21:23:43 +01:00
1 parent 46636939de
commit 86b9609ea7
11 files changed
+285 -56

No files matched your search

+7
View File
@@ -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
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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": {
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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": {
+15 -8
View File
@@ -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
+47
View File
@@ -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
+102 -37
View File
@@ -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);
}
async function fetchAuditRows(pool) {
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
};
}
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,
+72 -2
View File
@@ -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</);
assert.ok(handlers['/settings/audit-log/export']);
const pageQuery = queries.find(([sql]) => 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/);
});
+31
View File
@@ -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' });
+6 -4
View File
@@ -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';