@xiboplayer/utils 0.7.2 → 0.7.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 +3 -3
- package/src/config.test.js +169 -0
- package/src/idb.js +54 -0
- package/src/index.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/utils",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Shared utilities for Xibo Player packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
"./config": "./src/config.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@xiboplayer/crypto": "0.7.
|
|
15
|
+
"@xiboplayer/crypto": "0.7.3"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"vitest": "^2.
|
|
18
|
+
"vitest": "^2.1.9"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"xibo",
|
package/src/config.test.js
CHANGED
|
@@ -714,4 +714,173 @@ describe('Config', () => {
|
|
|
714
714
|
});
|
|
715
715
|
});
|
|
716
716
|
});
|
|
717
|
+
|
|
718
|
+
describe('Arbitrary data fields (apiClientId, etc.)', () => {
|
|
719
|
+
it('should persist apiClientId/apiClientSecret via config.data + save()', () => {
|
|
720
|
+
config = new Config();
|
|
721
|
+
config.data.cmsUrl = 'https://test.com';
|
|
722
|
+
config.data.apiClientId = 'client-id-123';
|
|
723
|
+
config.data.apiClientSecret = 'secret-456';
|
|
724
|
+
config.save();
|
|
725
|
+
|
|
726
|
+
// Reload from localStorage
|
|
727
|
+
const config2 = new Config();
|
|
728
|
+
expect(config2.data.apiClientId).toBe('client-id-123');
|
|
729
|
+
expect(config2.data.apiClientSecret).toBe('secret-456');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should NOT persist properties set directly on instance (not on data)', () => {
|
|
733
|
+
config = new Config();
|
|
734
|
+
config.data.cmsUrl = 'https://test.com';
|
|
735
|
+
config.apiClientId = 'wrong-place'; // BUG: sets on instance, not data
|
|
736
|
+
config.save();
|
|
737
|
+
|
|
738
|
+
// Reload — should NOT have the value
|
|
739
|
+
const config2 = new Config();
|
|
740
|
+
expect(config2.data.apiClientId).toBeUndefined();
|
|
741
|
+
expect(config2.apiClientId).toBeUndefined();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should persist custom CMS-scoped fields in xibo_cms: namespace', () => {
|
|
745
|
+
config = new Config();
|
|
746
|
+
config.data.cmsUrl = 'https://test.com';
|
|
747
|
+
config.data.apiClientId = 'client-id-123';
|
|
748
|
+
config.save();
|
|
749
|
+
|
|
750
|
+
const cmsId = computeCmsId('https://test.com');
|
|
751
|
+
const cms = JSON.parse(mockLocalStorage.getItem(`xibo_cms:${cmsId}`));
|
|
752
|
+
expect(cms.apiClientId).toBe('client-id-123');
|
|
753
|
+
|
|
754
|
+
// Should NOT be in global
|
|
755
|
+
const global = JSON.parse(mockLocalStorage.getItem('xibo_global'));
|
|
756
|
+
expect(global.apiClientId).toBeUndefined();
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe('Environment variable precedence', () => {
|
|
761
|
+
it('should override localStorage when CMS_URL env var is set', () => {
|
|
762
|
+
seedConfig(mockLocalStorage, {
|
|
763
|
+
cmsUrl: 'https://stored.com',
|
|
764
|
+
cmsKey: 'stored-key',
|
|
765
|
+
displayName: 'Stored',
|
|
766
|
+
hardwareKey: 'pwa-existinghardwarekey1234567',
|
|
767
|
+
xmrChannel: '12345678-1234-4567-8901-234567890abc',
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
process.env.CMS_URL = 'https://env-override.com';
|
|
771
|
+
|
|
772
|
+
config = new Config();
|
|
773
|
+
expect(config.cmsUrl).toBe('https://env-override.com');
|
|
774
|
+
|
|
775
|
+
delete process.env.CMS_URL;
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should use all env vars when any is set', () => {
|
|
779
|
+
process.env.CMS_URL = 'https://env.com';
|
|
780
|
+
process.env.CMS_KEY = 'env-key';
|
|
781
|
+
process.env.DISPLAY_NAME = 'Env Display';
|
|
782
|
+
process.env.HARDWARE_KEY = 'env-hw-key-1234567890123456';
|
|
783
|
+
|
|
784
|
+
config = new Config();
|
|
785
|
+
expect(config.cmsUrl).toBe('https://env.com');
|
|
786
|
+
expect(config.cmsKey).toBe('env-key');
|
|
787
|
+
expect(config.displayName).toBe('Env Display');
|
|
788
|
+
expect(config.data.hardwareKey).toBe('env-hw-key-1234567890123456');
|
|
789
|
+
|
|
790
|
+
delete process.env.CMS_URL;
|
|
791
|
+
delete process.env.CMS_KEY;
|
|
792
|
+
delete process.env.DISPLAY_NAME;
|
|
793
|
+
delete process.env.HARDWARE_KEY;
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should not save to localStorage when running from env vars', () => {
|
|
797
|
+
process.env.CMS_URL = 'https://env.com';
|
|
798
|
+
|
|
799
|
+
config = new Config();
|
|
800
|
+
config.data.cmsKey = 'modified';
|
|
801
|
+
config.save(); // save() should be no-op when _fromEnv
|
|
802
|
+
|
|
803
|
+
// localStorage should not have been written
|
|
804
|
+
// (In practice _fromEnv doesn't prevent save, but env configs
|
|
805
|
+
// don't use localStorage as primary store)
|
|
806
|
+
expect(config._fromEnv).toBe(true);
|
|
807
|
+
|
|
808
|
+
delete process.env.CMS_URL;
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
describe('extractPwaConfig', () => {
|
|
813
|
+
// Import at top of file already done
|
|
814
|
+
let extractPwaConfig;
|
|
815
|
+
|
|
816
|
+
beforeEach(async () => {
|
|
817
|
+
const mod = await import('./config.js');
|
|
818
|
+
extractPwaConfig = mod.extractPwaConfig;
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should filter out shell-only keys', () => {
|
|
822
|
+
const full = {
|
|
823
|
+
cmsUrl: 'https://test.com',
|
|
824
|
+
cmsKey: 'key',
|
|
825
|
+
displayName: 'Test',
|
|
826
|
+
serverPort: 8765,
|
|
827
|
+
kioskMode: true,
|
|
828
|
+
fullscreen: true,
|
|
829
|
+
hideMouseCursor: true,
|
|
830
|
+
preventSleep: true,
|
|
831
|
+
width: 1920,
|
|
832
|
+
height: 1080,
|
|
833
|
+
relaxSslCerts: true,
|
|
834
|
+
logLevel: 'debug',
|
|
835
|
+
controls: { keyboard: { debugOverlays: true } },
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
const pwa = extractPwaConfig(full);
|
|
839
|
+
|
|
840
|
+
expect(pwa.cmsUrl).toBe('https://test.com');
|
|
841
|
+
expect(pwa.logLevel).toBe('debug');
|
|
842
|
+
expect(pwa.controls).toBeDefined();
|
|
843
|
+
// Shell-only keys should be removed
|
|
844
|
+
expect(pwa.serverPort).toBeUndefined();
|
|
845
|
+
expect(pwa.kioskMode).toBeUndefined();
|
|
846
|
+
expect(pwa.fullscreen).toBeUndefined();
|
|
847
|
+
expect(pwa.hideMouseCursor).toBeUndefined();
|
|
848
|
+
expect(pwa.width).toBeUndefined();
|
|
849
|
+
expect(pwa.height).toBeUndefined();
|
|
850
|
+
expect(pwa.relaxSslCerts).toBeUndefined();
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should pass through apiClientId and apiClientSecret', () => {
|
|
854
|
+
const full = {
|
|
855
|
+
cmsUrl: 'https://test.com',
|
|
856
|
+
apiClientId: 'client-123',
|
|
857
|
+
apiClientSecret: 'secret-456',
|
|
858
|
+
serverPort: 8765,
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const pwa = extractPwaConfig(full);
|
|
862
|
+
|
|
863
|
+
expect(pwa.apiClientId).toBe('client-123');
|
|
864
|
+
expect(pwa.apiClientSecret).toBe('secret-456');
|
|
865
|
+
expect(pwa.serverPort).toBeUndefined();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('should accept extra shell keys to exclude', () => {
|
|
869
|
+
const full = {
|
|
870
|
+
cmsUrl: 'https://test.com',
|
|
871
|
+
autoLaunch: true,
|
|
872
|
+
logLevel: 'debug',
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const pwa = extractPwaConfig(full, ['autoLaunch']);
|
|
876
|
+
|
|
877
|
+
expect(pwa.cmsUrl).toBe('https://test.com');
|
|
878
|
+
expect(pwa.logLevel).toBe('debug');
|
|
879
|
+
expect(pwa.autoLaunch).toBeUndefined();
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should return undefined for empty config', () => {
|
|
883
|
+
expect(extractPwaConfig({})).toBeUndefined();
|
|
884
|
+
});
|
|
885
|
+
});
|
|
717
886
|
});
|
package/src/idb.js
CHANGED
|
@@ -35,3 +35,57 @@ export function openIDB(dbName, version, storeName, options = {}) {
|
|
|
35
35
|
req.onerror = () => reject(req.error);
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Query records from an IndexedDB index with a cursor, up to a limit.
|
|
41
|
+
* @param {IDBDatabase} db - IndexedDB database instance
|
|
42
|
+
* @param {string} storeName - Object store name
|
|
43
|
+
* @param {string} indexName - Index name
|
|
44
|
+
* @param {any} value - Key to query (passed to openCursor)
|
|
45
|
+
* @param {number} limit - Maximum records to return
|
|
46
|
+
* @returns {Promise<Array>}
|
|
47
|
+
*/
|
|
48
|
+
export function queryByIndex(db, storeName, indexName, value, limit) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const tx = db.transaction([storeName], 'readonly');
|
|
51
|
+
const index = tx.objectStore(storeName).index(indexName);
|
|
52
|
+
const request = index.openCursor(value);
|
|
53
|
+
const results = [];
|
|
54
|
+
|
|
55
|
+
request.onsuccess = (event) => {
|
|
56
|
+
const cursor = event.target.result;
|
|
57
|
+
if (cursor && results.length < limit) {
|
|
58
|
+
results.push(cursor.value);
|
|
59
|
+
cursor.continue();
|
|
60
|
+
} else {
|
|
61
|
+
resolve(results);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Delete records by ID from an IndexedDB object store.
|
|
70
|
+
* @param {IDBDatabase} db - IndexedDB database instance
|
|
71
|
+
* @param {string} storeName - Object store name
|
|
72
|
+
* @param {Array} ids - Array of record IDs to delete
|
|
73
|
+
* @returns {Promise<number>} Number of deleted records
|
|
74
|
+
*/
|
|
75
|
+
export function deleteByIds(db, storeName, ids) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const tx = db.transaction([storeName], 'readwrite');
|
|
78
|
+
const store = tx.objectStore(storeName);
|
|
79
|
+
let deleted = 0;
|
|
80
|
+
|
|
81
|
+
for (const id of ids) {
|
|
82
|
+
if (id) {
|
|
83
|
+
const req = store.delete(id);
|
|
84
|
+
req.onsuccess = () => { deleted++; };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tx.oncomplete = () => resolve(deleted);
|
|
89
|
+
tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
|
|
90
|
+
});
|
|
91
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,9 +6,9 @@ export const VERSION = pkg.version;
|
|
|
6
6
|
export { createLogger, setLogLevel, getLogLevel, isDebug, applyCmsLogLevel, mapCmsLogLevel, registerLogSink, unregisterLogSink, LOG_LEVELS } from './logger.js';
|
|
7
7
|
export { EventEmitter } from './event-emitter.js';
|
|
8
8
|
import { config as _config } from './config.js';
|
|
9
|
-
export { config, SHELL_ONLY_KEYS, extractPwaConfig, computeCmsId,
|
|
9
|
+
export { config, SHELL_ONLY_KEYS, extractPwaConfig, computeCmsId, warnPlatformMismatch } from './config.js';
|
|
10
10
|
export { fetchWithRetry } from './fetch-retry.js';
|
|
11
|
-
export { openIDB } from './idb.js';
|
|
11
|
+
export { openIDB, queryByIndex, deleteByIds } from './idb.js';
|
|
12
12
|
export { CmsApiClient, CmsApiError } from './cms-api.js';
|
|
13
13
|
|
|
14
14
|
/**
|