Release v2.6.20
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.6.20 - 2026-08-09
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Startup now logs the previously detected schema version, the current app version, and whether migrations are required.
|
||||||
|
|
||||||
## 2.6.19 - 2026-08-09
|
## 2.6.19 - 2026-08-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.6.19",
|
"version": "2.6.20",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"main": "src/common.js",
|
"main": "src/common.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.6.19",
|
"version": "2.6.20",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"main": "src/common.js",
|
"main": "src/common.js",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.6.19",
|
"version": "2.6.20",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
+10
-2
@@ -1,3 +1,6 @@
|
|||||||
|
const { version: appVersion } = require('#root/package.json');
|
||||||
|
const { compareVersions, detectSchemaVersion, recordSchemaVersion, runMigrations } = require('./migrations');
|
||||||
|
|
||||||
// Snapshot only: keep this file aligned with the current schema state.
|
// Snapshot only: keep this file aligned with the current schema state.
|
||||||
async function ensureSchema(pool, options) {
|
async function ensureSchema(pool, options) {
|
||||||
const schemaLockName = 'pulse_signage_schema_lock';
|
const schemaLockName = 'pulse_signage_schema_lock';
|
||||||
@@ -12,6 +15,11 @@ async function ensureSchema(pool, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const currentVersion = await detectSchemaVersion(pool);
|
||||||
|
const updateRequired = compareVersions(currentVersion, appVersion) < 0;
|
||||||
|
|
||||||
|
console.info('[schema] previous=' + currentVersion + ' current=' + appVersion + ' update=' + (updateRequired ? 'yes' : 'no'));
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS c_canvas_sizes (
|
CREATE TABLE IF NOT EXISTS c_canvas_sizes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -384,8 +392,8 @@ async function ensureSchema(pool, options) {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const { runMigrations } = require('./migrations');
|
await runMigrations(pool, Object.assign({}, options, { currentVersion: currentVersion }));
|
||||||
await runMigrations(pool, options);
|
await recordSchemaVersion(pool, appVersion);
|
||||||
} finally {
|
} finally {
|
||||||
await pool.query('SELECT RELEASE_LOCK(?)', [schemaLockName]).catch(function () {
|
await pool.query('SELECT RELEASE_LOCK(?)', [schemaLockName]).catch(function () {
|
||||||
});
|
});
|
||||||
|
|||||||
+75
-7
@@ -1,5 +1,7 @@
|
|||||||
const { version: appVersion } = require('#root/package.json');
|
const { version: appVersion } = require('#root/package.json');
|
||||||
const TIMETABLE_TIME_ZONE = 'Europe/London';
|
const TIMETABLE_TIME_ZONE = 'Europe/London';
|
||||||
|
const APP_STATE_TABLE = 'o_app_state';
|
||||||
|
const APP_STATE_SCHEMA_VERSION_KEY = 'schema_version';
|
||||||
|
|
||||||
const VERSIONED_MIGRATIONS = [
|
const VERSIONED_MIGRATIONS = [
|
||||||
{
|
{
|
||||||
@@ -301,23 +303,42 @@ const VERSIONED_MIGRATIONS = [
|
|||||||
version: '2.6.16',
|
version: '2.6.16',
|
||||||
label: 'v2.6.16 timetable timezone schema',
|
label: 'v2.6.16 timetable timezone schema',
|
||||||
run: async function (pool) {
|
run: async function (pool) {
|
||||||
await ensureColumn(pool, 'i_schedule_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
|
||||||
|
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
|
||||||
|
const tableName = timetableGroupsExists ? 'i_timetable_groups' : scheduleGroupsExists ? 'i_schedule_groups' : null;
|
||||||
|
|
||||||
|
if (!tableName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureColumn(pool, tableName, 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
version: '2.6.17',
|
version: '2.6.17',
|
||||||
label: 'v2.6.17 timetable europe/london conversion',
|
label: 'v2.6.17 timetable europe/london conversion',
|
||||||
run: async function (pool) {
|
run: async function (pool) {
|
||||||
await ensureColumn(pool, 'i_schedule_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
|
||||||
|
const timetableEntriesExists = await tableExists(pool, 'i_timetable_entries');
|
||||||
|
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
|
||||||
|
const scheduleEntriesExists = await tableExists(pool, 'i_schedule_entries');
|
||||||
|
const groupTableName = timetableGroupsExists ? 'i_timetable_groups' : scheduleGroupsExists ? 'i_schedule_groups' : null;
|
||||||
|
const entryTableName = timetableEntriesExists ? 'i_timetable_entries' : scheduleEntriesExists ? 'i_schedule_entries' : null;
|
||||||
|
|
||||||
await pool.query('UPDATE i_schedule_groups SET timezone = ?', [TIMETABLE_TIME_ZONE]);
|
if (!groupTableName || !entryTableName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [rows] = await pool.query('SELECT id, start_datetime, end_datetime FROM i_schedule_entries ORDER BY id ASC');
|
await ensureColumn(pool, groupTableName, 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
||||||
|
|
||||||
|
await pool.query('UPDATE ' + groupTableName + ' SET timezone = ?', [TIMETABLE_TIME_ZONE]);
|
||||||
|
|
||||||
|
const [rows] = await pool.query('SELECT id, start_datetime, end_datetime FROM ' + entryTableName + ' ORDER BY id ASC');
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const startDate = convertMigrationDateTimeFromTimeZone(row.start_datetime, TIMETABLE_TIME_ZONE);
|
const startDate = convertMigrationDateTimeFromTimeZone(row.start_datetime, TIMETABLE_TIME_ZONE);
|
||||||
const endDate = row.end_datetime ? convertMigrationDateTimeFromTimeZone(row.end_datetime, TIMETABLE_TIME_ZONE) : null;
|
const endDate = row.end_datetime ? convertMigrationDateTimeFromTimeZone(row.end_datetime, TIMETABLE_TIME_ZONE) : null;
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'UPDATE i_schedule_entries SET start_datetime = ?, end_datetime = ? WHERE id = ?',
|
'UPDATE ' + entryTableName + ' SET start_datetime = ?, end_datetime = ? WHERE id = ?',
|
||||||
[formatMigrationDateTimeUtc(startDate), endDate ? formatMigrationDateTimeUtc(endDate) : null, row.id]
|
[formatMigrationDateTimeUtc(startDate), endDate ? formatMigrationDateTimeUtc(endDate) : null, row.id]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -333,12 +354,15 @@ const VERSIONED_MIGRATIONS = [
|
|||||||
if (scheduleGroupsExists) {
|
if (scheduleGroupsExists) {
|
||||||
if (!timetableGroupsExists) {
|
if (!timetableGroupsExists) {
|
||||||
await pool.query('RENAME TABLE i_schedule_groups TO i_timetable_groups, i_schedule_entries TO i_timetable_entries');
|
await pool.query('RENAME TABLE i_schedule_groups TO i_timetable_groups, i_schedule_entries TO i_timetable_entries');
|
||||||
|
await ensureColumn(pool, 'i_timetable_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureColumn(pool, 'i_timetable_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT IGNORE INTO i_timetable_groups (id, name, short_description, timezone, created_at, created_by, modified_at, modified_by)
|
INSERT IGNORE INTO i_timetable_groups (id, name, short_description, timezone, created_at, created_by, modified_at, modified_by)
|
||||||
SELECT id, name, short_description, timezone, created_at, created_by, modified_at, modified_by
|
SELECT id, name, short_description, 'Europe/London' AS timezone, created_at, created_by, modified_at, modified_by
|
||||||
FROM i_schedule_groups
|
FROM i_schedule_groups
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
`);
|
`);
|
||||||
@@ -389,6 +413,47 @@ async function tableExists(pool, tableName) {
|
|||||||
return Number(rows && rows[0] && rows[0].table_count) > 0;
|
return Number(rows && rows[0] && rows[0].table_count) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function detectSchemaVersion(pool) {
|
||||||
|
if (await tableExists(pool, APP_STATE_TABLE)) {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
'SELECT state_value FROM ' + APP_STATE_TABLE + ' WHERE state_key = ? LIMIT 1',
|
||||||
|
[APP_STATE_SCHEMA_VERSION_KEY]
|
||||||
|
);
|
||||||
|
|
||||||
|
const storedVersion = String(rows && rows[0] && rows[0].state_value || '').trim();
|
||||||
|
if (storedVersion) {
|
||||||
|
return storedVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
|
||||||
|
const timetableEntriesExists = await tableExists(pool, 'i_timetable_entries');
|
||||||
|
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
|
||||||
|
const scheduleEntriesExists = await tableExists(pool, 'i_schedule_entries');
|
||||||
|
|
||||||
|
if ((timetableGroupsExists || timetableEntriesExists) && !scheduleGroupsExists && !scheduleEntriesExists) {
|
||||||
|
return '2.6.18';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordSchemaVersion(pool, version) {
|
||||||
|
await pool.query(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${APP_STATE_TABLE} (
|
||||||
|
state_key VARCHAR(191) NOT NULL PRIMARY KEY,
|
||||||
|
state_value MEDIUMTEXT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
|
||||||
|
);
|
||||||
|
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO ' + APP_STATE_TABLE + ' (state_key, state_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE state_value = VALUES(state_value)',
|
||||||
|
[APP_STATE_SCHEMA_VERSION_KEY, String(version || appVersion || '0.0.0').trim()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function columnIsAutoIncrement(pool, tableName, columnName) {
|
async function columnIsAutoIncrement(pool, tableName, columnName) {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT COUNT(*) AS auto_increment_count
|
`SELECT COUNT(*) AS auto_increment_count
|
||||||
@@ -772,5 +837,8 @@ async function runMigrations(pool, options) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
appVersion: appVersion,
|
appVersion: appVersion,
|
||||||
runMigrations: runMigrations
|
runMigrations: runMigrations,
|
||||||
|
detectSchemaVersion: detectSchemaVersion,
|
||||||
|
recordSchemaVersion: recordSchemaVersion,
|
||||||
|
compareVersions: compareVersions
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
require('../src/common');
|
||||||
|
|
||||||
|
const { runMigrations } = require('../src/db/migrations');
|
||||||
|
|
||||||
|
function createPool(responses) {
|
||||||
|
const queries = [];
|
||||||
|
return {
|
||||||
|
queries,
|
||||||
|
async query(sql, params) {
|
||||||
|
queries.push([sql, params]);
|
||||||
|
const text = String(sql);
|
||||||
|
for (const response of responses) {
|
||||||
|
if (response.match(text, params)) {
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('timetable rename migration does not depend on legacy timezone column', async () => {
|
||||||
|
const pool = createPool([
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?');
|
||||||
|
},
|
||||||
|
result: [[{ column_count: 0 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('FROM information_schema.TABLES') && sql.includes('TABLE_NAME = ?');
|
||||||
|
},
|
||||||
|
result: [[{ table_count: 1 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT id, name, short_description,') && sql.includes("'Europe/London' AS timezone") && sql.includes('FROM i_schedule_groups');
|
||||||
|
},
|
||||||
|
result: [{}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, created_by, modified_at, modified_by FROM i_schedule_entries');
|
||||||
|
},
|
||||||
|
result: [{}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_groups');
|
||||||
|
},
|
||||||
|
result: [[{ max_id: 1 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_entries');
|
||||||
|
},
|
||||||
|
result: [[{ max_id: 2 }]]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await runMigrations(pool, { currentVersion: '2.6.17' });
|
||||||
|
|
||||||
|
assert.ok(pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes("SELECT id, name, short_description, 'Europe/London' AS timezone") && String(sql).includes('FROM i_schedule_groups');
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.ok(!pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes('SELECT id, name, short_description, timezone, created_at, created_by, modified_at, modified_by') && String(sql).includes('FROM i_schedule_groups');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('timetable timezone migrations use timetable tables when legacy tables are gone', async () => {
|
||||||
|
const pool = createPool([
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('FROM information_schema.COLUMNS') && sql.includes('TABLE_NAME = ?') && sql.includes('COLUMN_NAME = ?');
|
||||||
|
},
|
||||||
|
result: [[{ column_count: 0 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(_sql, params) {
|
||||||
|
return Array.isArray(params) && params[0] === 'i_timetable_groups';
|
||||||
|
},
|
||||||
|
result: [[{ table_count: 1 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(_sql, params) {
|
||||||
|
return Array.isArray(params) && params[0] === 'i_timetable_entries';
|
||||||
|
},
|
||||||
|
result: [[{ table_count: 1 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(_sql, params) {
|
||||||
|
return Array.isArray(params) && params[0] === 'i_schedule_groups';
|
||||||
|
},
|
||||||
|
result: [[{ table_count: 0 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(_sql, params) {
|
||||||
|
return Array.isArray(params) && params[0] === 'i_schedule_entries';
|
||||||
|
},
|
||||||
|
result: [[{ table_count: 0 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT id, start_datetime, end_datetime FROM i_timetable_entries ORDER BY id ASC');
|
||||||
|
},
|
||||||
|
result: [[{ id: 1, start_datetime: '2026-08-09 09:00:00', end_datetime: '2026-08-09 10:00:00' }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_groups');
|
||||||
|
},
|
||||||
|
result: [[{ max_id: 1 }]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match(sql) {
|
||||||
|
return sql.includes('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_entries');
|
||||||
|
},
|
||||||
|
result: [[{ max_id: 1 }]]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await runMigrations(pool, { currentVersion: '2.6.15' });
|
||||||
|
|
||||||
|
assert.ok(pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes('ALTER TABLE i_timetable_groups ADD COLUMN timezone');
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.ok(pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes('UPDATE i_timetable_groups SET timezone = ?');
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.ok(pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes('UPDATE i_timetable_entries SET start_datetime = ?, end_datetime = ? WHERE id = ?');
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.ok(!pool.queries.some(function ([sql]) {
|
||||||
|
return String(sql).includes('i_schedule_groups') || String(sql).includes('i_schedule_entries');
|
||||||
|
}));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user