Release 2.22.0: normalize playlist schedules and unify player sync
This commit is contained in:
1 parent
7748129307
commit
b71958cb38
39 files changed
+1174
-314
No files matched your search
@@ -93,7 +93,7 @@ test('pending migrations are empty when the schema already matches the app versi
|
||||
|
||||
test('2.20.1 release follows the consolidated 2.20.0 migration', async () => {
|
||||
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.19.1' });
|
||||
assert.deepEqual(pending.map(migration => migration.version), ['2.20.0', '2.20.1']);
|
||||
assert.deepEqual(pending.filter(migration => migration.version.startsWith('2.20.')).map(migration => migration.version), ['2.20.0', '2.20.1']);
|
||||
});
|
||||
|
||||
test('2.20.1 removes retired security setting rows', async () => {
|
||||
@@ -627,4 +627,120 @@ test('2.19.0 indexed-reference migration backfills slide and template defaults',
|
||||
assert.ok(queries.every(([sql]) => !sql.includes('idx_content_reference_owner')));
|
||||
assert.ok(insertedReferences.some(row => row[0] === 'slide' && row[1] === 2 && row[3] === 'media' && row[4] === '/media/uploads/hero.png'));
|
||||
assert.ok(insertedReferences.some(row => row[0] === 'template' && row[1] === 4 && row[2] === 'headline' && row[3] === 'font-family' && row[4] === 'acme display'));
|
||||
});
|
||||
|
||||
function createPlaylistScheduleMigrationPool(groupRows, sourceRuleCount) {
|
||||
const queries = [];
|
||||
const legacyGroupColumns = new Set([
|
||||
'group_key', 'group_name', 'group_pick_count', 'group_shuffle',
|
||||
'group_mode', 'group_repeat_count', 'group_schedule_rules_json'
|
||||
]);
|
||||
let hasGroupId = false;
|
||||
let hasLegacyScheduleTable = true;
|
||||
let migratedSlideRuleCount = 0;
|
||||
const groupInsertParams = [];
|
||||
const groupRuleInsertParams = [];
|
||||
const memberGroupUpdates = [];
|
||||
|
||||
return {
|
||||
queries,
|
||||
groupInsertParams,
|
||||
groupRuleInsertParams,
|
||||
memberGroupUpdates,
|
||||
async query(sql, params) {
|
||||
const text = String(sql);
|
||||
queries.push([text, params]);
|
||||
if (text.includes('FROM information_schema.COLUMNS')) {
|
||||
const columnName = params[1];
|
||||
const exists = columnName === 'group_id' ? hasGroupId : legacyGroupColumns.has(columnName);
|
||||
return [[{ column_count: exists ? 1 : 0 }]];
|
||||
}
|
||||
if (text.includes('FROM information_schema.TABLES')) {
|
||||
return [[{ table_count: params[0] === 'c_playlist_slide_schedule_rules' && hasLegacyScheduleTable ? 1 : 0 }]];
|
||||
}
|
||||
if (text.includes('FROM information_schema.KEY_COLUMN_USAGE')) {
|
||||
return [[{ fk_count: 0 }]];
|
||||
}
|
||||
if (text.startsWith('ALTER TABLE c_playlist_slides ADD COLUMN group_id')) {
|
||||
hasGroupId = true;
|
||||
}
|
||||
if (text.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN')) {
|
||||
text.split('DROP COLUMN ').slice(1).forEach(part => legacyGroupColumns.delete(part.split(/[ ,]/)[0]));
|
||||
}
|
||||
if (text.includes('SELECT id, playlist_id, group_key, group_name')) {
|
||||
return [structuredClone(groupRows)];
|
||||
}
|
||||
if (text.startsWith('INSERT INTO c_playlist_groups')) {
|
||||
groupInsertParams.push(params);
|
||||
return [{ insertId: 77 }];
|
||||
}
|
||||
if (text.startsWith('INSERT INTO c_playlist_schedule_rules') && text.includes('VALUES')) {
|
||||
groupRuleInsertParams.push(params);
|
||||
}
|
||||
if (text.startsWith('UPDATE c_playlist_slides SET group_id')) {
|
||||
memberGroupUpdates.push(params);
|
||||
}
|
||||
if (text.startsWith('SELECT COUNT(*) AS rule_count FROM c_playlist_slide_schedule_rules')) {
|
||||
return [[{ rule_count: sourceRuleCount }]];
|
||||
}
|
||||
if (text.startsWith('DELETE FROM c_playlist_schedule_rules WHERE playlist_slide_id')) {
|
||||
migratedSlideRuleCount = 0;
|
||||
}
|
||||
if (text.includes('INSERT INTO c_playlist_schedule_rules') && text.includes('SELECT playlist_slide_id')) {
|
||||
migratedSlideRuleCount = sourceRuleCount;
|
||||
}
|
||||
if (text.startsWith('SELECT COUNT(*) AS rule_count FROM c_playlist_schedule_rules WHERE playlist_slide_id')) {
|
||||
return [[{ rule_count: migratedSlideRuleCount }]];
|
||||
}
|
||||
if (text === 'DROP TABLE c_playlist_slide_schedule_rules') {
|
||||
hasLegacyScheduleTable = false;
|
||||
}
|
||||
return [[]];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildLegacyPlaylistGroupRows(groupRulesJson, secondPickCount) {
|
||||
return [
|
||||
{ id: 501, playlist_id: 8, group_key: 'events', group_name: 'Events', group_pick_count: 1, group_shuffle: 1, group_mode: 'select', group_repeat_count: 1, group_schedule_rules_json: groupRulesJson, created_at: '2026-10-01 10:00:00', created_by: 4, modified_at: '2026-10-02 10:00:00', modified_by: 5 },
|
||||
{ id: 502, playlist_id: 8, group_key: 'events', group_name: 'Events', group_pick_count: secondPickCount || 1, group_shuffle: 1, group_mode: 'select', group_repeat_count: 1, group_schedule_rules_json: groupRulesJson, created_at: '2026-10-01 10:00:00', created_by: 4, modified_at: '2026-10-02 10:00:00', modified_by: 5 }
|
||||
];
|
||||
}
|
||||
|
||||
test('2.22.0 playlist schedule migration consolidates shared groups and copies both rule owners', async () => {
|
||||
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.21.6' });
|
||||
const migration = pending.find(item => item.version === '2.22.0');
|
||||
assert.ok(migration);
|
||||
|
||||
const groupRules = JSON.stringify([{ start_datetime: '2026-10-01T10:00', days: [2] }]);
|
||||
const pool = createPlaylistScheduleMigrationPool(buildLegacyPlaylistGroupRows(groupRules), 2);
|
||||
await migration.run(pool);
|
||||
|
||||
assert.equal(pool.groupInsertParams.length, 1);
|
||||
assert.deepEqual(pool.groupInsertParams[0].slice(0, 7), [8, 'events', 'Events', 1, 1, 'select', 1]);
|
||||
assert.equal(pool.memberGroupUpdates.length, 2);
|
||||
assert.deepEqual(pool.memberGroupUpdates, [[77, 501], [77, 502]]);
|
||||
assert.deepEqual(pool.groupRuleInsertParams, [[77, 0, '2026-10-01T10:00', null, null, null, '[2]', 5, 5]]);
|
||||
assert.ok(pool.queries.some(([sql]) => sql.includes('INSERT INTO c_playlist_schedule_rules') && sql.includes('SELECT playlist_slide_id')));
|
||||
assert.ok(pool.queries.some(([sql]) => sql === 'DROP TABLE c_playlist_slide_schedule_rules'));
|
||||
const legacyGroupColumnDrops = pool.queries.filter(([sql]) => sql.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN'));
|
||||
assert.equal(legacyGroupColumnDrops.length, 1);
|
||||
assert.match(legacyGroupColumnDrops[0][0], /DROP COLUMN group_key, DROP COLUMN group_name, DROP COLUMN group_pick_count, DROP COLUMN group_shuffle, DROP COLUMN group_mode, DROP COLUMN group_repeat_count, DROP COLUMN group_schedule_rules_json/);
|
||||
|
||||
const schema = require('node:fs').readFileSync(require.resolve('../src/db/index.js'), 'utf8');
|
||||
assert.match(schema, /CREATE TABLE IF NOT EXISTS c_playlist_groups/);
|
||||
assert.match(schema, /CREATE TABLE IF NOT EXISTS c_playlist_schedule_rules/);
|
||||
assert.doesNotMatch(schema, /group_schedule_rules_json/);
|
||||
});
|
||||
|
||||
test('2.22.0 playlist schedule migration aborts before dropping inconsistent legacy group data', async () => {
|
||||
const pending = await getPendingMigrations(createPool([]), { currentVersion: '2.21.6' });
|
||||
const migration = pending.find(item => item.version === '2.22.0');
|
||||
assert.ok(migration);
|
||||
|
||||
const pool = createPlaylistScheduleMigrationPool(buildLegacyPlaylistGroupRows('[]', 2), 0);
|
||||
await assert.rejects(migration.run(pool), /inconsistent settings across members/);
|
||||
assert.equal(pool.groupInsertParams.length, 0);
|
||||
assert.equal(pool.queries.some(([sql]) => sql === 'DROP TABLE c_playlist_slide_schedule_rules'), false);
|
||||
assert.equal(pool.queries.some(([sql]) => sql.startsWith('ALTER TABLE c_playlist_slides DROP COLUMN')), false);
|
||||
});
|
||||
Reference in new issue
Block a user