From 2dbd0c65f17657c81fe66654879c17f264b77d97 Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Thu, 21 Apr 2022 13:39:22 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20v2=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests completed with a mock into what would be react native's AsyncStorage and community implementations of it (original package has been deprecated). Works in real browser tests. BREAKING CHANGE: 🧨 All functions return promises now. Options are different. Package renamed from better-web-storage. --- package.json | 8 +- scripts/updatePackage.ts | 8 +- src/index.ts | 56 ++++++-- test/test.ts | 271 ++++++++++++++++++++++++++++++++++++++- test/types.ts | 4 +- tsconfig.json | 1 + 6 files changed, 330 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 2c7c3b7..8cddeff 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "bws2", + "name": "appstorage", "version": "1.0.0", "main": "cjs.js", "module": "es.js", @@ -7,6 +7,12 @@ "types": "types.d.ts", "license": "MIT", "private": true, + "author": { + "name": "Danilo Alonso", + "email": "danilo@alonso.network", + "url": "https://github.com/damusix" + }, + "repository": "git@github.com:damusix/better-web-storage.git", "scripts": { "build": "sh scripts/build.sh", "test": "mocha -r esm -r ./test/_setup.js -r ts-node/register 'test/**/*.ts'", diff --git a/scripts/updatePackage.ts b/scripts/updatePackage.ts index 99b27ad..8dc792a 100644 --- a/scripts/updatePackage.ts +++ b/scripts/updatePackage.ts @@ -7,7 +7,9 @@ import { module, cdn, types, - license + license, + author, + repository } from '../package.json'; const paths = { @@ -26,7 +28,9 @@ writeFileSync( module, cdn, types, - license + license, + author, + repository }) ); diff --git a/src/index.ts b/src/index.ts index 2a7c88f..4e05cb4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,23 @@ class StorageError extends Error {}; -interface StorageImplementation { - getItem(K: string): string | null | Promise - setItem(K: string, V: string): void | Promise - removeItem(key: string): void | Promise; - clear(): void | Promise; - length: number -} +type MaybePromise = T | Promise; + +export type StorageImplementation = { + clear(): MaybePromise; + getItem(key: string): MaybePromise; + removeItem(key: string): MaybePromise; + setItem(key: string, value: string): MaybePromise; + multiGet?(keys: string[]): Promise<[string, string][]>; + length: number, +} & Record + +// interface StorageImplementation { +// getItem(K: string): string | null | Promise +// setItem(K: string, V: string): void | Promise +// removeItem(key: string): void | Promise; +// clear(): void | Promise; +// length: number +// } type JsonStringifyable = ( null | number | string | boolean | @@ -183,19 +194,44 @@ export class AppStorage { * whether the keys exist within the storage or not * @param keys */ - has(keys: string[]): boolean[] + has(keys: string[]): Promise /** * Returns whether key exists within the storage * @param key */ - has(key: string): boolean + has(key: string): Promise - has(keyOrKeys: any): unknown { + async has(keyOrKeys: any): Promise { this._assertKey(keyOrKeys[0]); + // Handle async storage + if (!!this.storage.multiGet) { + + let keys = [keyOrKeys]; + + if (Array.isArray(keyOrKeys)) { + + keys = keyOrKeys; + } + + const values = ( + + await this.storage.multiGet(keys) + ).map(([key]) => key); + + const found = keys.map(key => values.indexOf(key) !== -1); + + if (found.length === 1) { + + return found.pop(); + } + + return found; + } + if (Array.isArray(keyOrKeys)) { return keyOrKeys.map( diff --git a/test/test.ts b/test/test.ts index db012c6..2e181b7 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,8 +1,273 @@ +import { AppStorage, StorageImplementation } from '../build'; +import { expect } from 'chai'; +const clearStores = () => { -describe('SimpleAppStorage', function () { + window.localStorage.clear() + window.sessionStorage.clear() +} - it('should create an instance', function () { +const defineProps = (obj: any, props: Record) => { + const entries = Object.entries(props); + + Object.defineProperties(obj, entries.reduce((o, [key, val]) => { + + return { + ...o, + [key]: { + configurable: false, + enumerable: false, + value: val, + + } + }; + }, {})); +} + +const fakeStorage = { + clear() { + + for (const key of Object.keys(asyncStorage)) { + + delete asyncStorage[key]; + } + + return Promise.resolve(); + }, + getItem(key: string) { + + return Promise.resolve(asyncStorage[key]) + }, + removeItem(key: string) { + + delete asyncStorage[key]; + + return Promise.resolve(); + }, + setItem: (key: string, value: string) => { + + asyncStorage[key] = value; + + return Promise.resolve(); + }, + multiGet: (keys: string[]) => { + + return Object.entries(asyncStorage).filter( + ([key]) => keys.includes(key) + ); + } +}; + +const asyncStorage = {} as StorageImplementation; + +defineProps(asyncStorage, fakeStorage) +Object.defineProperty(asyncStorage, 'length', { + + configurable: false, + enumerable: false, + get() { + + return Object.keys(asyncStorage).length; + } +}) + + +describe('AppStorage', () => { + + before(clearStores) + after(clearStores) + + it('References localStorage or sessionStorage', () => { + + const ls = new AppStorage(window.localStorage); + const ss = new AppStorage(window.sessionStorage); + + expect(ls.storage).to.equal(window.localStorage); + expect(ss.storage).to.equal(window.sessionStorage); + }); + + // Test both local storage and session storage + const testSuites: [string, StorageImplementation][] = [ + // name storage reference + ['LocalStorage', window.localStorage], + ['SessionStorage', window.sessionStorage], + ['AsyncStorage', asyncStorage] + ]; + + testSuites.forEach(([name, storage]) => { + + const store: { + store: AppStorage; + prefixed?: AppStorage; + } = { + store: new AppStorage(storage) + }; + + it(`${name}: Sets json stringified keys`, async () => { + + + const val = [1,2,3,4]; + await store.store.set('test', val); + + expect(storage.test).to.equal(JSON.stringify(val)); + }); + + it(`${name}: Sets an entire object as keys`, async () => { + + const val = { + one: 'two', + buckle: 'myshow' + }; + + await store.store.set(val); + + expect(storage.one).to.equal(JSON.stringify(val.one)); + expect(storage.buckle).to.equal(JSON.stringify(val.buckle)); + }); + + it(`${name}: Gets and parses a single key`, async () => { + + expect(await store.store.get('test')).to.include.members([1,2,3,4]) + }); + + it(`${name}: Gets and returns an object of key values when passed multiple values`, async () => { + + const vals = await store.store.get(['one', 'buckle']); + expect(vals).to.include({ + one: 'two', + buckle: 'myshow' + }); + }); + + it(`${name}: Checks to see if has keys`, async () => { + + expect(await store.store.has('test')).to.equal(true); + expect(await store.store.has(['test', 'one', 'buckle'])).to.have.members([true, true, true]); + expect(await store.store.has(['test', 'one', 'buckle', 'three'])).to.have.members([true, true, true, false]); + }); + + it(`${name}: Removes item from store`, async () => { + + await store.store.rm('test'); + expect(await store.store.get('test')).to.not.exist; + }); + + it(`${name}: Retrieves storage length`, async () => { + + await store.store.set('test1', true); + await store.store.set('test2', true); + await store.store.set('test3', true); + + expect(store.store.length).to.equal(5); + }); + + it(`${name}: Retrieves a copy of entire store`, async () => { + + expect(await store.store.get()).to.include({ + one: 'two', + buckle: 'myshow', + test1: true, + test2: true, + test3: true + }); + }); + + it(`${name}: Clears storage`, async () => { + + await store.store.clear(); + expect(store.store.length).to.equal(0); + expect(storage.length).to.equal(0); + }) + + it(`${name}: Sets keys using a prefix`, async () => { + + store.prefixed = new AppStorage(storage, 'test'); + const store2 = new AppStorage(storage, 'test2'); + + await store.prefixed!.set('test', true); + await store2.set('test', false); + + expect(storage.test).to.not.exist; + expect(storage['test:test']).to.exist; + expect(storage['test2:test']).to.exist; + expect(storage['test:test']).to.equal('true'); + expect(storage['test2:test']).to.equal('false'); + }); + + it(`${name}: Retrieves prefixed storage length accurately`, async () => { + + await store.prefixed!.set('test1', true); + await store.prefixed!.set('test2', true); + await store.prefixed!.set('test3', true); + expect(await storage.length).to.equal(5); + expect(await store.prefixed!.length).to.equal(4); + }); + + it(`${name}: Retrieves all prefixed items`, async () => { + + expect(await store.prefixed!.get()).to.include({ + test: true, + test1: true, + test2: true, + test3: true + }); + }) + + it(`${name}: Clears all prefixed items`, async () => { + + await store.prefixed!.clear(); + expect(store.prefixed!.length).to.equal(0); + expect(storage.length).to.equal(1); + }); + + it(`${name}: Object assigns to a current object in store`, async () => { + + const cur = { a: true }; + const assign = { b: false }; + await store.store.set('cur', cur); + + expect(await store.store.get('cur')).to.not.include(assign); + + await store.store.assign('cur', assign); + + expect(await store.store.get('cur')).to.include(cur); + expect(await store.store.get('cur')).to.include(assign); + }); + + it(`${name}: Does not allow assigns if value not an object`, async () => { + + const cur = { a: true }; + const assign = 'wat'; + await store.store.set('cur', cur); + + try { + + await store.store.assign('cur', assign as any); + expect(await store.store.get('cur')).to.not.include(assign); + } + catch (e) { + + expect(e).to.be.an('error'); + } + }); + + it(`${name}: Does not allow assigns if item not an object`, async () => { + + const cur = 'wat'; + const assign = { a: true }; + await store.store.set('cur', cur); + + try { + + await store.store.assign('cur', assign); + expect(await store.store.get('cur')).to.not.include(assign); + } + catch (e) { + + expect(e).to.be.an('error'); + } + }); }); -}); \ No newline at end of file + +}); diff --git a/test/types.ts b/test/types.ts index 9c1c69e..c6f13e0 100644 --- a/test/types.ts +++ b/test/types.ts @@ -47,8 +47,8 @@ const session = new AppStorage(global.window.sessionStorage, 'ho'); const y = storage.keys(); const z = await storage.values(); - const had = storage.has('a'); - const hasMany = storage.has(['a', 'b']); + const had = await storage.has('a'); + const hasMany = await storage.has(['a', 'b']); had === true; diff --git a/tsconfig.json b/tsconfig.json index 2fce2dc..133eeac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "lib": ["DOM", "ESNext"], "strict": true, "declaration": true, + "strictNullChecks": true, "paths": { "pkg": ["./package.json"] }