mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
Clearing an optional playlist field (User-Agent, Referrer, Origin, credentials, EPG URL lists, file path) did not persist in Electron: the codec maps an empty value to `undefined`, `ON CONFLICT DO UPDATE` skips undefined keys, and `parseAppPlaylist` prefers the stale column over the updated payload, so the old value came back on the next read. `playlistConflictUpdate` now sets every optional column the document leaves empty to NULL, so both desktop upserts (`DB_UPSERT_APP_PLAYLIST` and `DB_UPSERT_APP_PLAYLISTS`) replace the stored row. The cleared-columns list moves from the web backend's playlist repository into the shared conflict module, and the server now uses the shared version. `date_created`, `name`, `type`, `autoRefresh`, `payload` and `last_usage` are never cleared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
import { SQL } from 'drizzle-orm';
|
|
import { buildPlaylistRow } from './app-playlist-codec';
|
|
import { playlistConflictUpdate } from './app-playlist-conflict';
|
|
import type { NewPlaylist } from './schema';
|
|
|
|
function rowOf(document: Record<string, unknown>): NewPlaylist {
|
|
const row = buildPlaylistRow(document);
|
|
if (!row) {
|
|
throw new Error('Expected the document to produce a row');
|
|
}
|
|
return row;
|
|
}
|
|
|
|
/** Every optional column of a document that only has an id, title and URL. */
|
|
const CLEARED_COLUMNS = {
|
|
serverUrl: null,
|
|
username: null,
|
|
password: null,
|
|
lastUpdated: null,
|
|
userAgent: null,
|
|
origin: null,
|
|
referrer: null,
|
|
filePath: null,
|
|
epgUrls: null,
|
|
detectedEpgUrls: null,
|
|
manualEpgUrls: null,
|
|
disabledEpgUrls: null,
|
|
macAddress: null,
|
|
portalUrl: null,
|
|
count: null,
|
|
importDate: null,
|
|
updateDate: null,
|
|
position: null,
|
|
favorites: null,
|
|
recentlyViewed: null,
|
|
};
|
|
|
|
describe('playlistConflictUpdate', () => {
|
|
const document = {
|
|
_id: 'm3u-1',
|
|
title: 'Family',
|
|
url: 'https://example.test/list.m3u',
|
|
userAgent: '',
|
|
referrer: ' ',
|
|
};
|
|
|
|
it('sets every optional column the document leaves empty to NULL', () => {
|
|
const row = rowOf(document);
|
|
|
|
expect(playlistConflictUpdate(row, document)).toMatchObject({
|
|
...CLEARED_COLUMNS,
|
|
url: 'https://example.test/list.m3u',
|
|
});
|
|
});
|
|
|
|
it('never clears the creation date or the columns every row carries', () => {
|
|
const row = rowOf(document);
|
|
const update = playlistConflictUpdate(row, document);
|
|
|
|
expect(update).not.toHaveProperty('dateCreated');
|
|
expect(update).toMatchObject({
|
|
id: 'm3u-1',
|
|
name: 'Family',
|
|
type: 'm3u-url',
|
|
autoRefresh: false,
|
|
lastUsage: row.lastUsage,
|
|
});
|
|
// A clockless document keeps a stored panel clock through SQL.
|
|
expect(update.payload).toBeInstanceOf(SQL);
|
|
});
|
|
|
|
it('clears the same columns when the document carries its own clock', () => {
|
|
const withClock = { ...document, serverTimezone: 'UTC' };
|
|
const row = rowOf(withClock);
|
|
const update = playlistConflictUpdate(row, withClock);
|
|
|
|
expect(update).toMatchObject(CLEARED_COLUMNS);
|
|
expect(update.payload).toBe(row.payload);
|
|
});
|
|
});
|