Release v2.6.19
This commit is contained in:
+5
-4
@@ -238,10 +238,11 @@ async function ensureSchema(pool, options) {
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS i_schedule_groups (
|
||||
CREATE TABLE IF NOT EXISTS i_timetable_groups (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
short_description VARCHAR(255) NULL,
|
||||
timezone VARCHAR(64) NOT NULL DEFAULT 'Europe/London',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
@@ -250,7 +251,7 @@ async function ensureSchema(pool, options) {
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS i_schedule_entries (
|
||||
CREATE TABLE IF NOT EXISTS i_timetable_entries (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
schedule_group_id INT NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
@@ -261,8 +262,8 @@ async function ensureSchema(pool, options) {
|
||||
created_by INT NULL,
|
||||
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
modified_by INT NULL,
|
||||
CONSTRAINT fk_schedule_entries_group FOREIGN KEY (schedule_group_id) REFERENCES i_schedule_groups(id) ON DELETE CASCADE,
|
||||
INDEX idx_schedule_entries_group_start (schedule_group_id, start_datetime)
|
||||
CONSTRAINT fk_timetable_entries_group FOREIGN KEY (schedule_group_id) REFERENCES i_timetable_groups(id) ON DELETE CASCADE,
|
||||
INDEX idx_timetable_entries_group_start (schedule_group_id, start_datetime)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
|
||||
+208
-23
@@ -1,4 +1,5 @@
|
||||
const { version: appVersion } = require('#root/package.json');
|
||||
const TIMETABLE_TIME_ZONE = 'Europe/London';
|
||||
|
||||
const VERSIONED_MIGRATIONS = [
|
||||
{
|
||||
@@ -22,32 +23,32 @@ const VERSIONED_MIGRATIONS = [
|
||||
// Store the player pointer on screens so we can resolve the player without needing a player-side screen_id.
|
||||
// This is the singleton-player shortcut; a multi-player model should make this relational instead of hardcoded to '1'.
|
||||
if (!(await columnExists(pool, 'd_screens', 'player_id'))) {
|
||||
await pool.query("ALTER TABLE d_screens ADD COLUMN player_id VARCHAR(128) NOT NULL DEFAULT '1' AFTER playlist_id");
|
||||
} else {
|
||||
await pool.query("UPDATE d_screens SET player_id = '1' WHERE player_id IS NULL OR player_id <> '1'");
|
||||
await pool.query("ALTER TABLE d_screens ADD COLUMN player_id VARCHAR(128) NOT NULL DEFAULT '1' AFTER playlist_id");
|
||||
} else {
|
||||
await pool.query("UPDATE d_screens SET player_id = '1' WHERE player_id IS NULL OR player_id <> '1'");
|
||||
|
||||
const [playerColumnNullableRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS nullable_count
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'd_screens'
|
||||
AND COLUMN_NAME = 'player_id'
|
||||
AND IS_NULLABLE = 'YES'`
|
||||
);
|
||||
if (Number(playerColumnNullableRows && playerColumnNullableRows[0] && playerColumnNullableRows[0].nullable_count) > 0) {
|
||||
await pool.query("ALTER TABLE d_screens MODIFY COLUMN player_id VARCHAR(128) NOT NULL DEFAULT '1' AFTER playlist_id");
|
||||
}
|
||||
}
|
||||
|
||||
const [screenPlayerUniqueRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS index_count
|
||||
FROM information_schema.STATISTICS
|
||||
const [playerColumnNullableRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS nullable_count
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'd_screens'
|
||||
AND INDEX_NAME = 'uq_screens_player_id'`
|
||||
AND COLUMN_NAME = 'player_id'
|
||||
AND IS_NULLABLE = 'YES'`
|
||||
);
|
||||
if (Number(screenPlayerUniqueRows && screenPlayerUniqueRows[0] && screenPlayerUniqueRows[0].index_count) > 0) {
|
||||
await pool.query('ALTER TABLE d_screens DROP INDEX uq_screens_player_id');
|
||||
if (Number(playerColumnNullableRows && playerColumnNullableRows[0] && playerColumnNullableRows[0].nullable_count) > 0) {
|
||||
await pool.query("ALTER TABLE d_screens MODIFY COLUMN player_id VARCHAR(128) NOT NULL DEFAULT '1' AFTER playlist_id");
|
||||
}
|
||||
}
|
||||
|
||||
const [screenPlayerUniqueRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS index_count
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'd_screens'
|
||||
AND INDEX_NAME = 'uq_screens_player_id'`
|
||||
);
|
||||
if (Number(screenPlayerUniqueRows && screenPlayerUniqueRows[0] && screenPlayerUniqueRows[0].index_count) > 0) {
|
||||
await pool.query('ALTER TABLE d_screens DROP INDEX uq_screens_player_id');
|
||||
}
|
||||
|
||||
// Recreate the screen-to-player foreign key after the column exists and legacy data is copied over.
|
||||
@@ -56,7 +57,6 @@ const VERSIONED_MIGRATIONS = [
|
||||
if (await columnExists(pool, 'c_template_regions', 'font_family')) {
|
||||
await pool.query('ALTER TABLE c_template_regions DROP COLUMN font_family');
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -276,6 +276,7 @@ const VERSIONED_MIGRATIONS = [
|
||||
await pool.query('RENAME TABLE d_players_rebuild TO d_players');
|
||||
|
||||
await pool.query('ALTER TABLE d_screens MODIFY COLUMN player_id INT NULL AFTER playlist_id');
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -295,6 +296,71 @@ const VERSIONED_MIGRATIONS = [
|
||||
await dropForeignKeyIfExists(pool, 'd_screens', 'player_id');
|
||||
await dropColumnIfExists(pool, 'd_screens', 'player_id');
|
||||
}
|
||||
},
|
||||
{
|
||||
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');
|
||||
}
|
||||
},
|
||||
{
|
||||
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');
|
||||
|
||||
await pool.query('UPDATE i_schedule_groups SET timezone = ?', [TIMETABLE_TIME_ZONE]);
|
||||
|
||||
const [rows] = await pool.query('SELECT id, start_datetime, end_datetime FROM i_schedule_entries 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 = ?',
|
||||
[formatMigrationDateTimeUtc(startDate), endDate ? formatMigrationDateTimeUtc(endDate) : null, row.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
version: '2.6.18',
|
||||
label: 'v2.6.18 timetable table rename',
|
||||
run: async function (pool) {
|
||||
const scheduleGroupsExists = await tableExists(pool, 'i_schedule_groups');
|
||||
const timetableGroupsExists = await tableExists(pool, 'i_timetable_groups');
|
||||
|
||||
if (scheduleGroupsExists) {
|
||||
if (!timetableGroupsExists) {
|
||||
await pool.query('RENAME TABLE i_schedule_groups TO i_timetable_groups, i_schedule_entries TO i_timetable_entries');
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
FROM i_schedule_groups
|
||||
ORDER BY id ASC
|
||||
`);
|
||||
|
||||
await pool.query(`
|
||||
INSERT IGNORE INTO i_timetable_entries (id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, created_by, modified_at, modified_by)
|
||||
SELECT id, schedule_group_id, title, short_description, start_datetime, end_datetime, created_at, created_by, modified_at, modified_by
|
||||
FROM i_schedule_entries
|
||||
ORDER BY schedule_group_id ASC, start_datetime ASC, id ASC
|
||||
`);
|
||||
|
||||
const [groupRows] = await pool.query('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_groups');
|
||||
const [entryRows] = await pool.query('SELECT COALESCE(MAX(id), 0) AS max_id FROM i_timetable_entries');
|
||||
const nextGroupId = Number(groupRows && groupRows[0] && groupRows[0].max_id) + 1;
|
||||
const nextEntryId = Number(entryRows && entryRows[0] && entryRows[0].max_id) + 1;
|
||||
await pool.query('ALTER TABLE i_timetable_groups AUTO_INCREMENT = ' + nextGroupId);
|
||||
await pool.query('ALTER TABLE i_timetable_entries AUTO_INCREMENT = ' + nextEntryId);
|
||||
|
||||
await pool.query('DROP TABLE i_schedule_entries');
|
||||
await pool.query('DROP TABLE i_schedule_groups');
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -311,6 +377,18 @@ async function columnExists(pool, tableName, columnName) {
|
||||
return Number(rows && rows[0] && rows[0].column_count) > 0;
|
||||
}
|
||||
|
||||
async function tableExists(pool, tableName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS table_count
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?`,
|
||||
[tableName]
|
||||
);
|
||||
|
||||
return Number(rows && rows[0] && rows[0].table_count) > 0;
|
||||
}
|
||||
|
||||
async function columnIsAutoIncrement(pool, tableName, columnName) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT COUNT(*) AS auto_increment_count
|
||||
@@ -512,6 +590,107 @@ function formatMigrationDateTime(value) {
|
||||
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
||||
}
|
||||
|
||||
function parseMigrationDateTimeParts(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
year: value.getUTCFullYear(),
|
||||
month: value.getUTCMonth() + 1,
|
||||
day: value.getUTCDate(),
|
||||
hour: value.getUTCHours(),
|
||||
minute: value.getUTCMinutes(),
|
||||
second: value.getUTCSeconds()
|
||||
};
|
||||
}
|
||||
|
||||
const raw = String(value || '').trim();
|
||||
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
year: Number(match[1]),
|
||||
month: Number(match[2]),
|
||||
day: Number(match[3]),
|
||||
hour: Number(match[4] || 0),
|
||||
minute: Number(match[5] || 0),
|
||||
second: Number(match[6] || 0)
|
||||
};
|
||||
}
|
||||
|
||||
function getMigrationTimeZoneOffsetMillis(date, timeZone) {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const parts = new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: timeZone,
|
||||
hour12: false,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).formatToParts(date).reduce(function (acc, part) {
|
||||
if (part && part.type && part.type !== 'literal') {
|
||||
acc[part.type] = part.value;
|
||||
}
|
||||
return acc;
|
||||
}, Object.create(null));
|
||||
|
||||
const localAsUtc = Date.UTC(
|
||||
Number(parts.year) || 0,
|
||||
(Number(parts.month) || 1) - 1,
|
||||
Number(parts.day) || 1,
|
||||
Number(parts.hour) || 0,
|
||||
Number(parts.minute) || 0,
|
||||
Number(parts.second) || 0,
|
||||
0
|
||||
);
|
||||
|
||||
return localAsUtc - date.getTime();
|
||||
}
|
||||
|
||||
function convertMigrationDateTimeFromTimeZone(value, timeZone) {
|
||||
const parts = parseMigrationDateTimeParts(value);
|
||||
if (!parts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const utcMillis = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, 0);
|
||||
let adjusted = new Date(utcMillis - getMigrationTimeZoneOffsetMillis(new Date(utcMillis), timeZone));
|
||||
const adjustedOffset = getMigrationTimeZoneOffsetMillis(adjusted, timeZone);
|
||||
|
||||
if (adjustedOffset !== getMigrationTimeZoneOffsetMillis(new Date(utcMillis), timeZone)) {
|
||||
adjusted = new Date(utcMillis - adjustedOffset);
|
||||
}
|
||||
|
||||
return adjusted;
|
||||
}
|
||||
|
||||
function formatMigrationDateTimeUtc(value) {
|
||||
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const year = value.getUTCFullYear();
|
||||
const month = String(value.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(value.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(value.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(value.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(value.getUTCSeconds()).padStart(2, '0');
|
||||
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
|
||||
}
|
||||
|
||||
function formatMigrationTime(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -568,6 +747,8 @@ async function runMigrations(pool, options) {
|
||||
const currentVersion = String(options && options.currentVersion || '0.0.0').trim();
|
||||
const legacyPlayerSchemaPresent = await columnExists(pool, 'd_players', 'device_id');
|
||||
const screenPlayerColumnPresent = await columnExists(pool, 'd_screens', 'player_id');
|
||||
const legacyTimetableGroupsPresent = await tableExists(pool, 'i_schedule_groups');
|
||||
const legacyTimetableEntriesPresent = await tableExists(pool, 'i_schedule_entries');
|
||||
let effectiveCurrentVersion = currentVersion;
|
||||
|
||||
if (!legacyPlayerSchemaPresent && compareVersions(effectiveCurrentVersion, '2.1.0') < 0) {
|
||||
@@ -578,6 +759,10 @@ async function runMigrations(pool, options) {
|
||||
effectiveCurrentVersion = '2.6.3';
|
||||
}
|
||||
|
||||
if (legacyTimetableGroupsPresent || legacyTimetableEntriesPresent) {
|
||||
effectiveCurrentVersion = compareVersions(effectiveCurrentVersion, '2.6.18') < 0 ? '2.6.17' : '2.6.17';
|
||||
}
|
||||
|
||||
for (const migration of VERSIONED_MIGRATIONS) {
|
||||
if (compareVersions(migration.version, effectiveCurrentVersion) > 0 && compareVersions(migration.version, targetVersion) <= 0) {
|
||||
await migration.run(pool);
|
||||
|
||||
Reference in New Issue
Block a user