From 0b8083ff16450edbf38193ca9f1ca4c94e7e824b Mon Sep 17 00:00:00 2001 From: Ian K Smith Date: Tue, 1 Oct 2019 12:10:48 -0600 Subject: [PATCH 1/2] Add 'restoreDefaults' feature --- package.json | 1 + src/lib.ts | 42 +++++++++++++++++++++++++++---- test/src/mocks/browser.ts | 3 +++ test/src/shims.d.ts | 1 + test/src/storage-proxy.spec.ts | 46 ++++++++++++++++++++++++++++++++-- yarn.lock | 5 ++++ 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 005068a..28f68b0 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@ikscodes/tslint-config": "^5.3.1", "alsatian": "^2.4.0", + "atob": "^2.1.2", "btoa": "^1.2.1", "microbundle": "^0.8.4", "mock-browser": "^0.92.14", diff --git a/src/lib.ts b/src/lib.ts index b838335..6eda80a 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -7,6 +7,7 @@ import onChange from 'on-change'; const namespaceSymbol = Symbol('namespaceSymbol'); const isStorageProxy = Symbol('isStorageProxy'); const storageTargetSymbol = Symbol('storageTargetSymbol'); +const defaultValuesSymbol = Symbol('defaultValuesSymbol'); const storageProxyIntegrityKey = '__storageProxyIntegrity'; /** Web storage targets: `localStorage` and `sessionStorage`. */ @@ -20,6 +21,7 @@ export type StorageProxy = Partial & { readonly [namespaceSymbol]: string; readonly [isStorageProxy]: true; readonly [storageTargetSymbol]: StorageTarget; + readonly [defaultValuesSymbol]: Partial; [storageProxyIntegrityKey]: string; }; @@ -32,10 +34,23 @@ export type StorageProxy = Partial & { * * @return Returns true if value is undefined, else false. */ -export function isUndefined(value: any): value is undefined { +function isUndefined(value: any): value is undefined { return value === undefined; } +/** + * Asserts that the given argument is a valid `StorageProxy` object, otherwise + * raising an error. + * + * @param value - Any value to test for validity as a `StorageProxy` object. + */ +function enforceStorageProxy(value?: any) { + // Argument must be a `StorageProxy` object. + if (!value || !value[isStorageProxy]) { + throw new TypeError('[storage-proxy] Supplied argument is not a `StorageProxy` object.'); + } +} + /** * Initializes the web storage interface. If no storage exists, we save an empty * object. @@ -103,6 +118,8 @@ function createProxy( storageProxy[key] = value; } } + + storageProxy[defaultValuesSymbol] = defaults; } return storageProxy; @@ -156,10 +173,7 @@ export const StorageProxy = { * @return `boolean` indicating whether the cache integrity is sound. */ verifyCache>(storageProxy: TStorageProxy, seed: string) { - // Argument must be a `StorageProxy` object. - if (!storageProxy[isStorageProxy]) { - throw new Error('[storage-proxy] Provided argument is not a `StorageProxy` object.'); - } + enforceStorageProxy(storageProxy); // Get a seed from the raw web storage data and decode it. const data = getDataFromStorage(storageProxy[namespaceSymbol], storageProxy[storageTargetSymbol]); @@ -182,8 +196,26 @@ export const StorageProxy = { * @param storageProxy - The storage proxy object to clear. */ clearStorage>(storageProxy: TStorageProxy) { + enforceStorageProxy(storageProxy); + for (const key of Object.keys(storageProxy)) { delete storageProxy[key]; } }, + + /** + * Restores the default values given to `StorageProxy.createLocalStorage()` + * and `StorageProxy.createSessionStorage()`. However, unlike when the + * `StorageProxy` was initially created, this function privelages the default + * values _over_ what is currently in `WebStorage`. + * + * @param storageProxy - The storage proxy object to restore to a default state. + */ + restoreDefaults>(storageProxy: TStorageProxy) { + enforceStorageProxy(storageProxy); + + for (const [key, value] of Object.entries(storageProxy[defaultValuesSymbol])) { + (storageProxy as any)[key] = value; + } + }, }; diff --git a/test/src/mocks/browser.ts b/test/src/mocks/browser.ts index 0d9d913..d24d57e 100644 --- a/test/src/mocks/browser.ts +++ b/test/src/mocks/browser.ts @@ -1,3 +1,4 @@ +import atob from 'atob'; import btoa from 'btoa'; import { mocks } from 'mock-browser'; @@ -10,6 +11,8 @@ const mb = new mocks.MockBrowser(); (global as any).window = mb.getWindow(); (global as any).btoa = btoa; (global as any).window.btoa = btoa; +(global as any).atob = atob; +(global as any).window.atob = atob; (global as any).location = mb.getLocation(); (global as any).navigator = mb.getNavigator(); (global as any).history = mb.getHistory(); diff --git a/test/src/shims.d.ts b/test/src/shims.d.ts index 941c9e3..64ce597 100644 --- a/test/src/shims.d.ts +++ b/test/src/shims.d.ts @@ -1,2 +1,3 @@ declare module 'mock-browser'; declare module 'btoa'; +declare module 'atob'; diff --git a/test/src/storage-proxy.spec.ts b/test/src/storage-proxy.spec.ts index 2cae530..3844cbf 100644 --- a/test/src/storage-proxy.spec.ts +++ b/test/src/storage-proxy.spec.ts @@ -12,6 +12,8 @@ const testStr = 'hello world'; const testObj = { monty: 'python', numbers: [1, 2, 3] }; const testArr = [1, 2, 3]; +const storageProxyIntegrityKey = '__storageProxyIntegrity'; + interface TestStorage { bar: string; baz: typeof testObj; @@ -43,8 +45,8 @@ function getItem(storageTarget: StorageTarget, path: string) { @TestFixture('StorageProxy Tests') export class StorageProxyTestFixture { - lStore: Partial; - sStore: Partial; + lStore: StorageProxy; + sStore: StorageProxy; @SetupFixture public setupFixture() { @@ -79,6 +81,12 @@ export class StorageProxyTestFixture { Expect(this.lStore.alreadySetDefault).toEqual(999); } + @Test('Restoring defaults privelages default values over `WebStorage` values.') + public restoreDefaultsTest() { + StorageProxy.restoreDefaults(this.lStore); + Expect(this.lStore.alreadySetDefault).toEqual(123); + } + @Test('Set `localStorage` key') public setLocalStorageKeyTest() { this.lStore.fizz = 123; @@ -105,6 +113,21 @@ export class StorageProxyTestFixture { Expect(data).toEqual(testStr); } + @Test('Verify caching helper works') + public verifyCacheTest() { + Expect(getItem(StorageTarget.Local, storageProxyIntegrityKey)).not.toBeDefined(); + + const shouldBeTrue = StorageProxy.verifyCache(this.lStore, 'very seedy'); + Expect(getItem(StorageTarget.Local, storageProxyIntegrityKey)).toBeDefined(); + Expect(shouldBeTrue).toBeTruthy(); + + const shouldAlsoBeTrue = StorageProxy.verifyCache(this.lStore, 'very seedy'); + Expect(shouldAlsoBeTrue).toBeTruthy(); + + const shouldBeFalse = StorageProxy.verifyCache(this.lStore, 'even seedier'); + Expect(shouldBeFalse).not.toBeTruthy(); + } + @Test('Validate `Array.prototype.push`') public arrayPushTest() { this.lStore.baz!.numbers.push(4, 5, 6); @@ -174,4 +197,23 @@ export class StorageProxyTestFixture { Expect(getItem(StorageTarget.Local, 'baz.numbers')).toEqual(expected); Expect(getItem(StorageTarget.Local, 'arrayValue')).toEqual(expected); } + + @Test('Clearing `StorageProxy` removes all keys from both `WebStorage` and the local object') + public clearStorageTest() { + StorageProxy.clearStorage(this.lStore); + + Expect(this.lStore.alreadySetDefault).toBeNull(); + Expect(this.lStore.arrayValue).toBeNull(); + Expect(this.lStore.bar).toBeNull(); + Expect(this.lStore.baz).toBeNull(); + Expect(this.lStore.defaults).toBeNull(); + Expect(this.lStore.fizz).toBeNull(); + + Expect(getItem(StorageTarget.Local, 'alreadySetDefault')).not.toBeDefined(); + Expect(getItem(StorageTarget.Local, 'arrayValue')).not.toBeDefined(); + Expect(getItem(StorageTarget.Local, 'bar')).not.toBeDefined(); + Expect(getItem(StorageTarget.Local, 'baz')).not.toBeDefined(); + Expect(getItem(StorageTarget.Local, 'defaults')).not.toBeDefined(); + Expect(getItem(StorageTarget.Local, 'fizz')).not.toBeDefined(); + } } diff --git a/yarn.lock b/yarn.lock index 58be7b9..a307c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -399,6 +399,11 @@ asyncro@^3.0.0: resolved "https://registry.yarnpkg.com/asyncro/-/asyncro-3.0.0.tgz#3c7a732e263bc4a42499042f48d7d858e9c0134e" integrity sha512-nEnWYfrBmA3taTiuiOoZYmgJ/CNrSoQLeLs29SeLcPu60yaw/mHDBHV0iOZ051fTvsTHxpCY+gXibqT9wbQYfg== +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" From 14c2e2b6b6e67af1d50e1228f908db1f7485b37f Mon Sep 17 00:00:00 2001 From: Ian K Smith Date: Tue, 1 Oct 2019 12:11:13 -0600 Subject: [PATCH 2/2] v1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28f68b0..50a82a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "storage-proxy", - "version": "0.11.3", + "version": "1.0.0", "description": "Use web storage (localStorage/sessionStorage) just like plain objects using ES6 Proxies.", "author": "Ian K Smith ", "license": "MIT",