Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39f2098ffe |
@@ -2,6 +2,12 @@
|
||||
|
||||
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
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-player",
|
||||
"version": "2.6.19",
|
||||
"version": "2.6.20",
|
||||
"private": false,
|
||||
"description": "Pulse Signage player application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage-web",
|
||||
"version": "2.6.19",
|
||||
"version": "2.6.20",
|
||||
"private": false,
|
||||
"description": "Pulse Signage web and bridge application bundle",
|
||||
"main": "src/common.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pulse-signage",
|
||||
"version": "2.6.19",
|
||||
"version": "2.6.20",
|
||||
"private": false,
|
||||
"description": "Pulse Signage application with MySQL and media storage",
|
||||
"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.
|
||||
async function ensureSchema(pool, options) {
|
||||
const schemaLockName = 'pulse_signage_schema_lock';
|
||||
@@ -12,6 +15,11 @@ async function ensureSchema(pool, options) {
|
||||
}
|
||||
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS c_canvas_sizes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
@@ -384,8 +392,8 @@ async function ensureSchema(pool, options) {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
const { runMigrations } = require('./migrations');
|
||||
await runMigrations(pool, options);
|
||||
await runMigrations(pool, Object.assign({}, options, { currentVersion: currentVersion }));
|
||||
await recordSchemaVersion(pool, appVersion);
|
||||
} finally {
|
||||
await pool.query('SELECT RELEASE_LOCK(?)', [schemaLockName]).catch(function () {
|
||||
});
|
||||
|
||||
+75
-7
@@ -1,5 +1,7 @@
|
||||
const { version: appVersion } = require('#root/package.json');
|
||||
const TIMETABLE_TIME_ZONE = 'Europe/London';
|
||||
const APP_STATE_TABLE = 'o_app_state';
|
||||
const APP_STATE_SCHEMA_VERSION_KEY = 'schema_version';
|
||||
|
||||
const VERSIONED_MIGRATIONS = [
|
||||
{
|
||||
@@ -301,23 +303,42 @@ const VERSIONED_MIGRATIONS = [
|
||||
version: '2.6.16',
|
||||
label: 'v2.6.16 timetable timezone schema',
|
||||
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',
|
||||
label: 'v2.6.17 timetable europe/london conversion',
|
||||
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) {
|
||||
const startDate = convertMigrationDateTimeFromTimeZone(row.start_datetime, TIMETABLE_TIME_ZONE);
|
||||
const endDate = row.end_datetime ? convertMigrationDateTimeFromTimeZone(row.end_datetime, TIMETABLE_TIME_ZONE) : null;
|
||||
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]
|
||||
);
|
||||
}
|
||||
@@ -333,12 +354,15 @@ const VERSIONED_MIGRATIONS = [
|
||||
if (scheduleGroupsExists) {
|
||||
if (!timetableGroupsExists) {
|
||||
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;
|
||||
}
|
||||
|
||||
await ensureColumn(pool, 'i_timetable_groups', 'timezone', "VARCHAR(64) NOT NULL DEFAULT 'Europe/London'", 'short_description');
|
||||
|
||||
await pool.query(`
|
||||
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
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
@@ -389,6 +413,47 @@ async function tableExists(pool, tableName) {
|
||||
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) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS auto_increment_count
|
||||
@@ -772,5 +837,8 @@ async function runMigrations(pool, options) {
|
||||
|
||||
module.exports = {
|
||||
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