@xiboplayer/utils 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/utils",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Shared utilities for Xibo Player packages",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,7 +12,7 @@
12
12
  "./config": "./src/config.js"
13
13
  },
14
14
  "dependencies": {
15
- "@xiboplayer/crypto": "0.6.1"
15
+ "@xiboplayer/crypto": "0.6.3"
16
16
  },
17
17
  "devDependencies": {
18
18
  "vitest": "^2.0.0"
package/src/cms-api.js CHANGED
@@ -111,15 +111,7 @@ export class CmsApiClient {
111
111
  const response = await fetch(url, options);
112
112
 
113
113
  if (!response.ok) {
114
- const text = await response.text();
115
- let errorMsg;
116
- try {
117
- const errorData = JSON.parse(text);
118
- errorMsg = errorData.error?.message || errorData.message || text;
119
- } catch (_) {
120
- errorMsg = text;
121
- }
122
- throw new CmsApiError(method, path, response.status, errorMsg);
114
+ await this._handleErrorResponse(response, method, path);
123
115
  }
124
116
 
125
117
  // Some endpoints return empty body (204)
@@ -141,6 +133,35 @@ export class CmsApiClient {
141
133
  /** DELETE request (path relative to /api/) */
142
134
  del(path) { return this.request('DELETE', path); }
143
135
 
136
+ /**
137
+ * GET a list endpoint and ensure the result is always an array.
138
+ * @param {string} path - API path
139
+ * @param {Object} [filters={}] - Query parameters
140
+ * @returns {Promise<Array>}
141
+ */
142
+ async _listRequest(path, filters = {}) {
143
+ const data = await this.request('GET', path, filters);
144
+ return Array.isArray(data) ? data : [];
145
+ }
146
+
147
+ /**
148
+ * Parse an error response body and throw a structured error.
149
+ * @param {Response} response - Fetch response
150
+ * @param {string} method - HTTP method
151
+ * @param {string} path - API path
152
+ */
153
+ async _handleErrorResponse(response, method, path) {
154
+ const text = await response.text();
155
+ let errorMsg;
156
+ try {
157
+ const errorData = JSON.parse(text);
158
+ errorMsg = errorData.error?.message || errorData.message || text;
159
+ } catch (_) {
160
+ errorMsg = text;
161
+ }
162
+ throw new CmsApiError(method, path, response.status, errorMsg);
163
+ }
164
+
144
165
  // ── Display Management ──────────────────────────────────────────
145
166
 
146
167
  /**
@@ -150,10 +171,7 @@ export class CmsApiClient {
150
171
  */
151
172
  async findDisplay(hardwareKey) {
152
173
  log.info('Looking up display by hardwareKey:', hardwareKey);
153
- const data = await this.request('GET', '/display', { hardwareKey });
154
-
155
- // API returns array of matching displays
156
- const displays = Array.isArray(data) ? data : [];
174
+ const displays = await this._listRequest('/display', { hardwareKey });
157
175
  if (displays.length === 0) {
158
176
  log.info('No display found for hardwareKey:', hardwareKey);
159
177
  return null;
@@ -192,8 +210,7 @@ export class CmsApiClient {
192
210
  * @returns {Promise<Array>} Array of display objects
193
211
  */
194
212
  async listDisplays(filters = {}) {
195
- const data = await this.request('GET', '/display', filters);
196
- return Array.isArray(data) ? data : [];
213
+ return this._listRequest('/display', filters);
197
214
  }
198
215
 
199
216
  /**
@@ -238,15 +255,7 @@ export class CmsApiClient {
238
255
  });
239
256
 
240
257
  if (!response.ok) {
241
- const text = await response.text();
242
- let errorMsg;
243
- try {
244
- const errorData = JSON.parse(text);
245
- errorMsg = errorData.error?.message || errorData.message || text;
246
- } catch (_) {
247
- errorMsg = text;
248
- }
249
- throw new Error(`CMS API ${method} ${path} failed (${response.status}): ${errorMsg}`);
258
+ await this._handleErrorResponse(response, method, path);
250
259
  }
251
260
 
252
261
  const contentType = response.headers.get('Content-Type') || '';
@@ -278,8 +287,7 @@ export class CmsApiClient {
278
287
  * @returns {Promise<Array>}
279
288
  */
280
289
  async listLayouts(filters = {}) {
281
- const data = await this.request('GET', '/layout', filters);
282
- return Array.isArray(data) ? data : [];
290
+ return this._listRequest('/layout', filters);
283
291
  }
284
292
 
285
293
  /**
@@ -446,8 +454,7 @@ export class CmsApiClient {
446
454
  * @returns {Promise<Array>}
447
455
  */
448
456
  async listMedia(filters = {}) {
449
- const data = await this.request('GET', '/library', filters);
450
- return Array.isArray(data) ? data : [];
457
+ return this._listRequest('/library', filters);
451
458
  }
452
459
 
453
460
  /**
@@ -485,8 +492,7 @@ export class CmsApiClient {
485
492
  * @returns {Promise<Array>}
486
493
  */
487
494
  async listCampaigns(filters = {}) {
488
- const data = await this.request('GET', '/campaign', filters);
489
- return Array.isArray(data) ? data : [];
495
+ return this._listRequest('/campaign', filters);
490
496
  }
491
497
 
492
498
  /**
@@ -600,8 +606,7 @@ export class CmsApiClient {
600
606
  * @returns {Promise<Array>}
601
607
  */
602
608
  async listDisplayGroups(filters = {}) {
603
- const data = await this.request('GET', '/displaygroup', filters);
604
- return Array.isArray(data) ? data : [];
609
+ return this._listRequest('/displaygroup', filters);
605
610
  }
606
611
 
607
612
  /**
@@ -688,8 +693,7 @@ export class CmsApiClient {
688
693
  * @returns {Promise<Array>}
689
694
  */
690
695
  async listResolutions() {
691
- const data = await this.request('GET', '/resolution');
692
- return Array.isArray(data) ? data : [];
696
+ return this._listRequest('/resolution');
693
697
  }
694
698
 
695
699
  // ── Template Management ──────────────────────────────────────────
@@ -700,8 +704,7 @@ export class CmsApiClient {
700
704
  * @returns {Promise<Array>}
701
705
  */
702
706
  async listTemplates(filters = {}) {
703
- const data = await this.request('GET', '/template', filters);
704
- return Array.isArray(data) ? data : [];
707
+ return this._listRequest('/template', filters);
705
708
  }
706
709
 
707
710
  // ── Playlist Management ──────────────────────────────────────────
@@ -870,8 +873,7 @@ export class CmsApiClient {
870
873
  * @returns {Promise<Array>}
871
874
  */
872
875
  async listCommands(filters = {}) {
873
- const data = await this.get('/command', filters);
874
- return Array.isArray(data) ? data : [];
876
+ return this._listRequest('/command', filters);
875
877
  }
876
878
 
877
879
  /**
@@ -949,8 +951,7 @@ export class CmsApiClient {
949
951
  * @returns {Promise<Array>}
950
952
  */
951
953
  async listDayParts(filters = {}) {
952
- const data = await this.get('/daypart', filters);
953
- return Array.isArray(data) ? data : [];
954
+ return this._listRequest('/daypart', filters);
954
955
  }
955
956
 
956
957
  /**
@@ -1055,8 +1056,7 @@ export class CmsApiClient {
1055
1056
  * @returns {Promise<Array>}
1056
1057
  */
1057
1058
  async listPlaylists(filters = {}) {
1058
- const data = await this.get('/playlist', filters);
1059
- return Array.isArray(data) ? data : [];
1059
+ return this._listRequest('/playlist', filters);
1060
1060
  }
1061
1061
 
1062
1062
  /**
@@ -1212,8 +1212,7 @@ export class CmsApiClient {
1212
1212
  * @returns {Promise<Array>}
1213
1213
  */
1214
1214
  async listDatasets(filters = {}) {
1215
- const data = await this.get('/dataset', filters);
1216
- return Array.isArray(data) ? data : [];
1215
+ return this._listRequest('/dataset', filters);
1217
1216
  }
1218
1217
 
1219
1218
  /**
@@ -1250,8 +1249,7 @@ export class CmsApiClient {
1250
1249
  * @returns {Promise<Array>}
1251
1250
  */
1252
1251
  async listDatasetColumns(dataSetId) {
1253
- const data = await this.get(`/dataset/${dataSetId}/column`);
1254
- return Array.isArray(data) ? data : [];
1252
+ return this._listRequest(`/dataset/${dataSetId}/column`);
1255
1253
  }
1256
1254
 
1257
1255
  /**
@@ -1292,8 +1290,7 @@ export class CmsApiClient {
1292
1290
  * @returns {Promise<Array>}
1293
1291
  */
1294
1292
  async listDatasetData(dataSetId, filters = {}) {
1295
- const data = await this.get(`/dataset/data/${dataSetId}`, filters);
1296
- return Array.isArray(data) ? data : [];
1293
+ return this._listRequest(`/dataset/data/${dataSetId}`, filters);
1297
1294
  }
1298
1295
 
1299
1296
  /**
@@ -1354,8 +1351,7 @@ export class CmsApiClient {
1354
1351
  * @returns {Promise<Array>}
1355
1352
  */
1356
1353
  async listNotifications(filters = {}) {
1357
- const data = await this.get('/notification', filters);
1358
- return Array.isArray(data) ? data : [];
1354
+ return this._listRequest('/notification', filters);
1359
1355
  }
1360
1356
 
1361
1357
  /**
@@ -1394,8 +1390,7 @@ export class CmsApiClient {
1394
1390
  * @returns {Promise<Array>}
1395
1391
  */
1396
1392
  async listFolders(filters = {}) {
1397
- const data = await this.get('/folder', filters);
1398
- return Array.isArray(data) ? data : [];
1393
+ return this._listRequest('/folder', filters);
1399
1394
  }
1400
1395
 
1401
1396
  /**
@@ -1434,8 +1429,7 @@ export class CmsApiClient {
1434
1429
  * @returns {Promise<Array>}
1435
1430
  */
1436
1431
  async listTags(filters = {}) {
1437
- const data = await this.get('/tag', filters);
1438
- return Array.isArray(data) ? data : [];
1432
+ return this._listRequest('/tag', filters);
1439
1433
  }
1440
1434
 
1441
1435
  /**
package/src/logger.js CHANGED
@@ -40,6 +40,12 @@ class Logger {
40
40
  }
41
41
  }
42
42
 
43
+ /** HH:MM:SS.mmm timestamp for log lines */
44
+ _ts() {
45
+ const d = new Date();
46
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
47
+ }
48
+
43
49
  setLevel(level) {
44
50
  this.useGlobal = false;
45
51
  if (typeof level === 'string') {
@@ -56,28 +62,28 @@ class Logger {
56
62
 
57
63
  debug(...args) {
58
64
  if (this.getEffectiveLevel() <= LOG_LEVELS.DEBUG) {
59
- console.log(`[${this.name}] DEBUG:`, ...args);
65
+ console.log(`${this._ts()} [${this.name}] DEBUG:`, ...args);
60
66
  }
61
67
  _dispatchToSinks('debug', this.name, args);
62
68
  }
63
69
 
64
70
  info(...args) {
65
71
  if (this.getEffectiveLevel() <= LOG_LEVELS.INFO) {
66
- console.log(`[${this.name}]`, ...args);
72
+ console.log(`${this._ts()} [${this.name}]`, ...args);
67
73
  }
68
74
  _dispatchToSinks('info', this.name, args);
69
75
  }
70
76
 
71
77
  warn(...args) {
72
78
  if (this.getEffectiveLevel() <= LOG_LEVELS.WARNING) {
73
- console.warn(`[${this.name}]`, ...args);
79
+ console.warn(`${this._ts()} [${this.name}]`, ...args);
74
80
  }
75
81
  _dispatchToSinks('warning', this.name, args);
76
82
  }
77
83
 
78
84
  error(...args) {
79
85
  if (this.getEffectiveLevel() <= LOG_LEVELS.ERROR) {
80
- console.error(`[${this.name}]`, ...args);
86
+ console.error(`${this._ts()} [${this.name}]`, ...args);
81
87
  }
82
88
  _dispatchToSinks('error', this.name, args);
83
89
  }
@@ -7,6 +7,10 @@
7
7
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
8
8
  import { createLogger, setLogLevel, getLogLevel, LOG_LEVELS } from './logger.js';
9
9
 
10
+ // Matches "HH:MM:SS.mmm [Name]" or "HH:MM:SS.mmm [Name] DEBUG:"
11
+ const ts = (name, suffix = '') =>
12
+ expect.stringMatching(new RegExp(`^\\d{2}:\\d{2}:\\d{2}\\.\\d{3} \\[${name}\\]${suffix}$`));
13
+
10
14
  describe('Logger', () => {
11
15
  let consoleLogSpy;
12
16
  let consoleWarnSpy;
@@ -62,7 +66,7 @@ describe('Logger', () => {
62
66
  logger.debug('Debug message', { data: 'value' });
63
67
 
64
68
  expect(consoleLogSpy).toHaveBeenCalledWith(
65
- '[Test] DEBUG:',
69
+ ts('Test', ' DEBUG:'),
66
70
  'Debug message',
67
71
  { data: 'value' }
68
72
  );
@@ -100,7 +104,7 @@ describe('Logger', () => {
100
104
  logger.info('Info message', 'arg1', 'arg2');
101
105
 
102
106
  expect(consoleLogSpy).toHaveBeenCalledWith(
103
- '[Test]',
107
+ ts('Test'),
104
108
  'Info message',
105
109
  'arg1',
106
110
  'arg2'
@@ -139,7 +143,7 @@ describe('Logger', () => {
139
143
  logger.warn('Warning message', { warn: true });
140
144
 
141
145
  expect(consoleWarnSpy).toHaveBeenCalledWith(
142
- '[Test]',
146
+ ts('Test'),
143
147
  'Warning message',
144
148
  { warn: true }
145
149
  );
@@ -177,7 +181,7 @@ describe('Logger', () => {
177
181
  logger.error('Error message', new Error('Test error'));
178
182
 
179
183
  expect(consoleErrorSpy).toHaveBeenCalledWith(
180
- '[Test]',
184
+ ts('Test'),
181
185
  'Error message',
182
186
  expect.any(Error)
183
187
  );
@@ -226,37 +230,37 @@ describe('Logger', () => {
226
230
  it('should delegate to debug()', () => {
227
231
  logger.log('DEBUG', 'message');
228
232
 
229
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test] DEBUG:', 'message');
233
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Test', ' DEBUG:'), 'message');
230
234
  });
231
235
 
232
236
  it('should delegate to info()', () => {
233
237
  logger.log('INFO', 'message');
234
238
 
235
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test]', 'message');
239
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Test'), 'message');
236
240
  });
237
241
 
238
242
  it('should delegate to warn() for WARNING', () => {
239
243
  logger.log('WARNING', 'message');
240
244
 
241
- expect(consoleWarnSpy).toHaveBeenCalledWith('[Test]', 'message');
245
+ expect(consoleWarnSpy).toHaveBeenCalledWith(ts('Test'), 'message');
242
246
  });
243
247
 
244
248
  it('should delegate to warn() for WARN', () => {
245
249
  logger.log('WARN', 'message');
246
250
 
247
- expect(consoleWarnSpy).toHaveBeenCalledWith('[Test]', 'message');
251
+ expect(consoleWarnSpy).toHaveBeenCalledWith(ts('Test'), 'message');
248
252
  });
249
253
 
250
254
  it('should delegate to error()', () => {
251
255
  logger.log('ERROR', 'message');
252
256
 
253
- expect(consoleErrorSpy).toHaveBeenCalledWith('[Test]', 'message');
257
+ expect(consoleErrorSpy).toHaveBeenCalledWith(ts('Test'), 'message');
254
258
  });
255
259
 
256
260
  it('should handle lowercase level names', () => {
257
261
  logger.log('debug', 'message');
258
262
 
259
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test] DEBUG:', 'message');
263
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Test', ' DEBUG:'), 'message');
260
264
  });
261
265
  });
262
266
 
@@ -345,7 +349,7 @@ describe('Logger', () => {
345
349
  logger.info('Message', 1, 'two', { three: 3 }, [4, 5]);
346
350
 
347
351
  expect(consoleLogSpy).toHaveBeenCalledWith(
348
- '[Test]',
352
+ ts('Test'),
349
353
  'Message',
350
354
  1,
351
355
  'two',
@@ -357,7 +361,7 @@ describe('Logger', () => {
357
361
  it('should handle zero arguments', () => {
358
362
  logger.info();
359
363
 
360
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test]');
364
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Test'));
361
365
  });
362
366
 
363
367
  it('should handle objects and errors', () => {
@@ -367,7 +371,7 @@ describe('Logger', () => {
367
371
  logger.error('Error:', error, obj);
368
372
 
369
373
  expect(consoleErrorSpy).toHaveBeenCalledWith(
370
- '[Test]',
374
+ ts('Test'),
371
375
  'Error:',
372
376
  error,
373
377
  obj
@@ -381,7 +385,7 @@ describe('Logger', () => {
381
385
 
382
386
  logger.info('Test');
383
387
 
384
- expect(consoleLogSpy).toHaveBeenCalledWith('[MyModule]', 'Test');
388
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('MyModule'), 'Test');
385
389
  });
386
390
 
387
391
  it('should support different module names', () => {
@@ -391,8 +395,8 @@ describe('Logger', () => {
391
395
  logger1.info('From 1');
392
396
  logger2.info('From 2');
393
397
 
394
- expect(consoleLogSpy).toHaveBeenCalledWith('[Module1]', 'From 1');
395
- expect(consoleLogSpy).toHaveBeenCalledWith('[Module2]', 'From 2');
398
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Module1'), 'From 1');
399
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Module2'), 'From 2');
396
400
  });
397
401
  });
398
402
 
@@ -402,7 +406,7 @@ describe('Logger', () => {
402
406
 
403
407
  logger.info('Test');
404
408
 
405
- expect(consoleLogSpy).toHaveBeenCalledWith('[null]', 'Test');
409
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('null'), 'Test');
406
410
  });
407
411
 
408
412
  it('should handle undefined level (use default)', () => {
@@ -419,7 +423,7 @@ describe('Logger', () => {
419
423
 
420
424
  logger.info(longMessage);
421
425
 
422
- expect(consoleLogSpy).toHaveBeenCalledWith('[Test]', longMessage);
426
+ expect(consoleLogSpy).toHaveBeenCalledWith(ts('Test'), longMessage);
423
427
  });
424
428
 
425
429
  it('should handle circular references in objects', () => {