Files
iptvnator/libs/shared/database/src/lib/app-playlist-conflict.spec.ts
T
4grayandClaude Opus 5.5 434cbf3681 fix(playlist): keep cleared optional fields cleared on desktop (#1904)
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>
2026-10-12 23:08:35 +02:00

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);
});
});