@bizjournals/js-storage 0.0.1
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/README.md +0 -0
- package/babel.config.js +12 -0
- package/lib/abstract/__mocks__/mock-storage.js +19 -0
- package/lib/abstract/__tests__/storage.clear.spec.js +30 -0
- package/lib/abstract/__tests__/storage.constructor.spec.js +52 -0
- package/lib/abstract/__tests__/storage.expirationkey.spec.js +22 -0
- package/lib/abstract/__tests__/storage.getitem.spec.js +71 -0
- package/lib/abstract/__tests__/storage.hasitem.spec.js +19 -0
- package/lib/abstract/__tests__/storage.issupported.spec.js +37 -0
- package/lib/abstract/__tests__/storage.namespacekey.spec.js +54 -0
- package/lib/abstract/__tests__/storage.parser.spec.js +36 -0
- package/lib/abstract/__tests__/storage.removeitem.spec.js +70 -0
- package/lib/abstract/__tests__/storage.resetattempts.spec.js +20 -0
- package/lib/abstract/__tests__/storage.setitem.spec.js +101 -0
- package/lib/abstract/__tests__/storage.translator.spec.js +25 -0
- package/lib/abstract/constants/index.js +3 -0
- package/lib/abstract/exceptions/index.js +1 -0
- package/lib/abstract/index.js +293 -0
- package/lib/local/__mocks__/local-storage.js +28 -0
- package/lib/local/__tests__/storage.local.spec.js +8 -0
- package/lib/local/index.js +24 -0
- package/lib/reference/__tests__/storagereference.general.spec.js +47 -0
- package/lib/reference/exceptions/index.js +1 -0
- package/lib/reference/index.js +67 -0
- package/lib/session/__tests__/storage.session.spec.js +8 -0
- package/lib/session/index.js +24 -0
- package/main.js +5 -0
- package/package.json +25 -0
package/README.md
ADDED
|
File without changes
|
package/babel.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
let store = {};
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
clear: jest.fn().mockImplementation(() => {
|
|
5
|
+
store = {};
|
|
6
|
+
}),
|
|
7
|
+
|
|
8
|
+
getItem: jest.fn().mockImplementation((key) => {
|
|
9
|
+
return store[key];
|
|
10
|
+
}),
|
|
11
|
+
|
|
12
|
+
setItem: jest.fn().mockImplementation((key, value) => {
|
|
13
|
+
store[key] = value;
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
removeItem: jest.fn().mockImplementation((key) => {
|
|
17
|
+
delete store[key];
|
|
18
|
+
})
|
|
19
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:clear', () => {
|
|
4
|
+
let storageAbstract;
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageAbstract = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('never fails or throws when storage is not set', () => {
|
|
11
|
+
delete storageAbstract.storage;
|
|
12
|
+
expect(() => {
|
|
13
|
+
storageAbstract.clear();
|
|
14
|
+
}).not.toThrow();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('always returns `this`', () => {
|
|
18
|
+
const test = storageAbstract.clear();
|
|
19
|
+
expect(test).toEqual(storageAbstract);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('runs the clear method on the storage system when defined', () => {
|
|
23
|
+
storageAbstract.storage = {
|
|
24
|
+
clear: jest.fn()
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
storageAbstract.clear();
|
|
28
|
+
expect(storageAbstract.storage.clear).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
import { DEFAULT_EXPIRATION, DEFAULT_NAMESPACE } from '../constants';
|
|
3
|
+
import { NAMESPACE_MUST_BE_STRING, EXPIRATION_MUST_BE_POSITIVE_INTEGER } from '../exceptions';
|
|
4
|
+
|
|
5
|
+
describe('core:class >> storageabstract:constructor', () => {
|
|
6
|
+
it('defaults to constant as it\'s namespace when none provided', () => {
|
|
7
|
+
const storageAbstract = new StorageAbstract();
|
|
8
|
+
expect(storageAbstract.namespace).toMatch(DEFAULT_NAMESPACE);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('accepts an object parameter with key namespace and sets it to this.namespace', () => {
|
|
12
|
+
const namespace = 'test';
|
|
13
|
+
const storageAbstract = new StorageAbstract({ namespace });
|
|
14
|
+
expect(storageAbstract.namespace).toMatch(namespace);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('coercies the namespace to lower case before setting', () => {
|
|
18
|
+
const namespace = 'TeSt';
|
|
19
|
+
const storageAbstract = new StorageAbstract({ namespace });
|
|
20
|
+
expect(storageAbstract.namespace).toMatch(namespace.toLowerCase());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('throws error when namespace provided is not a string', () => {
|
|
24
|
+
const namespaces = [true, {}, [], 12];
|
|
25
|
+
for (const namespace of namespaces) {
|
|
26
|
+
expect(() => {
|
|
27
|
+
new StorageAbstract({ namespace });
|
|
28
|
+
}).toThrow(NAMESPACE_MUST_BE_STRING);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('defaults to constant as it\'s expiration when none provided', () => {
|
|
33
|
+
const storageAbstract = new StorageAbstract();
|
|
34
|
+
expect(storageAbstract.expires).toEqual(DEFAULT_EXPIRATION);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('accepts an object parameter with key expires and sets it to this.expires', () => {
|
|
38
|
+
const expires = 3600 * 4;
|
|
39
|
+
const storageAbstract = new StorageAbstract({ expires });
|
|
40
|
+
expect(storageAbstract.expires).toEqual(expires);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('defaults the number of attempts to zero', () => {
|
|
44
|
+
const storageAbstract = new StorageAbstract();
|
|
45
|
+
expect(storageAbstract.attempts).toEqual(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('sets the storage to a default of null', () => {
|
|
49
|
+
const storageAbstract = new StorageAbstract();
|
|
50
|
+
expect(storageAbstract.storage).toEqual({});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
import { EXPIRES_KEY_SUFFIX } from '../constants';
|
|
3
|
+
|
|
4
|
+
describe('core:class >> storageabstract:namespacekey', () => {
|
|
5
|
+
let storageAbstract;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
storageAbstract = new StorageAbstract();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('calls the namespaceKey to generate the prefix', () => {
|
|
12
|
+
storageAbstract.namespaceKey = jest.fn();
|
|
13
|
+
storageAbstract.expirationKey('hey');
|
|
14
|
+
expect(storageAbstract.namespaceKey).toHaveBeenCalledTimes(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns a concatenated variation of the namespace and string key', () => {
|
|
18
|
+
let key = 'key';
|
|
19
|
+
expect(storageAbstract.expirationKey(key))
|
|
20
|
+
.toEqual(`${ storageAbstract.namespaceKey(key) }.${ EXPIRES_KEY_SUFFIX }`);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:getitem', () => {
|
|
4
|
+
let storageClassMock, mockStorage, key = 'test';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageClassMock = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
storageClassMock.namespaceKey = jest.fn().mockImplementation(key => key);
|
|
12
|
+
storageClassMock.parser = jest.fn().mockImplementation(data => data);
|
|
13
|
+
storageClassMock.cache = jest.fn().mockImplementation(data => data);
|
|
14
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => true);
|
|
15
|
+
storageClassMock.isItemExpired = jest.fn().mockImplementation(() => false);
|
|
16
|
+
|
|
17
|
+
mockStorage = {
|
|
18
|
+
getItem: jest.fn()
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
storageClassMock.storage = mockStorage;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns null when it is not supported', () => {
|
|
25
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => false);
|
|
26
|
+
|
|
27
|
+
expect(storageClassMock.getItem()).toEqual(null);
|
|
28
|
+
expect(storageClassMock.namespaceKey).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns null when the item is expired', () => {
|
|
32
|
+
storageClassMock.isItemExpired = jest.fn().mockImplementation(() => true);
|
|
33
|
+
|
|
34
|
+
expect(storageClassMock.getItem()).toEqual(null);
|
|
35
|
+
expect(storageClassMock.namespaceKey).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('calls `namespace` key to get a dynamically generated key', () => {
|
|
39
|
+
storageClassMock.namespaceKey = jest.fn().mockImplementation(() => {
|
|
40
|
+
throw new Error('Just a test fall through.');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
storageClassMock.getItem();
|
|
44
|
+
expect(storageClassMock.namespaceKey).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('calls getItem on the storage system when suspended data not available', () => {
|
|
48
|
+
storageClassMock.getItem(key);
|
|
49
|
+
expect(storageClassMock.storage.getItem).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(storageClassMock.storage.getItem).toHaveBeenCalledWith(key);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('calls the `parser` method with the data fetched from suspend or getItem', () => {
|
|
54
|
+
const data = { hey: 'you' };
|
|
55
|
+
|
|
56
|
+
const localStorage = {
|
|
57
|
+
getItem: jest.fn().mockImplementation(() => data)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
storageClassMock.storage = localStorage;
|
|
61
|
+
|
|
62
|
+
storageClassMock.getItem(key);
|
|
63
|
+
expect(storageClassMock.parser).toHaveBeenCalled();
|
|
64
|
+
expect(storageClassMock.parser).toHaveBeenCalledWith(data);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('caches the response data once obtained', () => {
|
|
68
|
+
storageClassMock.getItem(key);
|
|
69
|
+
expect(storageClassMock.cache).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:hasitem', () => {
|
|
4
|
+
let storageClassMock, key = 'test';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageClassMock = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
storageClassMock.getItem = jest.fn().mockImplementation(key => key);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('calls getItem to fetch the item from the storage system', () => {
|
|
15
|
+
storageClassMock.hasItem(key);
|
|
16
|
+
|
|
17
|
+
expect(storageClassMock.getItem).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
import mockStorage from '../__mocks__/mock-storage';
|
|
3
|
+
|
|
4
|
+
describe('core:class >> storageabstract:issupported', () => {
|
|
5
|
+
let storageAbstract;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
storageAbstract = new StorageAbstract();
|
|
9
|
+
mockStorage.setItem.mockClear();
|
|
10
|
+
mockStorage.removeItem.mockClear();
|
|
11
|
+
|
|
12
|
+
if (mockStorage._supported) {
|
|
13
|
+
delete mockStorage._supported;
|
|
14
|
+
}
|
|
15
|
+
storageAbstract.storage = mockStorage;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false when the storage system is defaulted', () => {
|
|
19
|
+
storageAbstract.storage = {};
|
|
20
|
+
expect(storageAbstract.isSupported()).toBeFalsy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns true when the storage system is valid and usable', () => {
|
|
24
|
+
storageAbstract.storage = mockStorage;
|
|
25
|
+
expect(storageAbstract.isSupported()).toBeTruthy();
|
|
26
|
+
expect(mockStorage.setItem).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(mockStorage.removeItem).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns a stored answer once the storage system has been checked', () => {
|
|
31
|
+
storageAbstract.storage = mockStorage;
|
|
32
|
+
let isSupported = storageAbstract.isSupported();
|
|
33
|
+
isSupported = storageAbstract.isSupported();
|
|
34
|
+
expect(mockStorage.setItem).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(mockStorage.removeItem).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
import { KEY_IS_REQUIRED_TO_FETCH_DATA } from '../exceptions';
|
|
3
|
+
|
|
4
|
+
describe('core:class >> storageabstract:namespacekey', () => {
|
|
5
|
+
let storageAbstract;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
storageAbstract = new StorageAbstract();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('throws an exception when a non-string is passed', () => {
|
|
12
|
+
let keys = [true, [], {}, 1];
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
expect(() => {
|
|
15
|
+
storageAbstract.namespaceKey(key);
|
|
16
|
+
}).toThrow(KEY_IS_REQUIRED_TO_FETCH_DATA);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns a concatenated variation of the namespace and string key', () => {
|
|
21
|
+
let key = 'key';
|
|
22
|
+
expect(storageAbstract.namespaceKey(key))
|
|
23
|
+
.toEqual(`${ storageAbstract.namespace }.${ key }`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('coerces the key to lower case before returning', () => {
|
|
27
|
+
let key = 'KEy';
|
|
28
|
+
expect(storageAbstract.namespaceKey(key))
|
|
29
|
+
.toEqual(`${ storageAbstract.namespace }.${ key.toLowerCase() }`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('extracts the namespace and key separator when present at the beginning of the string', () => {
|
|
33
|
+
const theRealKey = 'myrealkey';
|
|
34
|
+
let key = `${ storageAbstract.namespace }.${ theRealKey }`;
|
|
35
|
+
expect(storageAbstract.namespaceKey(key))
|
|
36
|
+
.toEqual(`${ storageAbstract.namespace }.${ theRealKey.toLowerCase() }`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not extract the namespace and key separator when present anywhere else in the string', () => {
|
|
40
|
+
const theRealKey = 'myrealkey';
|
|
41
|
+
let key = `a${ storageAbstract.namespace }.${ theRealKey }`;
|
|
42
|
+
expect(storageAbstract.namespaceKey(key))
|
|
43
|
+
.not.toEqual(`${ storageAbstract.namespace }.${ theRealKey.toLowerCase() }`);
|
|
44
|
+
expect(storageAbstract.namespaceKey(key))
|
|
45
|
+
.toEqual(`${ storageAbstract.namespace }.${ key.toLowerCase() }`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('does not extract the namespace when missing the key separator', () => {
|
|
49
|
+
const theRealKey = 'myrealkey';
|
|
50
|
+
let key = `${ storageAbstract.namespace }${ theRealKey }`;
|
|
51
|
+
expect(storageAbstract.namespaceKey(key))
|
|
52
|
+
.toEqual(`${ storageAbstract.namespace }.${ key.toLowerCase() }`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:parser', () => {
|
|
4
|
+
let storageAbstract;
|
|
5
|
+
const copyParse = JSON.parse;
|
|
6
|
+
delete JSON.parse;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
storageAbstract = new StorageAbstract();
|
|
10
|
+
JSON.parse = jest.fn();
|
|
11
|
+
JSON.parse.mockClear();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns the raw data when not a string', () => {
|
|
15
|
+
const data = {
|
|
16
|
+
my: 'value'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(storageAbstract.parser(data)).toEqual(data);
|
|
20
|
+
expect(JSON.parse).not.toBeCalled();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('attempts to parse the item when it is a string', () => {
|
|
24
|
+
const data = 'somestuff';
|
|
25
|
+
|
|
26
|
+
storageAbstract.parser(data);
|
|
27
|
+
expect(JSON.parse).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws an exception when string is not valid json', () => {
|
|
31
|
+
JSON.parse = copyParse;
|
|
32
|
+
expect(() => {
|
|
33
|
+
storageAbstract.parser('wowowow');
|
|
34
|
+
}).toThrow();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:removeitem', () => {
|
|
4
|
+
let storageClassMock, mockStorage, key = 'test';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageClassMock = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => true);
|
|
12
|
+
storageClassMock.namespaceKey = jest.fn().mockImplementation(key => key);
|
|
13
|
+
storageClassMock.expirationKey = jest.fn().mockImplementation(key => `${key}.expires`);
|
|
14
|
+
storageClassMock.resetAttempts = jest.fn().mockImplementation(() => {
|
|
15
|
+
storageClassMock.attempts = 0;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
mockStorage = {
|
|
19
|
+
removeItem: jest.fn()
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
storageClassMock.attempts = 0;
|
|
23
|
+
storageClassMock.storage = mockStorage;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('resets the attempts and halts when it is not supported', () => {
|
|
27
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => false);
|
|
28
|
+
storageClassMock.removeItem(key);
|
|
29
|
+
|
|
30
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(storageClassMock.storage.removeItem).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('resets the attempts and halts when attempts are >= 2', () => {
|
|
35
|
+
storageClassMock.attempts = 2;
|
|
36
|
+
storageClassMock.removeItem(key);
|
|
37
|
+
|
|
38
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(storageClassMock.storage.removeItem).not.toHaveBeenCalled();
|
|
40
|
+
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('attempts two times when encountering an error', () => {
|
|
44
|
+
const badStorage = {
|
|
45
|
+
removeItem: jest.fn().mockImplementation(() => {
|
|
46
|
+
throw new Error('testing code');
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
storageClassMock.storage = badStorage;
|
|
51
|
+
storageClassMock.removeItem();
|
|
52
|
+
|
|
53
|
+
expect(storageClassMock.storage.removeItem).toHaveBeenCalledTimes(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('removes both the item at namespace and expiration keys', () => {
|
|
57
|
+
storageClassMock.removeItem();
|
|
58
|
+
|
|
59
|
+
expect(storageClassMock.namespaceKey).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(storageClassMock.expirationKey).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(storageClassMock.storage.removeItem).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resets attempts to zero on successful return', () => {
|
|
65
|
+
storageClassMock.removeItem();
|
|
66
|
+
|
|
67
|
+
expect(storageClassMock.storage.removeItem).toHaveBeenCalledTimes(2);
|
|
68
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:resetattempts', () => {
|
|
4
|
+
let storageAbstract;
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageAbstract = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('resets the attempts to 0 when called', () => {
|
|
11
|
+
storageAbstract.attempts = 2;
|
|
12
|
+
|
|
13
|
+
storageAbstract.resetAttempts();
|
|
14
|
+
expect(storageAbstract.attempts).toEqual(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns `this`', () => {
|
|
18
|
+
expect(storageAbstract.resetAttempts()).toEqual(storageAbstract);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
describe('core:class >> storageabstract:setitem', () => {
|
|
4
|
+
let storageClassMock, mockStorage, key = 'test';
|
|
5
|
+
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
storageClassMock = new StorageAbstract();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => true);
|
|
12
|
+
storageClassMock.namespaceKey = jest.fn().mockImplementation(key => key);
|
|
13
|
+
storageClassMock.expirationKey = jest.fn().mockImplementation(key => key);
|
|
14
|
+
storageClassMock.translator = jest.fn().mockImplementation(data => data);
|
|
15
|
+
storageClassMock.timestamp = jest.fn().mockImplementation(() => 12345);
|
|
16
|
+
storageClassMock.cache = jest.fn().mockImplementation(data => data);
|
|
17
|
+
storageClassMock.clear = jest.fn().mockImplementation(() => storageClassMock);
|
|
18
|
+
storageClassMock.resetAttempts = jest.fn().mockImplementation(() => {
|
|
19
|
+
storageClassMock.attempts = 0;
|
|
20
|
+
return storageClassMock;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
mockStorage = {
|
|
24
|
+
setItem: jest.fn()
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
storageClassMock.attempts = 0;
|
|
28
|
+
storageClassMock.storage = mockStorage;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('resets the attempts and returns when it is not supported without continuing', () => {
|
|
32
|
+
storageClassMock.isSupported = jest.fn().mockImplementation(() => false);
|
|
33
|
+
storageClassMock.setItem();
|
|
34
|
+
|
|
35
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(storageClassMock.namespaceKey).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('resets the attempts and returns when attempts is >= 2 without continuing', () => {
|
|
40
|
+
storageClassMock.attempts = 3;
|
|
41
|
+
storageClassMock.setItem();
|
|
42
|
+
|
|
43
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
44
|
+
expect(storageClassMock.namespaceKey).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses a dynamically generated namespaceKey to set the item', () => {
|
|
48
|
+
storageClassMock.setItem(key, { data: true });
|
|
49
|
+
expect(storageClassMock.namespaceKey).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('calls the translator prior to attempting to set the item', () => {
|
|
53
|
+
storageClassMock.translator = jest.fn().mockImplementation(() => {
|
|
54
|
+
throw new Error('testing script error');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
storageClassMock.setItem(key, { data: true });
|
|
58
|
+
expect(storageClassMock.translator).toHaveBeenCalled();
|
|
59
|
+
expect(storageClassMock.storage.setItem).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('calls to set the item one time when not expired', () => {
|
|
63
|
+
const data = { it: true };
|
|
64
|
+
|
|
65
|
+
storageClassMock.setItem(key, data, false);
|
|
66
|
+
|
|
67
|
+
expect(storageClassMock.storage.setItem).toHaveBeenCalledTimes(1);
|
|
68
|
+
expect(storageClassMock.storage.setItem).toHaveBeenCalledWith(key, data);
|
|
69
|
+
expect(storageClassMock.expirationKey).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('calls set item again with the expiration key if defaulted to expire', () => {
|
|
73
|
+
const data = { it: true };
|
|
74
|
+
|
|
75
|
+
storageClassMock.setItem(key, data);
|
|
76
|
+
|
|
77
|
+
expect(storageClassMock.storage.setItem).toHaveBeenCalledTimes(2);
|
|
78
|
+
expect(storageClassMock.expirationKey).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('calls to cache the data with the key', () => {
|
|
82
|
+
const data = { it: true };
|
|
83
|
+
|
|
84
|
+
storageClassMock.setItem(key, data);
|
|
85
|
+
expect(storageClassMock.cache).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('calls clear when there is an error and attempts up to 2 times', () => {
|
|
89
|
+
storageClassMock.translator = jest.fn().mockImplementation(() => {
|
|
90
|
+
throw new Error('testing script error');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
storageClassMock.setItem(key, { data: true });
|
|
94
|
+
expect(storageClassMock.translator).toHaveBeenCalledTimes(2);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('calls to reset attempts to zero once completed', () => {
|
|
98
|
+
storageClassMock.setItem(key, { data: true });
|
|
99
|
+
expect(storageClassMock.resetAttempts).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import StorageAbstract from '../';
|
|
2
|
+
|
|
3
|
+
// Mocking JSON.parse failed in this version of Jest
|
|
4
|
+
describe('core:class >> storageabstract:translator', () => {
|
|
5
|
+
let storageAbstract;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
storageAbstract = new StorageAbstract();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('calls json stringify when passed an object', () => {
|
|
12
|
+
const data = {
|
|
13
|
+
my: 'value'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
expect(storageAbstract.translator(data))
|
|
17
|
+
.toEqual(JSON.stringify(data));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns a string back exactly as is', () => {
|
|
21
|
+
const data = 'somestuff';
|
|
22
|
+
|
|
23
|
+
expect(storageAbstract.translator(data)).toEqual(data);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const KEY_IS_REQUIRED_TO_FETCH_DATA = 'A key must be provided to obtain data from the store.';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { empty } from "@bizjournals/js-utilities";
|
|
2
|
+
import { DEFAULT_EXPIRATION, DEFAULT_NAMESPACE, EXPIRES_KEY_SUFFIX } from "./constants";
|
|
3
|
+
import { KEY_IS_REQUIRED_TO_FETCH_DATA } from "./exceptions";
|
|
4
|
+
|
|
5
|
+
// module scoped caching object
|
|
6
|
+
// does share data between session / local / any other
|
|
7
|
+
// If this becomes necessary attach it to the storage object
|
|
8
|
+
const cache = {};
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
|--------------------------------------------------------------------------
|
|
12
|
+
| Storage Abstract / Contract
|
|
13
|
+
|--------------------------------------------------------------------------
|
|
14
|
+
|
|
|
15
|
+
| This is the base storage class that is extended for localStorage and
|
|
16
|
+
| sessionStorage. In time we may look to use this for InnoDB as well.
|
|
17
|
+
|
|
|
18
|
+
|
|
|
19
|
+
*/
|
|
20
|
+
export default class StorageAbstract {
|
|
21
|
+
/**
|
|
22
|
+
* Constructor takes in options for namespace and expiration.
|
|
23
|
+
* Session storage is the default storage type.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} options
|
|
26
|
+
* @constructor
|
|
27
|
+
*/
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.namespace = options.namespace || DEFAULT_NAMESPACE;
|
|
30
|
+
this.expires = options.expires || DEFAULT_EXPIRATION;
|
|
31
|
+
this.attempts = 0;
|
|
32
|
+
|
|
33
|
+
this.namespace = this.namespace.toLowerCase();
|
|
34
|
+
|
|
35
|
+
cache[this.namespace] = cache[this.namespace] || {};
|
|
36
|
+
|
|
37
|
+
this.storage = {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets an item from storage.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @returns {(Array|Number|Object|String)}
|
|
45
|
+
*/
|
|
46
|
+
getItem(key) {
|
|
47
|
+
if (!this.isSupported() || this.isItemExpired(key)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let response = null;
|
|
52
|
+
|
|
53
|
+
if (cache[this.namespace][key]) {
|
|
54
|
+
return cache[this.namespace][key];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const generatedKey = this.namespaceKey(key);
|
|
59
|
+
|
|
60
|
+
response = this.parser(cache[generatedKey] || this.storage.getItem(generatedKey));
|
|
61
|
+
|
|
62
|
+
this.cache(key, response);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// fall through
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses the data returned from the storage system
|
|
72
|
+
*
|
|
73
|
+
* @param data
|
|
74
|
+
* @returns {any}
|
|
75
|
+
*/
|
|
76
|
+
parser(data) {
|
|
77
|
+
return typeof data === "string" ? JSON.parse(data) : data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Translates the data before setting to the storage system
|
|
82
|
+
*
|
|
83
|
+
* @param value
|
|
84
|
+
* @returns {any}
|
|
85
|
+
*/
|
|
86
|
+
translator(value) {
|
|
87
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sets an item in the storage system.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} key
|
|
94
|
+
* @param {(array|object|number|string)} value
|
|
95
|
+
* @param {boolean} expires
|
|
96
|
+
*/
|
|
97
|
+
setItem(key, value, expires = true) {
|
|
98
|
+
if (!this.isSupported() || this.attempts >= 2) {
|
|
99
|
+
return this.resetAttempts();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const generatedKey = this.namespaceKey(key);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
this.storage.setItem(generatedKey, this.translator(value));
|
|
106
|
+
|
|
107
|
+
if (expires) {
|
|
108
|
+
this.storage.setItem(this.expirationKey(key), String(this.timestamp(this.expires)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.cache(key, value);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
this.attempts++;
|
|
114
|
+
this.setItem(key, value, expires);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return this.resetAttempts();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Removes an item from the storage system.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} key
|
|
124
|
+
*/
|
|
125
|
+
removeItem(key) {
|
|
126
|
+
if (!this.isSupported() || this.attempts >= 2) {
|
|
127
|
+
return this.resetAttempts();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
this.storage.removeItem(this.namespaceKey(key));
|
|
132
|
+
this.storage.removeItem(this.expirationKey(key));
|
|
133
|
+
|
|
134
|
+
this.free(key);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
this.attempts++;
|
|
137
|
+
this.removeItem(key);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return this.resetAttempts();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resets the number of attempt operations to zero.
|
|
145
|
+
*
|
|
146
|
+
* @returns {StorageAbstract}
|
|
147
|
+
*/
|
|
148
|
+
resetAttempts() {
|
|
149
|
+
this.attempts = 0;
|
|
150
|
+
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clears all items from storage.
|
|
156
|
+
*/
|
|
157
|
+
clear() {
|
|
158
|
+
try {
|
|
159
|
+
this.storage.clear();
|
|
160
|
+
|
|
161
|
+
cache[this.namespace] = {};
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// fall through
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks to see if an item exists in local storage.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} key
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
hasItem(key) {
|
|
176
|
+
return !empty(this.getItem(key));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Calculates the expires for an item.
|
|
181
|
+
*
|
|
182
|
+
* @param {number} offset
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
timestamp(offset = 0) {
|
|
186
|
+
return Math.ceil(Date.now() / 1000) + offset;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates the key used for storing information in the storage
|
|
191
|
+
*
|
|
192
|
+
* @param {string} key
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
namespaceKey(key) {
|
|
196
|
+
if (typeof key !== "string") {
|
|
197
|
+
throw new Error(KEY_IS_REQUIRED_TO_FETCH_DATA);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (key.indexOf(`${this.namespace}.`) === 0) {
|
|
201
|
+
key = key.slice(this.namespace.length + 1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `${this.namespace}.${key.toLowerCase()}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Creates the expires key used for testing storage usage
|
|
209
|
+
*
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
expirationKey(key) {
|
|
213
|
+
return `${this.namespaceKey(key)}.${EXPIRES_KEY_SUFFIX}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Checks to see if an item is expired and removes it if so.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} key
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
isItemExpired(key) {
|
|
223
|
+
if (!this.isSupported()) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
let value = this.storage.getItem(this.expirationKey(key));
|
|
229
|
+
|
|
230
|
+
if (value && "length" in value && value.length) {
|
|
231
|
+
let expires = parseInt(value, 10);
|
|
232
|
+
|
|
233
|
+
if (expires - this.timestamp() <= 0) {
|
|
234
|
+
this.removeItem(key);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
// fall through
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Stores the data in memory.
|
|
247
|
+
*
|
|
248
|
+
* @param key
|
|
249
|
+
* @param value
|
|
250
|
+
*/
|
|
251
|
+
cache(key, value) {
|
|
252
|
+
cache[this.namespace][key] = value;
|
|
253
|
+
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Frees the data from memory.
|
|
259
|
+
*
|
|
260
|
+
* @param key
|
|
261
|
+
* @param value
|
|
262
|
+
*/
|
|
263
|
+
free(key) {
|
|
264
|
+
if (cache[this.namespace][key]) {
|
|
265
|
+
delete cache[this.namespace][key];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks to see if the storage system is supported
|
|
273
|
+
*
|
|
274
|
+
* @returns {boolean}
|
|
275
|
+
*/
|
|
276
|
+
isSupported() {
|
|
277
|
+
if (typeof this.storage._supported === "boolean") {
|
|
278
|
+
return this.storage._supported;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const test = `${this.namespaceKey("storage")}.test`;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
this.storage.setItem(test, test);
|
|
285
|
+
this.storage.removeItem(test);
|
|
286
|
+
this.storage._supported = true;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
this.storage._supported = false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this.storage._supported;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import StorageAbstract from '../../abstract';
|
|
2
|
+
|
|
3
|
+
export default class LocalStorageMock extends StorageAbstract {
|
|
4
|
+
constructor (options) {
|
|
5
|
+
super(options);
|
|
6
|
+
this.store = {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
clear () {
|
|
10
|
+
this.store = {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getItem (key) {
|
|
14
|
+
return this.store[key] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setItem (key, value, expires = true) {
|
|
18
|
+
this.store[key] = value;
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
removeItem (key) {
|
|
23
|
+
delete this.store[key];
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class InvalidStorageMock {}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import LocalStorage from '../';
|
|
2
|
+
|
|
3
|
+
describe('bizjournals / core:class >> localstorage:constructor', () => {
|
|
4
|
+
it('builds the object with localStorage set as the storage system', () => {
|
|
5
|
+
const localStorage = new LocalStorage();
|
|
6
|
+
expect(localStorage.storage).toEqual(window.localStorage);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import StorageAbstract from "../abstract";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
| Local Storage
|
|
6
|
+
|--------------------------------------------------------------------------
|
|
7
|
+
| This is the base class for hooking into local storage. There isn't much
|
|
8
|
+
| overlap here if you just keep instantiating, so there isn't much need
|
|
9
|
+
| to singleton this.
|
|
10
|
+
|
|
|
11
|
+
|
|
|
12
|
+
*/
|
|
13
|
+
export default class LocalStorage extends StorageAbstract {
|
|
14
|
+
/**
|
|
15
|
+
* Create a local storage adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
*/
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super(options);
|
|
21
|
+
|
|
22
|
+
this.storage = window.localStorage;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import StorageReference from '../';
|
|
2
|
+
import LocalStorageMock, { InvalidStorageMock } from '../../local/__mocks__/local-storage';
|
|
3
|
+
import { INVALID_CONSTRUCTOR_PARAMETERS } from '../exceptions';
|
|
4
|
+
|
|
5
|
+
describe('bizjournals / core:class >> storagereference', () => {
|
|
6
|
+
it('throws error when the cachekey is not a string', () => {
|
|
7
|
+
expect(() => {
|
|
8
|
+
new StorageReference([], new LocalStorageMock());
|
|
9
|
+
}).toThrow(INVALID_CONSTRUCTOR_PARAMETERS);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('throws an error when the storage system does not adhere to contract', () => {
|
|
13
|
+
expect(() => {
|
|
14
|
+
new StorageReference('astring', new InvalidStorageMock());
|
|
15
|
+
}).toThrow(INVALID_CONSTRUCTOR_PARAMETERS);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not throw an error when constructor parameters are valid', () => {
|
|
19
|
+
expect(() => {
|
|
20
|
+
new StorageReference('astring', new LocalStorageMock());
|
|
21
|
+
}).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('assigns the constructor parameters to variables for reference', () => {
|
|
25
|
+
const cacheKey = 'astring';
|
|
26
|
+
const localStorage = new LocalStorageMock();
|
|
27
|
+
|
|
28
|
+
const storage = new StorageReference(cacheKey, localStorage);
|
|
29
|
+
|
|
30
|
+
expect(storage.cacheKey).toEqual(cacheKey);
|
|
31
|
+
expect(storage.storageSystem).toEqual(localStorage);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('can save and obtain the data using the key from storage', () => {
|
|
35
|
+
const cacheKey = 'astring';
|
|
36
|
+
const localStorage = new LocalStorageMock();
|
|
37
|
+
const mockData = { luke: 'skywalker', plot: 'ruined' };
|
|
38
|
+
|
|
39
|
+
const storage = new StorageReference(cacheKey, localStorage);
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
storage.saveAs(mockData);
|
|
43
|
+
}).not.toThrow();
|
|
44
|
+
|
|
45
|
+
expect(storage.obtain()).toEqual(mockData);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const INVALID_CONSTRUCTOR_PARAMETERS = 'Invalid construction of cache system. Please refer to documentation.';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import StorageAbstract from "../abstract";
|
|
2
|
+
import { INVALID_CONSTRUCTOR_PARAMETERS } from "./exceptions";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
|--------------------------------------------------------------------------
|
|
6
|
+
| Storage Reference
|
|
7
|
+
|--------------------------------------------------------------------------
|
|
8
|
+
| This is a reference to something in storage. Each item has a key
|
|
9
|
+
| belonging to a specific storage system. These storage systems have a
|
|
10
|
+
| common abstract / contract that gives them the same expected pattern.
|
|
11
|
+
| This is the adapter.
|
|
12
|
+
|
|
|
13
|
+
|
|
|
14
|
+
*/
|
|
15
|
+
export default class StorageReference {
|
|
16
|
+
/**
|
|
17
|
+
* Creates an instance of a StorageReference system, which needs a cacheKey and
|
|
18
|
+
* a Storage system (StorageAbstract).
|
|
19
|
+
*
|
|
20
|
+
* @param {string} cacheKey
|
|
21
|
+
* @param {StorageAbstract} storageSystem
|
|
22
|
+
* @constructor
|
|
23
|
+
* @return {StorageReference}
|
|
24
|
+
*/
|
|
25
|
+
constructor(cacheKey, storageSystem) {
|
|
26
|
+
if (typeof cacheKey !== "string" || !(storageSystem instanceof StorageAbstract)) {
|
|
27
|
+
throw new Error(INVALID_CONSTRUCTOR_PARAMETERS);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.cacheKey = cacheKey;
|
|
31
|
+
this.storageSystem = storageSystem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Pulls data from the local store or cached resource.
|
|
36
|
+
*
|
|
37
|
+
* @returns {*}
|
|
38
|
+
*/
|
|
39
|
+
obtain() {
|
|
40
|
+
return this.storageSystem.getItem(this.cacheKey);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Destroys the cache reference with the cache key
|
|
45
|
+
*
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
bust() {
|
|
49
|
+
this.storageSystem.removeItem(this.cacheKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Saves the locally stored cache to the caching system. This is
|
|
54
|
+
* differed via a Promise because the timing of this can be
|
|
55
|
+
* performed asynchronously.
|
|
56
|
+
*
|
|
57
|
+
* @param data
|
|
58
|
+
* @returns {StorageReference}
|
|
59
|
+
*/
|
|
60
|
+
saveAs(data) {
|
|
61
|
+
if (data) {
|
|
62
|
+
this.storageSystem.setItem(this.cacheKey, data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import SessionStorage from "../";
|
|
2
|
+
|
|
3
|
+
describe("bizjournals / core:class >> sessionstorage:constructor", () => {
|
|
4
|
+
it("builds the object with sessionStorage set as the storage system", () => {
|
|
5
|
+
const sessionStorage = new SessionStorage();
|
|
6
|
+
expect(sessionStorage.storage).toEqual(window.sessionStorage);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import StorageAbstract from "../abstract";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
| Session Storage
|
|
6
|
+
|--------------------------------------------------------------------------
|
|
7
|
+
| This is the base class for hooking into session storage. There isn't much
|
|
8
|
+
| overlap here if you just keep instantiating, so there isn't much need
|
|
9
|
+
| to singleton this.
|
|
10
|
+
|
|
|
11
|
+
|
|
|
12
|
+
*/
|
|
13
|
+
export default class SessionStorage extends StorageAbstract {
|
|
14
|
+
/**
|
|
15
|
+
* Create a session storage adapter.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} [options]
|
|
18
|
+
*/
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super(options);
|
|
21
|
+
|
|
22
|
+
this.storage = window.sessionStorage;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/main.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bizjournals/js-storage",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ACBJ javascript storage classes",
|
|
5
|
+
"repository": {
|
|
6
|
+
"git": "https://gitlab.bizjournals.com/bizjournals/js-storage"
|
|
7
|
+
},
|
|
8
|
+
"main": "main.js",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest --transformIgnorePatterns \"node_modules/(?!(@bizjournals))/\""
|
|
13
|
+
},
|
|
14
|
+
"keywords": [],
|
|
15
|
+
"author": "DPD",
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@babel/preset-env": "^7.9.5",
|
|
19
|
+
"babel-jest": "^25.4.0",
|
|
20
|
+
"jest": "^25.4.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bizjournals/js-utilities": "0.0.1"
|
|
24
|
+
}
|
|
25
|
+
}
|