diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js index 9a8ea316a..97bf69fe9 100644 --- a/src/modules/serverSideStorage.js +++ b/src/modules/serverSideStorage.js @@ -3,6 +3,7 @@ import { isEqual, cloneDeep, set, + unset, get, clamp, flatten, @@ -35,7 +36,8 @@ export const defaultState = { _journal: [], simple: { dontShowUpdateNotifs: false, - collapseNav: false + collapseNav: false, + filters: {} }, collections: { pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'], @@ -78,11 +80,16 @@ const _verifyPrefs = (state) => { simple: {}, collections: {} } + + // Simple Object.entries(defaultState.prefsStorage.simple).forEach(([k, v]) => { if (typeof v === 'number' || typeof v === 'boolean') return + if (typeof v === 'object' && v != null) return console.warn(`Preference simple.${k} as invalid type, reinitializing`) set(state.prefsStorage.simple, k, defaultState.prefsStorage.simple[k]) }) + + // Collections Object.entries(defaultState.prefsStorage.collections).forEach(([k, v]) => { if (Array.isArray(v)) return console.warn(`Preference collections.${k} as invalid type, reinitializing`) @@ -224,8 +231,27 @@ export const _mergePrefs = (recent, stale) => { } switch (operation) { case 'set': + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error('Illegal operation "set" on a collection') + return + } + if (path.split(/\./g).length <= 1) { + console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) + return + } set(resultOutput, path, args[0]) break + case 'unset': + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error('Illegal operation "unset" on a collection') + return + } + if (path.split(/\./g).length <= 2) { + console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + return + } + unset(resultOutput, path) + break case 'addToCollection': set(resultOutput, path, Array.from(new Set(get(resultOutput, path)).add(args[0]))) break @@ -380,7 +406,15 @@ export const mutations = { }, setPreference (state, { path, value }) { if (path.startsWith('_')) { - console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) + console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error(`Invalid operation 'set' for collection field '${path}', ignoring.`) + return + } + if (path.split(/\./g).length <= 1) { + console.error(`Calling set on depth <= 1 (path: ${path}) is not allowed`) return } set(state.prefsStorage, path, value) @@ -390,14 +424,46 @@ export const mutations = { ] state.dirty = true }, + unsetPreference (state, { path, value }) { + if (path.startsWith('_')) { + console.error(`Tried to edit internal (starts with _) field '${path}', ignoring.`) + return + } + if (path.startsWith('collections') || path.startsWith('objectCollections')) { + console.error(`Invalid operation 'unset' for collection field '${path}', ignoring.`) + return + } + if (path.split(/\./g).length <= 2) { + console.error(`Calling unset on depth <= 2 (path: ${path}) is not allowed`) + return + } + unset(state.prefsStorage, path, value) + state.prefsStorage._journal = [ + ...state.prefsStorage._journal, + { operation: 'unset', path, args: [], timestamp: Date.now() } + ] + state.dirty = true + }, addCollectionPreference (state, { path, value }) { if (path.startsWith('_')) { console.error(`tried to edit internal (starts with _) field '${path}', ignoring.`) return } - const collection = new Set(get(state.prefsStorage, path)) - collection.add(value) - set(state.prefsStorage, path, [...collection]) + if (path.startsWith('collections')) { + const collection = new Set(get(state.prefsStorage, path)) + collection.add(value) + set(state.prefsStorage, path, [...collection]) + } else if (path.startsWith('objectCollections')) { + const { _key } = value + if (!_key && typeof _key !== 'string') { + console.error('Object for storage is missing _key field! ignoring') + return + } + const collection = new Set(get(state.prefsStorage, path + '.index')) + collection.add(_key) + set(state.prefsStorage, path + '.index', [...collection]) + set(state.prefsStorage, path + '.data.' + _key, value) + } state.prefsStorage._journal = [ ...state.prefsStorage._journal, { operation: 'addToCollection', path, args: [value], timestamp: Date.now() } diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js index 1d4021a7a..986cf61e0 100644 --- a/test/unit/specs/modules/serverSideStorage.spec.js +++ b/test/unit/specs/modules/serverSideStorage.spec.js @@ -107,7 +107,7 @@ describe('The serverSideStorage module', () => { }) }) describe('setPreference', () => { - const { setPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations + const { setPreference, unsetPreference, updateCache, addCollectionPreference, removeCollectionPreference } = mutations it('should set preference and update journal log accordingly', () => { const state = cloneDeep(defaultState) @@ -160,6 +160,25 @@ describe('The serverSideStorage module', () => { expect(state.prefsStorage.collections.testing).to.eql([2]) expect(state.prefsStorage._journal.length).to.eql(2) }) + + it('should remove depth = 3 set/unset entries from journal', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.object.foo', value: 1 }) + unsetPreference(state, { path: 'simple.object.foo' }) + updateCache(state, { username: 'test' }) + expect(state.prefsStorage.simple.object).to.not.have.property('foo') + expect(state.prefsStorage._journal.length).to.eql(1) + }) + + it('should not allow unsetting depth <= 2', () => { + const state = cloneDeep(defaultState) + setPreference(state, { path: 'simple.object.foo', value: 1 }) + unsetPreference(state, { path: 'simple.object' }) + unsetPreference(state, { path: 'simple' }) + updateCache(state, { username: 'test' }) + expect(state.prefsStorage.simple.object).to.have.property('foo') + expect(state.prefsStorage._journal.length).to.eql(1) + }) }) }) @@ -315,6 +334,58 @@ describe('The serverSideStorage module', () => { ] }) }) + + it('should work with objects', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { lv2: { lv3: 'foo' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { lv2: { lv3: 'bar' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { lv2: { lv3: 'bar' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['bar'], timestamp: 4 } + ] + }) + }) + + it('should work with unset', () => { + expect( + _mergePrefs( + // RECENT + { + simple: { lv2: { lv3: 'foo' } }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'set', args: ['foo'], timestamp: 2 } + ] + }, + // STALE + { + simple: { lv2: {} }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 } + ] + } + ) + ).to.eql({ + simple: { lv2: {} }, + _journal: [ + { path: 'simple.lv2.lv3', operation: 'unset', args: [], timestamp: 4 } + ] + }) + }) }) describe('_resetFlags', () => {