diff --git a/.gitignore b/.gitignore index 3ec4bbd..fbd4da5 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ Thumbs.db /dist dist.tgz /.ng_pkg_build +/package-lock.json diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..842a539 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + "reporters": ["default"], + "roots": [ + "/tests" + ], + "testMatch": [ + "**/?(*.)+(spec|test).+(ts|tsx|js)" + ], + "transform": { + ".+\\.(ts)$": "ts-jest" + }, +} diff --git a/lib/index.js b/lib/index.js index f43f341..9289462 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,13 @@ "use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; Object.defineProperty(exports, "__esModule", { value: true }); -__export(require("./src/index")); +__exportStar(require("./src/index"), exports); diff --git a/lib/src/index.d.ts b/lib/src/index.d.ts index 0340438..71f4dc3 100644 --- a/lib/src/index.d.ts +++ b/lib/src/index.d.ts @@ -1 +1 @@ -export * from './webStorage'; +export * from './webstorage'; diff --git a/lib/src/index.js b/lib/src/index.js index bf61eab..10fc4a3 100644 --- a/lib/src/index.js +++ b/lib/src/index.js @@ -1,6 +1,13 @@ "use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; Object.defineProperty(exports, "__esModule", { value: true }); -__export(require("./webStorage")); +__exportStar(require("./webstorage"), exports); diff --git a/lib/src/webStorage.d.ts b/lib/src/webStorage.d.ts deleted file mode 100644 index 895c367..0000000 --- a/lib/src/webStorage.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface WebStorageOptions { - fieldName?: string; - encryptionKey?: string; - defaultValue?: any; -} -export declare function LocalStorage(opts?: WebStorageOptions): (target: object, key: string) => void; -export declare function SessionStorage(opts?: WebStorageOptions): (target: object, key: string) => void; diff --git a/lib/src/webStorage.js b/lib/src/webStorage.js deleted file mode 100644 index 4b1925d..0000000 --- a/lib/src/webStorage.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const crypto_js_1 = require("crypto-js"); -function LocalStorage(opts = {}) { - return storage(localStorage, opts); -} -exports.LocalStorage = LocalStorage; -function SessionStorage(opts = {}) { - return storage(sessionStorage, opts); -} -exports.SessionStorage = SessionStorage; -function storage(storageType, opts = {}) { - return function (target, key) { - if (!opts.fieldName) - opts.fieldName = key; - Object.defineProperty(target, key, { - get: function () { - let value = storageType.getItem(opts.fieldName); - if (!value && opts.defaultValue != null) - return opts.defaultValue; - if (value != null && opts.encryptionKey) - value = crypto_js_1.AES.decrypt(JSON.parse(value), opts.encryptionKey).toString(crypto_js_1.enc.Utf8); - return JSON.parse(value); - }, - set: function (value) { - if (value != null && opts.encryptionKey) - value = crypto_js_1.AES.encrypt(JSON.stringify(value), opts.encryptionKey).toString(); - storageType.setItem(opts.fieldName, JSON.stringify(value)); - } - }); - }; -} diff --git a/lib/src/webstorage.d.ts b/lib/src/webstorage.d.ts new file mode 100644 index 0000000..fffbe72 --- /dev/null +++ b/lib/src/webstorage.d.ts @@ -0,0 +1,42 @@ +/** + * Options to be used with WebStorage decorators + * @category WebStorage + */ +export interface WebStorageOptions { + /** Default value to provide if storage is empty */ + default?: any; + /** Key to save under */ + key?: string; +} +/** + * Automatically syncs localStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @LocalStorage() lastLogin: string; + * @LocalStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside localStorage. + * @param opts Any additional options + */ +export declare function LocalStorage(defaultValue?: any, opts?: WebStorageOptions): (target: object, key: string) => void; +/** + * Automatically syncs sessionStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @SessionStorage() lastLogin: string; + * @SessionStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside sessionStorage. + * @param opts Any additional options + */ +export declare function SessionStorage(defaultValue?: any, opts?: WebStorageOptions): (target: object, key: string) => void; diff --git a/lib/src/webstorage.js b/lib/src/webstorage.js new file mode 100644 index 0000000..d6906ae --- /dev/null +++ b/lib/src/webstorage.js @@ -0,0 +1,72 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SessionStorage = exports.LocalStorage = void 0; +/** + * Automatically syncs localStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @LocalStorage() lastLogin: string; + * @LocalStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside localStorage. + * @param opts Any additional options + */ +function LocalStorage(defaultValue, opts = {}) { + opts.default = defaultValue; + return storage(localStorage, opts); +} +exports.LocalStorage = LocalStorage; +/** + * Automatically syncs sessionStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @SessionStorage() lastLogin: string; + * @SessionStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside sessionStorage. + * @param opts Any additional options + */ +function SessionStorage(defaultValue, opts = {}) { + opts.default = defaultValue; + return storage(sessionStorage, opts); +} +exports.SessionStorage = SessionStorage; +/** + * **Internal use only** + * + * Overrides the properties getter/setter methods to read/write from the provided storage endpoint. + * + * @hidden + * @category WebStorage + * @param storage Web Storage API + * @param opts Any additional options + */ +function storage(storage, opts) { + return function (target, key) { + if (!opts.key) + opts.key = key; + Object.defineProperty(target, key, { + get: function () { + const storageVal = storage.getItem(opts.key); + if (storageVal == null || storageVal == 'null' || storageVal == 'undefined') + return opts.default || null; + return JSON.parse(storageVal); + }, + set: function (value) { + if (value == null) + storage.removeItem(opts.key); + storage.setItem(opts.key, JSON.stringify(value)); + } + }); + }; +} diff --git a/package.json b/package.json index 212871f..ba1bf29 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,32 @@ { "name": "webstorage-decorators", - "version": "1.0.1", - "description": "Decorators to sync variable to Local/Session storage", + "version": "2.0.0", + "description": "Decorators to sync class properties to Local/Session storage", "main": "./lib/index.js", "typings": "./lib/index.d.ts", "scripts": { "build": "tsc", - "test": "jest" + "test": "jest --verbose" }, "files": [ "lib" ], "keywords": [ + "Decorators", "LocalStorage", "SessionStorage", "WebStorage" ], "author": "Zak Timson", "license": "ISC", - "jest": { - "moduleFileExtensions": [ - "ts", - "js" - ], - "transform": { - "\\.ts$": "/node_modules/ts-jest/preprocessor.js" - }, - "testRegex": "/src/.*\\.spec\\.ts$" - }, "dependencies": { - "@types/crypto-js": "^3.1.39", - "crypto-js": "^3.1.9-1" + "@types/crypto-js": "^4.0.1", + "crypto-js": "^4.0.0" }, "devDependencies": { - "@types/jest": "^22.2.3", - "jest": "^22.4.3", - "ts-jest": "^22.4.4", - "typescript": "^2.8.3" + "@types/jest": "^26.0.15", + "jest": "^26.6.0", + "ts-jest": "^26.4.1", + "typescript": "^4.0.3" } } diff --git a/src/index.ts b/src/index.ts index 77f64c3..71f4dc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export * from './webStorage'; \ No newline at end of file +export * from './webstorage'; diff --git a/src/webStorage.spec.ts b/src/webStorage.spec.ts deleted file mode 100644 index 9957786..0000000 --- a/src/webStorage.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import {LocalStorage, SessionStorage} from './index' - -class TestClass { - @LocalStorage() localStorage: any; - @SessionStorage() sessionStorage: any; - @LocalStorage({fieldName: 'customKey'}) customKey: any; - @LocalStorage({fieldName: 'encrypted', encryptionKey: 'ENCRYPTION_KEY'}) encrypted: any; - @LocalStorage({defaultValue: 'test'}) defaultedStorage: any; - @LocalStorage({defaultValue: {a: true, b: 'test', c: 3.14}}) objectDefault: any; -} - -describe('LocalStorage Tests', () => { - it('LocalStorage', () => { - let testValue = Math.random().toString(36).substring(7); - new TestClass().localStorage = testValue; - expect(JSON.parse(localStorage.getItem('_localStorage'))).toEqual(testValue); - }); - - it('SessionStorage', () => { - let testValue = Math.random().toString(36).substring(7); - new TestClass().sessionStorage = testValue; - expect(JSON.parse(sessionStorage.getItem('_sessionStorage'))).toEqual(testValue); - }); - - it('Custom Key', () => { - let testValue = Math.random().toString(36).substring(7); - new TestClass().customKey = testValue; - expect(JSON.parse(localStorage.getItem('customKey'))).toEqual(testValue); - }); - - it('Maintain Object Structure', () => { - let testObject = new TestClass(); - let testValue = {a: Math.random().toString(36).substring(7), b: Math.random(), c: true, d: [{a: false}, {a: true}]}; - testObject.localStorage = testValue; - expect(testObject.localStorage).toEqual(testValue); - }); - - describe('Default', () => { - it('Default Value', () => { - localStorage.removeItem('_defaultedStorage'); - let testObject = new TestClass(); - expect(testObject.defaultedStorage).toEqual('test'); - }); - - it('Key Already Has Value', () => { - let testValue = {a: Math.random().toString(36).substring(7), b: Math.random(), c: true, d: [{a: false}, {a: true}]}; - localStorage.setItem('_defaultedStorage', JSON.stringify(testValue)); - let testObject = new TestClass(); - expect(testObject.defaultedStorage).toEqual(testValue); - }); - - it('Using Object As Default', () => { - let testObject = new TestClass(); - expect(testObject.objectDefault).toEqual({a: true, b: 'test', c: 3.14}); - }) - }); - - describe('Encryption Tests', () => { - it('Encrypt', () => { - let testObject = new TestClass(); - let testValue = Math.random().toString(36).substring(7); - testObject.encrypted = testValue; - expect(JSON.parse(localStorage.getItem('encrypted'))).not.toEqual(testValue); - }); - - it('Decrypt', () => { - let testObject = new TestClass(); - let testValue = Math.random().toString(36).substring(7); - testObject.encrypted = testValue; - expect(testObject.encrypted).toEqual(testValue); - }); - - it('Maintain Object Structure', () => { - let testObject = new TestClass(); - let testValue = {a: Math.random().toString(36).substring(7), b: Math.random(), c: true, d: [{a: false}, {a: true}]}; - testObject.encrypted = testValue; - expect(testObject.encrypted).toEqual(testValue); - }); - }); -}); diff --git a/src/webStorage.ts b/src/webStorage.ts deleted file mode 100644 index 16ae814..0000000 --- a/src/webStorage.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {AES, enc} from 'crypto-js'; - -export interface WebStorageOptions { - fieldName?: string; - encryptionKey?: string; - defaultValue?: any; -} - -export function LocalStorage(opts: WebStorageOptions) { - if(!opts) opts = {}; - return storage(localStorage, opts); -} - -export function SessionStorage(opts: WebStorageOptions) { - if(!opts) opts = {}; - return storage(sessionStorage, opts); -} - -function storage(storageType: Storage, opts: WebStorageOptions) { - return function(target: object, key: string) { - if(!opts.fieldName) opts.fieldName = key; - - Object.defineProperty(target, key, { - get: function() { - let value = storageType.getItem(opts.fieldName); - if(!value && opts.defaultValue != null) return opts.defaultValue; - if(value != null && opts.encryptionKey) value = AES.decrypt(JSON.parse(value), opts.encryptionKey).toString(enc.Utf8); - return JSON.parse(value); - }, - set: function(value) { - if(value != null && opts.encryptionKey) value = AES.encrypt(JSON.stringify(value), opts.encryptionKey).toString(); - storageType.setItem(opts.fieldName, JSON.stringify(value)); - } - }); - }; -} diff --git a/src/webstorage.ts b/src/webstorage.ts new file mode 100644 index 0000000..945d1c3 --- /dev/null +++ b/src/webstorage.ts @@ -0,0 +1,77 @@ +/** + * Options to be used with WebStorage decorators + * @category WebStorage + */ +export interface WebStorageOptions { + /** Default value to provide if storage is empty */ + default?: any + /** Key to save under */ + key?: string +} + +/** + * Automatically syncs localStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @LocalStorage() lastLogin: string; + * @LocalStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside localStorage. + * @param opts Any additional options + */ +export function LocalStorage(defaultValue?: any, opts: WebStorageOptions = {}) { + opts.default = defaultValue; + return storage(localStorage, opts); +} + +/** + * Automatically syncs sessionStorage with the decorated property. + * + * **Example** + * ``` + * class Example { + * @SessionStorage() lastLogin: string; + * @SessionStorage(false, {key: '_hideMenu'}) hideMenu: boolean; + * } + * ``` + * + * @category WebStorage + * @param defaultValue Default value to return if property does no exist inside sessionStorage. + * @param opts Any additional options + */ +export function SessionStorage(defaultValue?, opts: WebStorageOptions = {}) { + opts.default = defaultValue; + return storage(sessionStorage, opts); +} + +/** + * **Internal use only** + * + * Overrides the properties getter/setter methods to read/write from the provided storage endpoint. + * + * @hidden + * @category WebStorage + * @param storage Web Storage API + * @param opts Any additional options + */ +function storage(storage: Storage, opts: WebStorageOptions) { + return function(target: object, key: string) { + if(!opts.key) opts.key = key; + Object.defineProperty(target, key, { + get: function() { + const storageVal = storage.getItem(opts.key); + if(storageVal == null || storageVal == 'null' || storageVal == 'undefined') return opts.default || null; + return JSON.parse(storageVal); + }, + set: function(value) { + if(value == null) storage.removeItem(opts.key); + storage.setItem(opts.key, JSON.stringify(value)); + } + }); + }; +} diff --git a/tests/webstorage.spec.ts b/tests/webstorage.spec.ts new file mode 100644 index 0000000..f5981b8 --- /dev/null +++ b/tests/webstorage.spec.ts @@ -0,0 +1,91 @@ +import {LocalStorage, SessionStorage} from "../src"; + +const CUSTOM_KEY = '__MY_KEY' + +class TestClass { + @LocalStorage() localStorage: any; + @LocalStorage({a: true, b: 'test', c: 3.14}) defaultedLocalStorage: any; + @LocalStorage(null, {key: CUSTOM_KEY}) customLocalStorage: any; + @SessionStorage() sessionStorage: any; + @SessionStorage({a: true, b: 'test', c: 3.14}) defaultedSessionStorage: any; + @SessionStorage(null, {key: CUSTOM_KEY}) customSessionStorage: any; +} + +describe('WebStorage', () => { + let testComponent: TestClass; + + beforeEach(() => { + localStorage.clear(); + testComponent = new TestClass(); + }); + + describe('LocalStorage', () => { + test('NULL Value', () => { + expect(testComponent.localStorage).toBeNull(); + testComponent.localStorage = 0; + expect(testComponent.localStorage).not.toBeNull(); + testComponent.localStorage = null; + expect(testComponent.localStorage).toBeNull(); + }); + test('Default Value', () => expect(testComponent.defaultedLocalStorage.a).toBeTruthy()); + test('Number Value', () => { + const testValue = Math.random(); + testComponent.localStorage = testValue; + expect(localStorage.getItem('localStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.localStorage).toBe(testValue); + }); + test('String Value', () => { + const testValue = 'SOMETHING_RANDOM'; + testComponent.localStorage = testValue; + expect(localStorage.getItem('localStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.localStorage).toBe(testValue); + }); + test('Object Value', () => { + const testValue = {a: Math.floor(Math.random() * 100), b: Math.random()}; + testComponent.localStorage = testValue; + expect(localStorage.getItem('localStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.localStorage).toStrictEqual(testValue); + }); + test('Custom Key', () => { + const testValue = Math.random(); + testComponent.customLocalStorage = testValue; + expect(localStorage.getItem(CUSTOM_KEY)).toBe(JSON.stringify(testValue)); + expect(testComponent.customLocalStorage).toBe(testValue); + }); + }); + + describe('SessionStorage', () => { + test('NULL Value', () => { + expect(testComponent.sessionStorage).toBeNull(); + testComponent.sessionStorage = 0; + expect(testComponent.sessionStorage).not.toBeNull(); + testComponent.sessionStorage = null; + expect(testComponent.sessionStorage).toBeNull(); + }); + test('Default Value', () => expect(testComponent.defaultedSessionStorage.a).toBeTruthy()); + test('Number Value', () => { + const testValue = Math.random(); + testComponent.sessionStorage = testValue; + expect(sessionStorage.getItem('sessionStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.sessionStorage).toBe(testValue); + }); + test('String Value', () => { + const testValue = 'SOMETHING_RANDOM'; + testComponent.sessionStorage = testValue; + expect(sessionStorage.getItem('sessionStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.sessionStorage).toBe(testValue); + }); + test('Object Value', () => { + const testValue = {a: Math.floor(Math.random() * 100), b: Math.random()}; + testComponent.sessionStorage = testValue; + expect(sessionStorage.getItem('sessionStorage')).toBe(JSON.stringify(testValue)); + expect(testComponent.sessionStorage).toStrictEqual(testValue); + }); + test('Custom Key', () => { + const testValue = Math.random(); + testComponent.customSessionStorage = testValue; + expect(sessionStorage.getItem(CUSTOM_KEY)).toBe(JSON.stringify(testValue)); + expect(testComponent.customSessionStorage).toBe(testValue); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 8fdd2a9..263051b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,11 @@ "module": "commonjs", "declaration": true, "outDir": "lib", + "typeRoots": [ + "./node_modules/@types" + ], "strict": true, + "noImplicitAny": false, "experimentalDecorators": true, }, "include": [