├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── karma.conf.js ├── license-banner.txt ├── ng-package.json ├── package-lock.json ├── package.json ├── schematics ├── collection.json └── ng-add │ ├── add-config-to-index.rule.ts │ ├── add-import-to-root-module.rule.ts │ ├── create-config-file.rule.ts │ ├── index.ts │ └── install-dependencies.rule.ts ├── src ├── lib │ ├── config │ │ ├── config.helper.ts │ │ ├── config.interface.ts │ │ └── config.ts │ ├── decorator │ │ ├── cache.interface.ts │ │ ├── cache.ts │ │ ├── webstorage.spec.ts │ │ └── webstorage.ts │ ├── ngx-store.module.ts │ ├── ngx-store.types.ts │ ├── service │ │ ├── README.md │ │ ├── cookies-storage.service.ts │ │ ├── local-storage.service.ts │ │ ├── resource.ts │ │ ├── session-storage.service.ts │ │ ├── shared-storage.service.ts │ │ ├── web-storage.mock.ts │ │ ├── web-storage.service.spec.ts │ │ ├── webstorage.interface.ts │ │ └── webstorage.service.ts │ ├── tools.ts │ └── utility │ │ ├── index.ts │ │ ├── shared-storage.utility.ts │ │ ├── storage │ │ ├── cookies-storage.ts │ │ ├── shared-storage.ts │ │ ├── storage-event.ts │ │ └── storage.ts │ │ ├── webstorage.utility.spec.ts │ │ └── webstorage.utility.ts ├── public-api.ts └── test.ts ├── tsconfig.lib.json ├── tsconfig.lib.prod.json ├── tsconfig.schematics.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | quote_type = single 12 | indent_brace_style = K&R 13 | spaces_around_brackets = outside 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### This project 2 | /dist 3 | /documentation 4 | /coverage 5 | *.tgz 6 | 7 | ### Third party 8 | # Eclipse template 9 | .metadata 10 | bin/ 11 | tmp/ 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .settings/ 18 | .loadpath 19 | .recommenders 20 | 21 | # Eclipse Core 22 | .project 23 | 24 | # External tool builders 25 | .externalToolBuilders/ 26 | 27 | # Locally stored "Eclipse launch configurations" 28 | *.launch 29 | 30 | # PyDev specific (Python IDE for Eclipse) 31 | *.pydevproject 32 | 33 | # CDT-specific (C/C++ Development Tooling) 34 | .cproject 35 | 36 | # JDT-specific (Eclipse Java Development Tools) 37 | .classpath 38 | 39 | # Java annotation processor (APT) 40 | .factorypath 41 | 42 | # PDT-specific (PHP Development Tools) 43 | .buildpath 44 | 45 | # sbteclipse plugin 46 | .target 47 | 48 | # Tern plugin 49 | .tern-project 50 | 51 | # TeXlipse plugin 52 | .texlipse 53 | 54 | # STS (Spring Tool Suite) 55 | .springBeans 56 | 57 | # Code Recommenders 58 | .recommenders/ 59 | ### Windows template 60 | # Windows image file caches 61 | Thumbs.db 62 | ehthumbs.db 63 | 64 | # Folder config file 65 | Desktop.ini 66 | 67 | # Recycle Bin used on file shares 68 | $RECYCLE.BIN/ 69 | 70 | # Windows Installer files 71 | *.cab 72 | *.msi 73 | *.msm 74 | *.msp 75 | 76 | # Windows shortcuts 77 | *.lnk 78 | ### JetBrains template 79 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 80 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 81 | 82 | # User-specific stuff: 83 | .idea 84 | .idea/workspace.xml 85 | .idea/tasks.xml 86 | .idea/dictionaries 87 | .idea/vcs.xml 88 | .idea/jsLibraryMappings.xml 89 | 90 | # Sensitive or high-churn files: 91 | .idea/dataSources.ids 92 | .idea/dataSources.xml 93 | .idea/dataSources.local.xml 94 | .idea/sqlDataSources.xml 95 | .idea/dynamic.xml 96 | .idea/uiDesigner.xml 97 | 98 | # Gradle: 99 | .idea/gradle.xml 100 | .idea/libraries 101 | 102 | # Mongo Explorer plugin: 103 | .idea/mongoSettings.xml 104 | 105 | ## File-based project format: 106 | *.iws 107 | 108 | ## Plugin-specific files: 109 | 110 | # IntelliJ 111 | /out/ 112 | 113 | # mpeltonen/sbt-idea plugin 114 | .idea_modules/ 115 | 116 | # JIRA plugin 117 | atlassian-ide-plugin.xml 118 | 119 | # Crashlytics plugin (for Android Studio and IntelliJ) 120 | com_crashlytics_export_strings.xml 121 | crashlytics.properties 122 | crashlytics-build.properties 123 | fabric.properties 124 | ### SublimeText template 125 | # cache files for sublime text 126 | *.tmlanguage.cache 127 | *.tmPreferences.cache 128 | *.stTheme.cache 129 | 130 | # workspace files are user-specific 131 | *.sublime-workspace 132 | 133 | # project files should be checked into the repository, unless a significant 134 | # proportion of contributors will probably not be using SublimeText 135 | # *.sublime-project 136 | 137 | # sftp configuration file 138 | sftp-config.json 139 | 140 | # Package control specific files 141 | Package Control.last-run 142 | Package Control.ca-list 143 | Package Control.ca-bundle 144 | Package Control.system-ca-bundle 145 | Package Control.cache/ 146 | Package Control.ca-certs/ 147 | bh_unicode_properties.cache 148 | 149 | # Sublime-github package stores a github token in this file 150 | # https://packagecontrol.io/packages/sublime-github 151 | GitHub.sublime-settings 152 | ### OSX template 153 | *.DS_Store 154 | .AppleDouble 155 | .LSOverride 156 | 157 | # Icon must end with two \r 158 | Icon 159 | 160 | # Thumbnails 161 | ._* 162 | 163 | # Files that might appear in the root of a volume 164 | .DocumentRevisions-V100 165 | .fseventsd 166 | .Spotlight-V100 167 | .TemporaryItems 168 | .Trashes 169 | .VolumeIcon.icns 170 | .com.apple.timemachine.donotpresent 171 | 172 | # Directories potentially created on remote AFP share 173 | .AppleDB 174 | .AppleDesktop 175 | Network Trash Folder 176 | Temporary Items 177 | .apdisk 178 | ### Node template 179 | # Logs 180 | logs 181 | *.log 182 | npm-debug.log* 183 | 184 | # Runtime data 185 | pids 186 | *.pid 187 | *.seed 188 | 189 | # Directory for instrumented libs generated by jscoverage/JSCover 190 | lib-cov 191 | 192 | # Coverage directory used by tools like istanbul 193 | coverage 194 | 195 | # nyc test coverage 196 | .nyc_output 197 | 198 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 199 | .grunt 200 | 201 | # node-waf configuration 202 | .lock-wscript 203 | 204 | # Compiled binary addons (http://nodejs.org/api/addons.html) 205 | build/Release 206 | 207 | # Dependency directories 208 | node_modules 209 | jspm_packages 210 | 211 | # Optional npm cache directory 212 | .npm 213 | 214 | # Optional REPL history 215 | .node_repl_history 216 | 217 | ### Typings 218 | typings 219 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | addons: 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | cache: 11 | directories: 12 | - node_modules 13 | 14 | language: node_js 15 | 16 | node_js: 17 | - "7" 18 | - "8" 19 | 20 | before_install: 21 | - npm i npm@^5 -g 22 | - npm cache verify 23 | - npm prune 24 | - npm update 25 | 26 | install: 27 | - npm install 28 | 29 | script: 30 | - npm test 31 | - npm run build 32 | 33 | before_script: 34 | - export DISPLAY=:99.0 35 | - sh -e /etc/init.d/xvfb start 36 | - sleep 3 37 | 38 | matrix: 39 | fast_finish: true 40 | 41 | notifications: 42 | email: false 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Daniel Kucal, ZoomSphere 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Storage 2 | ### Decorators and services for cookies, session- and localStorage 3 | This library adds decorators that make it super easy to *automagically* save and restore variables using HTML5's `localStorage` and `sessionStorage`. It also provides Angular-Injectable Session- and LocalStorageService. 4 | 5 | 6 | ## What's included? 7 | - Decorator functions that are pretty easy to use and configure (see [Decorators config](#decorators-config)): 8 | + `@LocalStorage()` - to save variable in HTML5 localStorage 9 | + `@SessionStorage()` - to save variable in HTML5 sessionStorage 10 | + `@CookieStorage()` - to save variable as a cookie 11 | + `@SharedStorage()` - to keep variable in temporary memory that can be shared across classes 12 | + `@TempStorage()` - alias for `SharedStorage` 13 | - Injectable `LocalStorageService`, `SessionStorageService`, `CookiesStorageService` and `SharedStorageService` ([read more here](src/service#angular-storage)) 14 | - Possibility of [listening to storage changes](https://github.com/zoomsphere/ngx-store/tree/master/src/service#listening-to-changes) 15 | - Easy configuration (see [#configuration](#configuration) section) 16 | - Compatibility with: 17 | + all previous versions 18 | + Angular AoT compiler 19 | + `angular2-localstorage` (seamless migration) 20 | + [nativescript-localstorage](https://github.com/NathanaelA/nativescript-localstorage) 21 | + Angular since version 2 22 | + your own project! 23 | - Tests coverage 24 | 25 | ## CHANGELOG 26 | #### v3.1.0: 27 | - added `migrateKey` decorator config option 28 | #### v3.0.0: 29 | - support for Angular 11 & TypeScript 4 30 | - resolved circular dependencies 31 | - added schematics 32 | - added `.forRoot()` and `.forChild()` methods for future use 33 | - required dependencies moved to schematics installation script 34 | #### v2.1.0 - support for Angular 7 & TypeScript 3 35 | #### v2.0.0 - support for Angular 6 (RxJS v6) 36 | #### v1.4.x: 37 | - standardized behavior for: 38 | - more than 1 decorator, e.g. in `@LocalStorage() @CookieStorage() variable: any;` `CookieStorage` (decorator closer to variable) has higher priority, hence the value will be read from cookies only. The cookie value will be saved in `localStorage` regardless of its content to keep consistency. 39 | - `WebStorageService.clear('all')` - now will remove everything except `ngx-store`'s config (stored in `localStorage`) 40 | - removed deprecated (since v0.5) `WEBSTORAGE_CONFIG` 41 | - `@SharedStorage` has now alias `@TempStorage` 42 | - introduced [builder pattern](https://github.com/zoomsphere/ngx-store/tree/master/src/service#builder-pattern) 43 | - added unit tests coverage 44 | - fixes for storage events 45 | 46 | 47 | ## Upcoming (TODO) 48 | - [x] Storage events for keys removed from outside 49 | - [ ] Tests for storage events (accepting PRs) 50 | - [ ] Accepting Moment's instances as expiration date (accepting PRs) 51 | - [ ] More options for managing cookies 52 | - [ ] Support for Set and Map 53 | - [ ] Encoding of saved data 54 | - [ ] Take configuration from [npm config](https://www.npmjs.com/package/config)'s file (?) 55 | - [ ] Automatically handle all data manipulations using [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) (ES6) 56 | 57 | 58 | ## Installation 59 | ### Versions 3+ 60 | Just run: `ng add ngx-store`. Done! 61 | 62 | ### Older versions 63 | 1. Download the library: `npm i ngx-store --save` 64 | 2. Import the WebStorageModule in your `app.module.ts`: 65 | ```typescript 66 | import { NgModule } from '@angular/core'; 67 | import { WebStorageModule } from 'ngx-store'; 68 | 69 | @NgModule({ 70 | imports: [ 71 | WebStorageModule.forRoot(), 72 | ], 73 | }) 74 | export class AppModule {} 75 | ``` 76 | 77 | 78 | ## Configuration 79 | Things you should take into consideration while configuring this module: 80 | - Decorated objects have added `.save()` method to easily force save of made changes (configurable by `mutateObjects`) 81 | - Support for all `Array` methods that change array object's value can be disabled (configurable by `mutateObjects`) 82 | - Object mutation can be troublesome for object comparisons, so you can configure this feature for single field passing [decorator config](#decorators-config) 83 | - You may not use prefix (by setting it to `''`), however we recommend to use it, as it helps avoid conflicts with other libraries (configurable by `prefix`) 84 | - There are 3 ways to clear ngx-stored data: 85 | + `'all'` - completely clears current Storage 86 | + `'prefix'` - removes all variables prefixed by ngx-store 87 | + `'decorators'` - removes only variables created by decorating functions (useful when not using prefix) 88 | Default behaviour is specified by setting `clearType`, but it's possible to pass this parameter directly into service `clear()` method. 89 | - Examples for `cookiesScope` can be found in [this comment](https://github.com/zoomsphere/ngx-store/blob/master/src/utility/storage/cookies-storage.ts#L125) 90 | 91 | As this project uses decorating functions, it is important to provide custom configuration in global variable named `NGXSTORE_CONFIG` before Angular application load. Here are some ways to do it: 92 | 1. Add ` 105 | ``` 106 | 2. If you use webpack, you can provide global variable in your `webpack.js` file this way: 107 | ```javascript 108 | plugins: [ 109 | new webpack.DefinePlugin({ 110 | NGXSTORE_CONFIG: JSON.stringify({ 111 | prefix: '', // etc 112 | }) 113 | }), 114 | ] 115 | ``` 116 | 117 | 118 | ## Decorators config 119 | Decorating functions can take config object with the following fields: 120 | - `key: string` - key under the variable will be stored, default key is the variable name 121 | - `mutate: boolean` - enable or disable object mutation for instance, default depends on global config 122 | - `expires: Date` - for `@CookieStorage()` only, specifies expiration date, null = lifetime cookie 123 | - `migrateKey: string` - previously used key from which the value will be transferred to the current `key`, the old key will be cleared afterwards 124 | 125 | ## Usage 126 | 1. Pretty easy to use decorators. Here is where the real magic happens. 127 | ```typescript 128 | import { CookieStorage, LocalStorage, SessionStorage } from 'ngx-store'; 129 | 130 | export class MySuperComponent { 131 | // it will be stored under ${prefix}viewCounts name 132 | @LocalStorage() viewCounts: number = 0; 133 | // this under name: ${prefix}differentLocalStorageKey 134 | @LocalStorage('differentLocalStorageKey') userName: string = ''; 135 | // it will be stored under ${prefix}itWillBeRemovedAfterBrowserClose in session storage 136 | @SessionStorage({key: 'itWillBeRemovedAfterBrowserClose'}) previousUserNames: Array = []; 137 | // it will be read from cookie 'user_id' (can be shared with backend) and saved to localStorage and cookies after change 138 | @LocalStorage() @CookieStorage({prefix: '', key: 'user_id'}) userId: string = ''; 139 | // it will be stored in a cookie named ${prefix}user_workspaces for 24 hours 140 | @CookieStorage({key: 'user_workspaces', expires: new Date(new Date().getTime() + 24 * 60 * 60 * 1000)}) userWorkspaces = []; 141 | // the value will be transferred from localStorage's "location" key 142 | @LocalStorage({key: 'myLocation', migrateKey: 'location'}) myLocation: string = ''; 143 | 144 | constructor() { 145 | this.viewCounts++; 146 | this.userName = 'some name stored in localstorage'; 147 | this.previousUserNames.push(this.userName); 148 | for (let userName of this.previousUserNames) { 149 | console.log(userName); 150 | } 151 | this.previousUserNames.map(userName => userName.split('').reverse().join('')); 152 | } 153 | } 154 | ``` 155 | 156 | **Sharing variables across classes:** Decorated variables can be easily shared across different classes, e.g. Angular Components (also after their destruction) without need to create new service for this purpose. 157 | ```typescript 158 | import { LocalStorage, SharedStorage } from 'ngx-store'; 159 | 160 | export class HomeComponent { 161 | @SharedStorage() title: string = 'Homepage'; // it will be kept in temp memory until app reload 162 | @LocalStorage() userNote: string = 'Leave your note here'; // it will be read from and saved to localStorage 163 | 164 | constructor() { 165 | setTimeout(() => { 166 | console.log('userNote:', this.userNote); // it should be changed after user's visit to NestedComponent 167 | }, 5000); 168 | } 169 | } 170 | 171 | export class NestedComponent { 172 | @SharedStorage('title') homeTitle: string = ''; 173 | @LocalStorage() protected userNote: string = ''; 174 | 175 | constructor() { 176 | console.log('homeTitle:', this.homeTitle); // should print 'Homepage' 177 | console.log('userNote:', this.userNote); // should print userNote set in HomeComponent 178 | this.userNote = "You've visited NestedComponent!"; 179 | } 180 | } 181 | ``` 182 | 183 | **Force save changes:** If you need to modify stored object by not a direct assignment, then you can take advantage of `.save()` method to force save made changes. Example: 184 | ```typescript 185 | import { CookieStorage, LocalStorage, SessionStorage, WebstorableArray } from 'ngx-store'; 186 | 187 | export class MySuperComponent { 188 | @LocalStorage() someObject: any = { c: 3 }; 189 | @SessionStorage() arrayOfSomethings: WebstorableArray = [0,1,2,3,4]; 190 | @CookieStorage({ mutate: false }) someCookie: {version?: number, content?: string} = {}; 191 | 192 | constructor() { 193 | this.someObject.a = 1; 194 | this.someObject['b'] = 2; 195 | delete this.someObject['c']; 196 | for (let i = 0; i < this.arrayOfSomethings.length; i++) { 197 | this.arrayOfSomethings[i] += i; 198 | } 199 | this.someCookie.version++; 200 | this.someCookie.content = 'please save me'; 201 | // upper changes won't be saved without the lines below 202 | this.someObject.save(); 203 | this.arrayOfSomethings.save(); 204 | this.someCookie = this.someCookie; // it looks weird, but also will do the job even without object mutation 205 | } 206 | } 207 | ``` 208 | 209 | **Limited lifecycle classes in AoT compilation:** There is a special case when Service or Component in your application containing decorated variable is being destroyed: 210 | ```typescript 211 | import { OnDestroy } from '@angular/core'; 212 | import { LocalStorage } from 'ngx-store'; 213 | 214 | export class SomeService implements OnDestroy { // implement the interface 215 | @LocalStorage() destroyedVariable: any = {}; 216 | 217 | ngOnDestroy() {} // event empty method is needed to allow ngx-store handle class destruction 218 | } 219 | ``` 220 | 221 | 2. Use the [services](src/service#angular-storage) to manage your data: 222 | ```typescript 223 | import { CookiesStorageService, LocalStorageService, SessionStorageService, SharedStorageService } from 'ngx-store'; 224 | 225 | export class MyService { 226 | constructor( 227 | localStorageService: LocalStorageService, 228 | sessionStorageService: SessionStorageService, 229 | cookiesStorageService: CookiesStorageService, 230 | sharedStorageService: SharedStorageService, 231 | ) { 232 | console.log('all cookies:'); 233 | cookiesStorageService.utility.forEach((value, key) => console.log(key + '=', value)); 234 | } 235 | 236 | public saveSomeData(object: Object, array: Array) { 237 | this.localStorageService.set('someObject', object); 238 | this.sessionStorageService.set('someArray', array); 239 | 240 | this.localStorageService.keys.forEach((key) => { 241 | console.log(key + ' =', this.localStorageService.get(key)); 242 | }); 243 | } 244 | 245 | public clearSomeData(): void { 246 | this.localStorageService.clear('decorators'); // removes only variables created by decorating functions 247 | this.localStorageService.clear('prefix'); // removes variables starting with set prefix (including decorators) 248 | this.sessionStorageService.clear('all'); // removes all session storage data 249 | } 250 | } 251 | ``` 252 | 253 | **Note**: Always define default value at the property you are using decorator. 254 | 255 | **Note**: Never use `for-in` loop on decorated Arrays without filtering by `.hasOwnProperty()`. 256 | 257 | **Note**: Please don't ngx-store circular structures as this library uses JSON.stringify to encode data before saving. 258 | 259 | **Note**: When you change prefix from '' (empty string) old values won't be removed automatically to avoid deleting necessary data. You should handle it manually or set clearType to 'all' for some time. 260 | 261 | **Contributions are welcome!** 262 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | random: false 22 | }, 23 | clearContext: false // leave Jasmine Spec Runner output visible in browser 24 | }, 25 | jasmineHtmlReporter: { 26 | suppressAll: true // removes the duplicated traces 27 | }, 28 | coverageReporter: { 29 | dir: require('path').join(__dirname, '../../coverage/ngx-store'), 30 | subdir: '.', 31 | reporters: [ 32 | { type: 'html' }, 33 | { type: 'text-summary' } 34 | ] 35 | }, 36 | reporters: ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false, 43 | restartOnFileChange: true 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /license-banner.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license ngx-store 3 | * ISC license 4 | */ 5 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "allowedNonPeerDependencies": ["lodash.", "ts-debug", "@ngx-ext/"], 4 | "dest": "../../dist/ngx-store", 5 | "lib": { 6 | "entryFile": "src/public-api.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-store", 3 | "version": "3.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "3.0.1", 9 | "license": "ISC", 10 | "dependencies": { 11 | "@ngx-ext/schematics-api": "latest", 12 | "tslib": ">= 2" 13 | }, 14 | "devDependencies": { 15 | "@types/lodash.get": "^4.4.4", 16 | "@types/lodash.isequal": "^4.5.3", 17 | "@types/lodash.merge": "^4.6.4", 18 | "@types/lodash.set": "^4.3.4", 19 | "@types/node": "^14.14.36", 20 | "ts-debug": "^1.3.0" 21 | }, 22 | "engines": { 23 | "node": ">= 8", 24 | "npm": ">= 5" 25 | }, 26 | "peerDependencies": { 27 | "@angular/common": ">= 4", 28 | "@angular/core": ">= 4", 29 | "lodash.get": ">= 4", 30 | "lodash.isequal": ">= 4", 31 | "lodash.merge": ">= 4", 32 | "lodash.set": ">= 4", 33 | "ts-debug": ">= 1" 34 | } 35 | }, 36 | "node_modules/@angular-devkit/core": { 37 | "version": "12.2.8", 38 | "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.8.tgz", 39 | "integrity": "sha512-N13N1Lm7qllBXSVZYz4Khw75rnQnS3lu5QiJqlsaNklWgVfVz8jt99AAeGGvNGSLEbmZjlr35YLxu8ugD267Ug==", 40 | "peer": true, 41 | "dependencies": { 42 | "ajv": "8.6.2", 43 | "ajv-formats": "2.1.0", 44 | "fast-json-stable-stringify": "2.1.0", 45 | "magic-string": "0.25.7", 46 | "rxjs": "6.6.7", 47 | "source-map": "0.7.3" 48 | }, 49 | "engines": { 50 | "node": "^12.14.1 || >=14.0.0", 51 | "npm": "^6.11.0 || ^7.5.6", 52 | "yarn": ">= 1.13.0" 53 | } 54 | }, 55 | "node_modules/@angular-devkit/core/node_modules/rxjs": { 56 | "version": "6.6.7", 57 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 58 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 59 | "peer": true, 60 | "dependencies": { 61 | "tslib": "^1.9.0" 62 | }, 63 | "engines": { 64 | "npm": ">=2.0.0" 65 | } 66 | }, 67 | "node_modules/@angular-devkit/core/node_modules/tslib": { 68 | "version": "1.14.1", 69 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 70 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 71 | "peer": true 72 | }, 73 | "node_modules/@angular-devkit/schematics": { 74 | "version": "12.2.8", 75 | "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.8.tgz", 76 | "integrity": "sha512-SPiMFoCi1TpFXY6h1xGCakgdwT25gGHdbis1MuHE5yMcPLvhl/yr7EQVY1GY00/iMrgeslTHg/UPp4D6xHyQxA==", 77 | "peer": true, 78 | "dependencies": { 79 | "@angular-devkit/core": "12.2.8", 80 | "ora": "5.4.1", 81 | "rxjs": "6.6.7" 82 | }, 83 | "engines": { 84 | "node": "^12.14.1 || >=14.0.0", 85 | "npm": "^6.11.0 || ^7.5.6", 86 | "yarn": ">= 1.13.0" 87 | } 88 | }, 89 | "node_modules/@angular-devkit/schematics/node_modules/rxjs": { 90 | "version": "6.6.7", 91 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 92 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 93 | "peer": true, 94 | "dependencies": { 95 | "tslib": "^1.9.0" 96 | }, 97 | "engines": { 98 | "npm": ">=2.0.0" 99 | } 100 | }, 101 | "node_modules/@angular-devkit/schematics/node_modules/tslib": { 102 | "version": "1.14.1", 103 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 104 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 105 | "peer": true 106 | }, 107 | "node_modules/@angular/common": { 108 | "version": "12.2.8", 109 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.8.tgz", 110 | "integrity": "sha512-4nFlwC97wNEkB4vU2+xrbzpniuzmw8FG9zfqIeMFLLmceHLR7SQmxVKUrZylNXjT5TXXynpQzrpRAxQ1AEcTSA==", 111 | "peer": true, 112 | "dependencies": { 113 | "tslib": "^2.2.0" 114 | }, 115 | "engines": { 116 | "node": "^12.14.1 || >=14.0.0" 117 | }, 118 | "peerDependencies": { 119 | "@angular/core": "12.2.8", 120 | "rxjs": "^6.5.3 || ^7.0.0" 121 | } 122 | }, 123 | "node_modules/@angular/core": { 124 | "version": "12.2.8", 125 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.8.tgz", 126 | "integrity": "sha512-ko7RJ8BImcMiI64Z8DM54ylkUwu2r/Mhf37BME0EEm+RIrH0KUVzrFOl2rMaxKBZUtY9qaxvVt43bZPrvN2acg==", 127 | "peer": true, 128 | "dependencies": { 129 | "tslib": "^2.2.0" 130 | }, 131 | "engines": { 132 | "node": "^12.14.1 || >=14.0.0" 133 | }, 134 | "peerDependencies": { 135 | "rxjs": "^6.5.3 || ^7.0.0", 136 | "zone.js": "~0.11.4" 137 | } 138 | }, 139 | "node_modules/@ngx-ext/schematics-api": { 140 | "version": "0.0.2", 141 | "resolved": "https://registry.npmjs.org/@ngx-ext/schematics-api/-/schematics-api-0.0.2.tgz", 142 | "integrity": "sha512-A63rtRW5AsI9Vcgvx8RbdoTYXgtEGJDyEopDZG6izL/Qo4Xa6NneBhHLTEi4WchwQqvQfKejNrUvgAxxpE3Jjw==", 143 | "peerDependencies": { 144 | "@angular-devkit/schematics": ">= 9", 145 | "@schematics/angular": ">= 9" 146 | } 147 | }, 148 | "node_modules/@schematics/angular": { 149 | "version": "12.2.8", 150 | "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.8.tgz", 151 | "integrity": "sha512-xkVcX6lTHC5JzDOjGdRAZutVVpxkRkT84vXtVlJwojyhNjAZg5dm/GC84+gVGfmVnO9vkUIYo/vGoN+/ydcSdA==", 152 | "peer": true, 153 | "dependencies": { 154 | "@angular-devkit/core": "12.2.8", 155 | "@angular-devkit/schematics": "12.2.8", 156 | "jsonc-parser": "3.0.0" 157 | }, 158 | "engines": { 159 | "node": "^12.14.1 || >=14.0.0", 160 | "npm": "^6.11.0 || ^7.5.6", 161 | "yarn": ">= 1.13.0" 162 | } 163 | }, 164 | "node_modules/@types/lodash": { 165 | "version": "4.14.175", 166 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", 167 | "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", 168 | "dev": true 169 | }, 170 | "node_modules/@types/lodash.get": { 171 | "version": "4.4.6", 172 | "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.6.tgz", 173 | "integrity": "sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ==", 174 | "dev": true, 175 | "dependencies": { 176 | "@types/lodash": "*" 177 | } 178 | }, 179 | "node_modules/@types/lodash.isequal": { 180 | "version": "4.5.5", 181 | "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz", 182 | "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==", 183 | "dev": true, 184 | "dependencies": { 185 | "@types/lodash": "*" 186 | } 187 | }, 188 | "node_modules/@types/lodash.merge": { 189 | "version": "4.6.6", 190 | "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz", 191 | "integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==", 192 | "dev": true, 193 | "dependencies": { 194 | "@types/lodash": "*" 195 | } 196 | }, 197 | "node_modules/@types/lodash.set": { 198 | "version": "4.3.6", 199 | "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.6.tgz", 200 | "integrity": "sha512-ZeGDDlnRYTvS31Laij0RsSaguIUSBTYIlJFKL3vm3T2OAZAQj2YpSvVWJc0WiG4jqg9fGX6PAPGvDqBcHfSgFg==", 201 | "dev": true, 202 | "dependencies": { 203 | "@types/lodash": "*" 204 | } 205 | }, 206 | "node_modules/@types/node": { 207 | "version": "14.17.20", 208 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", 209 | "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", 210 | "dev": true 211 | }, 212 | "node_modules/ajv": { 213 | "version": "8.6.2", 214 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", 215 | "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", 216 | "peer": true, 217 | "dependencies": { 218 | "fast-deep-equal": "^3.1.1", 219 | "json-schema-traverse": "^1.0.0", 220 | "require-from-string": "^2.0.2", 221 | "uri-js": "^4.2.2" 222 | }, 223 | "funding": { 224 | "type": "github", 225 | "url": "https://github.com/sponsors/epoberezkin" 226 | } 227 | }, 228 | "node_modules/ajv-formats": { 229 | "version": "2.1.0", 230 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", 231 | "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", 232 | "peer": true, 233 | "dependencies": { 234 | "ajv": "^8.0.0" 235 | }, 236 | "peerDependencies": { 237 | "ajv": "^8.0.0" 238 | }, 239 | "peerDependenciesMeta": { 240 | "ajv": { 241 | "optional": true 242 | } 243 | } 244 | }, 245 | "node_modules/ansi-regex": { 246 | "version": "5.0.1", 247 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 248 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 249 | "peer": true, 250 | "engines": { 251 | "node": ">=8" 252 | } 253 | }, 254 | "node_modules/ansi-styles": { 255 | "version": "4.3.0", 256 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 257 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 258 | "peer": true, 259 | "dependencies": { 260 | "color-convert": "^2.0.1" 261 | }, 262 | "engines": { 263 | "node": ">=8" 264 | }, 265 | "funding": { 266 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 267 | } 268 | }, 269 | "node_modules/base64-js": { 270 | "version": "1.5.1", 271 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 272 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 273 | "funding": [ 274 | { 275 | "type": "github", 276 | "url": "https://github.com/sponsors/feross" 277 | }, 278 | { 279 | "type": "patreon", 280 | "url": "https://www.patreon.com/feross" 281 | }, 282 | { 283 | "type": "consulting", 284 | "url": "https://feross.org/support" 285 | } 286 | ], 287 | "peer": true 288 | }, 289 | "node_modules/bl": { 290 | "version": "4.1.0", 291 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 292 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 293 | "peer": true, 294 | "dependencies": { 295 | "buffer": "^5.5.0", 296 | "inherits": "^2.0.4", 297 | "readable-stream": "^3.4.0" 298 | } 299 | }, 300 | "node_modules/buffer": { 301 | "version": "5.7.1", 302 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 303 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 304 | "funding": [ 305 | { 306 | "type": "github", 307 | "url": "https://github.com/sponsors/feross" 308 | }, 309 | { 310 | "type": "patreon", 311 | "url": "https://www.patreon.com/feross" 312 | }, 313 | { 314 | "type": "consulting", 315 | "url": "https://feross.org/support" 316 | } 317 | ], 318 | "peer": true, 319 | "dependencies": { 320 | "base64-js": "^1.3.1", 321 | "ieee754": "^1.1.13" 322 | } 323 | }, 324 | "node_modules/chalk": { 325 | "version": "4.1.2", 326 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 327 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 328 | "peer": true, 329 | "dependencies": { 330 | "ansi-styles": "^4.1.0", 331 | "supports-color": "^7.1.0" 332 | }, 333 | "engines": { 334 | "node": ">=10" 335 | }, 336 | "funding": { 337 | "url": "https://github.com/chalk/chalk?sponsor=1" 338 | } 339 | }, 340 | "node_modules/cli-cursor": { 341 | "version": "3.1.0", 342 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 343 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 344 | "peer": true, 345 | "dependencies": { 346 | "restore-cursor": "^3.1.0" 347 | }, 348 | "engines": { 349 | "node": ">=8" 350 | } 351 | }, 352 | "node_modules/cli-spinners": { 353 | "version": "2.6.1", 354 | "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", 355 | "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", 356 | "peer": true, 357 | "engines": { 358 | "node": ">=6" 359 | }, 360 | "funding": { 361 | "url": "https://github.com/sponsors/sindresorhus" 362 | } 363 | }, 364 | "node_modules/clone": { 365 | "version": "1.0.4", 366 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 367 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", 368 | "peer": true, 369 | "engines": { 370 | "node": ">=0.8" 371 | } 372 | }, 373 | "node_modules/color-convert": { 374 | "version": "2.0.1", 375 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 376 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 377 | "peer": true, 378 | "dependencies": { 379 | "color-name": "~1.1.4" 380 | }, 381 | "engines": { 382 | "node": ">=7.0.0" 383 | } 384 | }, 385 | "node_modules/color-name": { 386 | "version": "1.1.4", 387 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 388 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 389 | "peer": true 390 | }, 391 | "node_modules/defaults": { 392 | "version": "1.0.3", 393 | "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", 394 | "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", 395 | "peer": true, 396 | "dependencies": { 397 | "clone": "^1.0.2" 398 | } 399 | }, 400 | "node_modules/fast-deep-equal": { 401 | "version": "3.1.3", 402 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 403 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 404 | "peer": true 405 | }, 406 | "node_modules/fast-json-stable-stringify": { 407 | "version": "2.1.0", 408 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 409 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 410 | "peer": true 411 | }, 412 | "node_modules/has-flag": { 413 | "version": "4.0.0", 414 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 415 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 416 | "peer": true, 417 | "engines": { 418 | "node": ">=8" 419 | } 420 | }, 421 | "node_modules/ieee754": { 422 | "version": "1.2.1", 423 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 424 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 425 | "funding": [ 426 | { 427 | "type": "github", 428 | "url": "https://github.com/sponsors/feross" 429 | }, 430 | { 431 | "type": "patreon", 432 | "url": "https://www.patreon.com/feross" 433 | }, 434 | { 435 | "type": "consulting", 436 | "url": "https://feross.org/support" 437 | } 438 | ], 439 | "peer": true 440 | }, 441 | "node_modules/inherits": { 442 | "version": "2.0.4", 443 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 444 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 445 | "peer": true 446 | }, 447 | "node_modules/is-interactive": { 448 | "version": "1.0.0", 449 | "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", 450 | "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", 451 | "peer": true, 452 | "engines": { 453 | "node": ">=8" 454 | } 455 | }, 456 | "node_modules/is-unicode-supported": { 457 | "version": "0.1.0", 458 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 459 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 460 | "peer": true, 461 | "engines": { 462 | "node": ">=10" 463 | }, 464 | "funding": { 465 | "url": "https://github.com/sponsors/sindresorhus" 466 | } 467 | }, 468 | "node_modules/json-schema-traverse": { 469 | "version": "1.0.0", 470 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 471 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 472 | "peer": true 473 | }, 474 | "node_modules/jsonc-parser": { 475 | "version": "3.0.0", 476 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", 477 | "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", 478 | "peer": true 479 | }, 480 | "node_modules/lodash.get": { 481 | "version": "4.4.2", 482 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 483 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", 484 | "peer": true 485 | }, 486 | "node_modules/lodash.isequal": { 487 | "version": "4.5.0", 488 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 489 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", 490 | "peer": true 491 | }, 492 | "node_modules/lodash.merge": { 493 | "version": "4.6.2", 494 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 495 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 496 | "peer": true 497 | }, 498 | "node_modules/lodash.set": { 499 | "version": "4.3.2", 500 | "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", 501 | "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", 502 | "peer": true 503 | }, 504 | "node_modules/log-symbols": { 505 | "version": "4.1.0", 506 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 507 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 508 | "peer": true, 509 | "dependencies": { 510 | "chalk": "^4.1.0", 511 | "is-unicode-supported": "^0.1.0" 512 | }, 513 | "engines": { 514 | "node": ">=10" 515 | }, 516 | "funding": { 517 | "url": "https://github.com/sponsors/sindresorhus" 518 | } 519 | }, 520 | "node_modules/magic-string": { 521 | "version": "0.25.7", 522 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 523 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 524 | "peer": true, 525 | "dependencies": { 526 | "sourcemap-codec": "^1.4.4" 527 | } 528 | }, 529 | "node_modules/mimic-fn": { 530 | "version": "2.1.0", 531 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 532 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 533 | "peer": true, 534 | "engines": { 535 | "node": ">=6" 536 | } 537 | }, 538 | "node_modules/onetime": { 539 | "version": "5.1.2", 540 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 541 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 542 | "peer": true, 543 | "dependencies": { 544 | "mimic-fn": "^2.1.0" 545 | }, 546 | "engines": { 547 | "node": ">=6" 548 | }, 549 | "funding": { 550 | "url": "https://github.com/sponsors/sindresorhus" 551 | } 552 | }, 553 | "node_modules/ora": { 554 | "version": "5.4.1", 555 | "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", 556 | "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", 557 | "peer": true, 558 | "dependencies": { 559 | "bl": "^4.1.0", 560 | "chalk": "^4.1.0", 561 | "cli-cursor": "^3.1.0", 562 | "cli-spinners": "^2.5.0", 563 | "is-interactive": "^1.0.0", 564 | "is-unicode-supported": "^0.1.0", 565 | "log-symbols": "^4.1.0", 566 | "strip-ansi": "^6.0.0", 567 | "wcwidth": "^1.0.1" 568 | }, 569 | "engines": { 570 | "node": ">=10" 571 | }, 572 | "funding": { 573 | "url": "https://github.com/sponsors/sindresorhus" 574 | } 575 | }, 576 | "node_modules/punycode": { 577 | "version": "2.1.1", 578 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 579 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 580 | "peer": true, 581 | "engines": { 582 | "node": ">=6" 583 | } 584 | }, 585 | "node_modules/readable-stream": { 586 | "version": "3.6.0", 587 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 588 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 589 | "peer": true, 590 | "dependencies": { 591 | "inherits": "^2.0.3", 592 | "string_decoder": "^1.1.1", 593 | "util-deprecate": "^1.0.1" 594 | }, 595 | "engines": { 596 | "node": ">= 6" 597 | } 598 | }, 599 | "node_modules/require-from-string": { 600 | "version": "2.0.2", 601 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 602 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 603 | "peer": true, 604 | "engines": { 605 | "node": ">=0.10.0" 606 | } 607 | }, 608 | "node_modules/restore-cursor": { 609 | "version": "3.1.0", 610 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 611 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 612 | "peer": true, 613 | "dependencies": { 614 | "onetime": "^5.1.0", 615 | "signal-exit": "^3.0.2" 616 | }, 617 | "engines": { 618 | "node": ">=8" 619 | } 620 | }, 621 | "node_modules/rxjs": { 622 | "version": "7.3.1", 623 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.1.tgz", 624 | "integrity": "sha512-vNenx7gqjPyeKpRnM6S5Ksm/oFTRijWWzYlRON04KaehZ3YjDwEmVjGUGo0TKWVjeNXOujVRlh0K1drUbcdPkw==", 625 | "peer": true, 626 | "dependencies": { 627 | "tslib": "~2.1.0" 628 | } 629 | }, 630 | "node_modules/rxjs/node_modules/tslib": { 631 | "version": "2.1.0", 632 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", 633 | "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", 634 | "peer": true 635 | }, 636 | "node_modules/safe-buffer": { 637 | "version": "5.2.1", 638 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 639 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 640 | "funding": [ 641 | { 642 | "type": "github", 643 | "url": "https://github.com/sponsors/feross" 644 | }, 645 | { 646 | "type": "patreon", 647 | "url": "https://www.patreon.com/feross" 648 | }, 649 | { 650 | "type": "consulting", 651 | "url": "https://feross.org/support" 652 | } 653 | ], 654 | "peer": true 655 | }, 656 | "node_modules/signal-exit": { 657 | "version": "3.0.5", 658 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", 659 | "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", 660 | "peer": true 661 | }, 662 | "node_modules/source-map": { 663 | "version": "0.7.3", 664 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 665 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 666 | "peer": true, 667 | "engines": { 668 | "node": ">= 8" 669 | } 670 | }, 671 | "node_modules/sourcemap-codec": { 672 | "version": "1.4.8", 673 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 674 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 675 | "peer": true 676 | }, 677 | "node_modules/string_decoder": { 678 | "version": "1.3.0", 679 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 680 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 681 | "peer": true, 682 | "dependencies": { 683 | "safe-buffer": "~5.2.0" 684 | } 685 | }, 686 | "node_modules/strip-ansi": { 687 | "version": "6.0.1", 688 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 689 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 690 | "peer": true, 691 | "dependencies": { 692 | "ansi-regex": "^5.0.1" 693 | }, 694 | "engines": { 695 | "node": ">=8" 696 | } 697 | }, 698 | "node_modules/supports-color": { 699 | "version": "7.2.0", 700 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 701 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 702 | "peer": true, 703 | "dependencies": { 704 | "has-flag": "^4.0.0" 705 | }, 706 | "engines": { 707 | "node": ">=8" 708 | } 709 | }, 710 | "node_modules/ts-debug": { 711 | "version": "1.3.0", 712 | "resolved": "https://registry.npmjs.org/ts-debug/-/ts-debug-1.3.0.tgz", 713 | "integrity": "sha512-sP9Q4Nfqu5ImWLH955PpxbjR2zgLWS3NIc2tCw/JZtZMFFxUZe3fvkhdA0vSIpjiGFKPwCg6v0drthjwnSQTGA==", 714 | "dev": true 715 | }, 716 | "node_modules/tslib": { 717 | "version": "2.3.1", 718 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 719 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 720 | }, 721 | "node_modules/uri-js": { 722 | "version": "4.4.1", 723 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 724 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 725 | "peer": true, 726 | "dependencies": { 727 | "punycode": "^2.1.0" 728 | } 729 | }, 730 | "node_modules/util-deprecate": { 731 | "version": "1.0.2", 732 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 733 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 734 | "peer": true 735 | }, 736 | "node_modules/wcwidth": { 737 | "version": "1.0.1", 738 | "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", 739 | "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", 740 | "peer": true, 741 | "dependencies": { 742 | "defaults": "^1.0.3" 743 | } 744 | }, 745 | "node_modules/zone.js": { 746 | "version": "0.11.4", 747 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.4.tgz", 748 | "integrity": "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==", 749 | "peer": true, 750 | "dependencies": { 751 | "tslib": "^2.0.0" 752 | } 753 | } 754 | }, 755 | "dependencies": { 756 | "@angular-devkit/core": { 757 | "version": "12.2.8", 758 | "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.8.tgz", 759 | "integrity": "sha512-N13N1Lm7qllBXSVZYz4Khw75rnQnS3lu5QiJqlsaNklWgVfVz8jt99AAeGGvNGSLEbmZjlr35YLxu8ugD267Ug==", 760 | "peer": true, 761 | "requires": { 762 | "ajv": "8.6.2", 763 | "ajv-formats": "2.1.0", 764 | "fast-json-stable-stringify": "2.1.0", 765 | "magic-string": "0.25.7", 766 | "rxjs": "6.6.7", 767 | "source-map": "0.7.3" 768 | }, 769 | "dependencies": { 770 | "rxjs": { 771 | "version": "6.6.7", 772 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 773 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 774 | "peer": true, 775 | "requires": { 776 | "tslib": "^1.9.0" 777 | } 778 | }, 779 | "tslib": { 780 | "version": "1.14.1", 781 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 782 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 783 | "peer": true 784 | } 785 | } 786 | }, 787 | "@angular-devkit/schematics": { 788 | "version": "12.2.8", 789 | "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.8.tgz", 790 | "integrity": "sha512-SPiMFoCi1TpFXY6h1xGCakgdwT25gGHdbis1MuHE5yMcPLvhl/yr7EQVY1GY00/iMrgeslTHg/UPp4D6xHyQxA==", 791 | "peer": true, 792 | "requires": { 793 | "@angular-devkit/core": "12.2.8", 794 | "ora": "5.4.1", 795 | "rxjs": "6.6.7" 796 | }, 797 | "dependencies": { 798 | "rxjs": { 799 | "version": "6.6.7", 800 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", 801 | "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", 802 | "peer": true, 803 | "requires": { 804 | "tslib": "^1.9.0" 805 | } 806 | }, 807 | "tslib": { 808 | "version": "1.14.1", 809 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", 810 | "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", 811 | "peer": true 812 | } 813 | } 814 | }, 815 | "@angular/common": { 816 | "version": "12.2.8", 817 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-12.2.8.tgz", 818 | "integrity": "sha512-4nFlwC97wNEkB4vU2+xrbzpniuzmw8FG9zfqIeMFLLmceHLR7SQmxVKUrZylNXjT5TXXynpQzrpRAxQ1AEcTSA==", 819 | "peer": true, 820 | "requires": { 821 | "tslib": "^2.2.0" 822 | } 823 | }, 824 | "@angular/core": { 825 | "version": "12.2.8", 826 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-12.2.8.tgz", 827 | "integrity": "sha512-ko7RJ8BImcMiI64Z8DM54ylkUwu2r/Mhf37BME0EEm+RIrH0KUVzrFOl2rMaxKBZUtY9qaxvVt43bZPrvN2acg==", 828 | "peer": true, 829 | "requires": { 830 | "tslib": "^2.2.0" 831 | } 832 | }, 833 | "@ngx-ext/schematics-api": { 834 | "version": "0.0.2", 835 | "resolved": "https://registry.npmjs.org/@ngx-ext/schematics-api/-/schematics-api-0.0.2.tgz", 836 | "integrity": "sha512-A63rtRW5AsI9Vcgvx8RbdoTYXgtEGJDyEopDZG6izL/Qo4Xa6NneBhHLTEi4WchwQqvQfKejNrUvgAxxpE3Jjw==", 837 | "requires": {} 838 | }, 839 | "@schematics/angular": { 840 | "version": "12.2.8", 841 | "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.8.tgz", 842 | "integrity": "sha512-xkVcX6lTHC5JzDOjGdRAZutVVpxkRkT84vXtVlJwojyhNjAZg5dm/GC84+gVGfmVnO9vkUIYo/vGoN+/ydcSdA==", 843 | "peer": true, 844 | "requires": { 845 | "@angular-devkit/core": "12.2.8", 846 | "@angular-devkit/schematics": "12.2.8", 847 | "jsonc-parser": "3.0.0" 848 | } 849 | }, 850 | "@types/lodash": { 851 | "version": "4.14.175", 852 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.175.tgz", 853 | "integrity": "sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==", 854 | "dev": true 855 | }, 856 | "@types/lodash.get": { 857 | "version": "4.4.6", 858 | "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.6.tgz", 859 | "integrity": "sha512-E6zzjR3GtNig8UJG/yodBeJeIOtgPkMgsLjDU3CbgCAPC++vJ0eCMnJhVpRZb/ENqEFlov1+3K9TKtY4UdWKtQ==", 860 | "dev": true, 861 | "requires": { 862 | "@types/lodash": "*" 863 | } 864 | }, 865 | "@types/lodash.isequal": { 866 | "version": "4.5.5", 867 | "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz", 868 | "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==", 869 | "dev": true, 870 | "requires": { 871 | "@types/lodash": "*" 872 | } 873 | }, 874 | "@types/lodash.merge": { 875 | "version": "4.6.6", 876 | "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz", 877 | "integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==", 878 | "dev": true, 879 | "requires": { 880 | "@types/lodash": "*" 881 | } 882 | }, 883 | "@types/lodash.set": { 884 | "version": "4.3.6", 885 | "resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.6.tgz", 886 | "integrity": "sha512-ZeGDDlnRYTvS31Laij0RsSaguIUSBTYIlJFKL3vm3T2OAZAQj2YpSvVWJc0WiG4jqg9fGX6PAPGvDqBcHfSgFg==", 887 | "dev": true, 888 | "requires": { 889 | "@types/lodash": "*" 890 | } 891 | }, 892 | "@types/node": { 893 | "version": "14.17.20", 894 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.20.tgz", 895 | "integrity": "sha512-gI5Sl30tmhXsqkNvopFydP7ASc4c2cLfGNQrVKN3X90ADFWFsPEsotm/8JHSUJQKTHbwowAHtcJPeyVhtKv0TQ==", 896 | "dev": true 897 | }, 898 | "ajv": { 899 | "version": "8.6.2", 900 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", 901 | "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", 902 | "peer": true, 903 | "requires": { 904 | "fast-deep-equal": "^3.1.1", 905 | "json-schema-traverse": "^1.0.0", 906 | "require-from-string": "^2.0.2", 907 | "uri-js": "^4.2.2" 908 | } 909 | }, 910 | "ajv-formats": { 911 | "version": "2.1.0", 912 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", 913 | "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", 914 | "peer": true, 915 | "requires": { 916 | "ajv": "^8.0.0" 917 | } 918 | }, 919 | "ansi-regex": { 920 | "version": "5.0.1", 921 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 922 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 923 | "peer": true 924 | }, 925 | "ansi-styles": { 926 | "version": "4.3.0", 927 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 928 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 929 | "peer": true, 930 | "requires": { 931 | "color-convert": "^2.0.1" 932 | } 933 | }, 934 | "base64-js": { 935 | "version": "1.5.1", 936 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 937 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 938 | "peer": true 939 | }, 940 | "bl": { 941 | "version": "4.1.0", 942 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 943 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 944 | "peer": true, 945 | "requires": { 946 | "buffer": "^5.5.0", 947 | "inherits": "^2.0.4", 948 | "readable-stream": "^3.4.0" 949 | } 950 | }, 951 | "buffer": { 952 | "version": "5.7.1", 953 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 954 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 955 | "peer": true, 956 | "requires": { 957 | "base64-js": "^1.3.1", 958 | "ieee754": "^1.1.13" 959 | } 960 | }, 961 | "chalk": { 962 | "version": "4.1.2", 963 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 964 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 965 | "peer": true, 966 | "requires": { 967 | "ansi-styles": "^4.1.0", 968 | "supports-color": "^7.1.0" 969 | } 970 | }, 971 | "cli-cursor": { 972 | "version": "3.1.0", 973 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 974 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 975 | "peer": true, 976 | "requires": { 977 | "restore-cursor": "^3.1.0" 978 | } 979 | }, 980 | "cli-spinners": { 981 | "version": "2.6.1", 982 | "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", 983 | "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", 984 | "peer": true 985 | }, 986 | "clone": { 987 | "version": "1.0.4", 988 | "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", 989 | "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", 990 | "peer": true 991 | }, 992 | "color-convert": { 993 | "version": "2.0.1", 994 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 995 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 996 | "peer": true, 997 | "requires": { 998 | "color-name": "~1.1.4" 999 | } 1000 | }, 1001 | "color-name": { 1002 | "version": "1.1.4", 1003 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1004 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1005 | "peer": true 1006 | }, 1007 | "defaults": { 1008 | "version": "1.0.3", 1009 | "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", 1010 | "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", 1011 | "peer": true, 1012 | "requires": { 1013 | "clone": "^1.0.2" 1014 | } 1015 | }, 1016 | "fast-deep-equal": { 1017 | "version": "3.1.3", 1018 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1019 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1020 | "peer": true 1021 | }, 1022 | "fast-json-stable-stringify": { 1023 | "version": "2.1.0", 1024 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1025 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 1026 | "peer": true 1027 | }, 1028 | "has-flag": { 1029 | "version": "4.0.0", 1030 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1031 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1032 | "peer": true 1033 | }, 1034 | "ieee754": { 1035 | "version": "1.2.1", 1036 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1037 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1038 | "peer": true 1039 | }, 1040 | "inherits": { 1041 | "version": "2.0.4", 1042 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1043 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1044 | "peer": true 1045 | }, 1046 | "is-interactive": { 1047 | "version": "1.0.0", 1048 | "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", 1049 | "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", 1050 | "peer": true 1051 | }, 1052 | "is-unicode-supported": { 1053 | "version": "0.1.0", 1054 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1055 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1056 | "peer": true 1057 | }, 1058 | "json-schema-traverse": { 1059 | "version": "1.0.0", 1060 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 1061 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 1062 | "peer": true 1063 | }, 1064 | "jsonc-parser": { 1065 | "version": "3.0.0", 1066 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", 1067 | "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", 1068 | "peer": true 1069 | }, 1070 | "lodash.get": { 1071 | "version": "4.4.2", 1072 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 1073 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", 1074 | "peer": true 1075 | }, 1076 | "lodash.isequal": { 1077 | "version": "4.5.0", 1078 | "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", 1079 | "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", 1080 | "peer": true 1081 | }, 1082 | "lodash.merge": { 1083 | "version": "4.6.2", 1084 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1085 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1086 | "peer": true 1087 | }, 1088 | "lodash.set": { 1089 | "version": "4.3.2", 1090 | "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", 1091 | "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", 1092 | "peer": true 1093 | }, 1094 | "log-symbols": { 1095 | "version": "4.1.0", 1096 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1097 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1098 | "peer": true, 1099 | "requires": { 1100 | "chalk": "^4.1.0", 1101 | "is-unicode-supported": "^0.1.0" 1102 | } 1103 | }, 1104 | "magic-string": { 1105 | "version": "0.25.7", 1106 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 1107 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 1108 | "peer": true, 1109 | "requires": { 1110 | "sourcemap-codec": "^1.4.4" 1111 | } 1112 | }, 1113 | "mimic-fn": { 1114 | "version": "2.1.0", 1115 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 1116 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 1117 | "peer": true 1118 | }, 1119 | "onetime": { 1120 | "version": "5.1.2", 1121 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 1122 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 1123 | "peer": true, 1124 | "requires": { 1125 | "mimic-fn": "^2.1.0" 1126 | } 1127 | }, 1128 | "ora": { 1129 | "version": "5.4.1", 1130 | "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", 1131 | "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", 1132 | "peer": true, 1133 | "requires": { 1134 | "bl": "^4.1.0", 1135 | "chalk": "^4.1.0", 1136 | "cli-cursor": "^3.1.0", 1137 | "cli-spinners": "^2.5.0", 1138 | "is-interactive": "^1.0.0", 1139 | "is-unicode-supported": "^0.1.0", 1140 | "log-symbols": "^4.1.0", 1141 | "strip-ansi": "^6.0.0", 1142 | "wcwidth": "^1.0.1" 1143 | } 1144 | }, 1145 | "punycode": { 1146 | "version": "2.1.1", 1147 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1148 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1149 | "peer": true 1150 | }, 1151 | "readable-stream": { 1152 | "version": "3.6.0", 1153 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1154 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1155 | "peer": true, 1156 | "requires": { 1157 | "inherits": "^2.0.3", 1158 | "string_decoder": "^1.1.1", 1159 | "util-deprecate": "^1.0.1" 1160 | } 1161 | }, 1162 | "require-from-string": { 1163 | "version": "2.0.2", 1164 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 1165 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 1166 | "peer": true 1167 | }, 1168 | "restore-cursor": { 1169 | "version": "3.1.0", 1170 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 1171 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 1172 | "peer": true, 1173 | "requires": { 1174 | "onetime": "^5.1.0", 1175 | "signal-exit": "^3.0.2" 1176 | } 1177 | }, 1178 | "rxjs": { 1179 | "version": "7.3.1", 1180 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.3.1.tgz", 1181 | "integrity": "sha512-vNenx7gqjPyeKpRnM6S5Ksm/oFTRijWWzYlRON04KaehZ3YjDwEmVjGUGo0TKWVjeNXOujVRlh0K1drUbcdPkw==", 1182 | "peer": true, 1183 | "requires": { 1184 | "tslib": "~2.1.0" 1185 | }, 1186 | "dependencies": { 1187 | "tslib": { 1188 | "version": "2.1.0", 1189 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", 1190 | "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", 1191 | "peer": true 1192 | } 1193 | } 1194 | }, 1195 | "safe-buffer": { 1196 | "version": "5.2.1", 1197 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1198 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1199 | "peer": true 1200 | }, 1201 | "signal-exit": { 1202 | "version": "3.0.5", 1203 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", 1204 | "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", 1205 | "peer": true 1206 | }, 1207 | "source-map": { 1208 | "version": "0.7.3", 1209 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 1210 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 1211 | "peer": true 1212 | }, 1213 | "sourcemap-codec": { 1214 | "version": "1.4.8", 1215 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 1216 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 1217 | "peer": true 1218 | }, 1219 | "string_decoder": { 1220 | "version": "1.3.0", 1221 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1222 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1223 | "peer": true, 1224 | "requires": { 1225 | "safe-buffer": "~5.2.0" 1226 | } 1227 | }, 1228 | "strip-ansi": { 1229 | "version": "6.0.1", 1230 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1231 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1232 | "peer": true, 1233 | "requires": { 1234 | "ansi-regex": "^5.0.1" 1235 | } 1236 | }, 1237 | "supports-color": { 1238 | "version": "7.2.0", 1239 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1240 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1241 | "peer": true, 1242 | "requires": { 1243 | "has-flag": "^4.0.0" 1244 | } 1245 | }, 1246 | "ts-debug": { 1247 | "version": "1.3.0", 1248 | "resolved": "https://registry.npmjs.org/ts-debug/-/ts-debug-1.3.0.tgz", 1249 | "integrity": "sha512-sP9Q4Nfqu5ImWLH955PpxbjR2zgLWS3NIc2tCw/JZtZMFFxUZe3fvkhdA0vSIpjiGFKPwCg6v0drthjwnSQTGA==", 1250 | "dev": true 1251 | }, 1252 | "tslib": { 1253 | "version": "2.3.1", 1254 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 1255 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 1256 | }, 1257 | "uri-js": { 1258 | "version": "4.4.1", 1259 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1260 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1261 | "peer": true, 1262 | "requires": { 1263 | "punycode": "^2.1.0" 1264 | } 1265 | }, 1266 | "util-deprecate": { 1267 | "version": "1.0.2", 1268 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1269 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1270 | "peer": true 1271 | }, 1272 | "wcwidth": { 1273 | "version": "1.0.1", 1274 | "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", 1275 | "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", 1276 | "peer": true, 1277 | "requires": { 1278 | "defaults": "^1.0.3" 1279 | } 1280 | }, 1281 | "zone.js": { 1282 | "version": "0.11.4", 1283 | "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.4.tgz", 1284 | "integrity": "sha512-DDh2Ab+A/B+9mJyajPjHFPWfYU1H+pdun4wnnk0OcQTNjem1XQSZ2CDW+rfZEUDjv5M19SBqAkjZi0x5wuB5Qw==", 1285 | "peer": true, 1286 | "requires": { 1287 | "tslib": "^2.0.0" 1288 | } 1289 | } 1290 | } 1291 | } 1292 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-store", 3 | "version": "3.1.1", 4 | "author": "Daniel Kucal", 5 | "license": "ISC", 6 | "description": "Angular decorators to automagically keep variables in HTML5 LocalStorage, SessionStorage, cookies; injectable services for managing and listening to data changes and a bit more.", 7 | "scripts": { 8 | "build": "ng build --prod", 9 | "prebuild": "npm run lint", 10 | "lint": "tslint ./*.ts -t verbose", 11 | "test": "ng test ngx-store --browsers ChromeHeadless --watch=false", 12 | "test:watch": "ng test ngx-store", 13 | "prepublishOnly": "npm run test", 14 | "pack-lib": "npm run build && npm pack ./dist", 15 | "publish-lib:rc": "npm run build && npm publish --tag RC ../../dist/ngx-store", 16 | "compodoc": "compodoc -p tsconfig.json", 17 | "compodoc-serve": "compodoc -s", 18 | "build:schematics": "../../node_modules/.bin/tsc -p tsconfig.schematics.json", 19 | "copy:collection": "cp schematics/collection.json ../../dist/ngx-store/schematics/collection.json", 20 | "postbuild": "npm run build:schematics && npm run copy:collection" 21 | }, 22 | "schematics": "./schematics/collection.json", 23 | "ng-add": { 24 | "save": "dependencies" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/zoomsphere/ngx-store.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/zoomsphere/ngx-store/issues" 32 | }, 33 | "homepage": "https://github.com/zoomsphere/ngx-store", 34 | "keywords": [ 35 | "Angular", 36 | "LocalStorage", 37 | "SessionStorage", 38 | "Storage", 39 | "Wrapper", 40 | "Cookies", 41 | "Decorator", 42 | "Library" 43 | ], 44 | "peerDependencies": { 45 | "@angular/common": ">= 4", 46 | "@angular/core": ">= 4", 47 | "rxjs": ">= 6", 48 | "lodash.get": ">= 4", 49 | "lodash.isequal": ">= 4", 50 | "lodash.merge": ">= 4", 51 | "lodash.set": ">= 4", 52 | "ts-debug": ">= 1" 53 | }, 54 | "dependencies": { 55 | "@ngx-ext/schematics-api": "latest", 56 | "tslib": ">= 2", 57 | "ts-debug": ">= 1" 58 | }, 59 | "devDependencies": { 60 | "@types/lodash.get": "^4.4.4", 61 | "@types/lodash.isequal": "^4.5.3", 62 | "@types/lodash.merge": "^4.6.4", 63 | "@types/lodash.set": "^4.3.4", 64 | "@types/node": "^14.14.36", 65 | "ts-debug": "^1.3.0" 66 | }, 67 | "engines": { 68 | "node": ">= 8", 69 | "npm": ">= 5" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "ng-add": { 5 | "description": "Adds WebStorageModule to AppModule.", 6 | "factory": "./ng-add/index#ngAdd" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /schematics/ng-add/add-config-to-index.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; 2 | 3 | export function addConfigToIndex(): Rule { 4 | return (tree: Tree, _context: SchematicContext) => { 5 | const fileName = './src/index.html'; 6 | const content = tree.read(fileName)?.toString(); 7 | if (!content) { 8 | throw new SchematicsException(`Couldn't find src/index.html file`); 9 | } 10 | const head = ''; 11 | const position = content.indexOf(head) + head.length; 12 | const template = `\n\t`; 13 | const recorder = tree.beginUpdate(fileName); 14 | recorder.insertRight(position, template); 15 | tree.commitUpdate(recorder); 16 | return tree; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /schematics/ng-add/add-import-to-root-module.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, Tree } from '@angular-devkit/schematics'; 2 | import { RootModule } from '@ngx-ext/schematics-api'; 3 | 4 | export function addImportToRootModule(): Rule { 5 | return (tree: Tree) => { 6 | const rootModule = RootModule.getInstance(tree); 7 | rootModule.addImport('WebStorageModule.forRoot()', 'ngx-store'); 8 | return rootModule.applyAllChanges(); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /schematics/ng-add/create-config-file.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | 3 | const content = 4 | `var NGXSTORE_CONFIG = { 5 | prefix: 'bgi_', // default: 'ngx_' 6 | clearType: 'prefix', // default: 'prefix' 7 | mutateObjects: true, // default: true 8 | debugMode: false, // you can enable debug logs if you ever meet any bug to localize its source 9 | cookiesScope: '', // what you pass here will prepend base domain e.g. "." => .domain.com (all subdomains) 10 | cookiesCheckInterval: 0, // number in ms describing how often cookies should be checked for changes, 0 = disabled 11 | };`; 12 | 13 | export function createConfigFile(): Rule { 14 | return (tree: Tree, _context: SchematicContext) => { 15 | const fileName = './src/assets/js/ngx-store.config.js'; 16 | tree.create(fileName, content); 17 | return tree; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /schematics/ng-add/index.ts: -------------------------------------------------------------------------------- 1 | import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | import { addImportToRootModule } from './add-import-to-root-module.rule'; 3 | import { installDependencies } from './install-dependencies.rule'; 4 | import { addConfigToIndex } from './add-config-to-index.rule'; 5 | import { createConfigFile } from './create-config-file.rule'; 6 | 7 | export function ngAdd(_options: any): Rule { 8 | return (_tree: Tree, _context: SchematicContext) => { 9 | return chain([ 10 | installDependencies(), 11 | addImportToRootModule(), 12 | createConfigFile(), 13 | addConfigToIndex(), 14 | ]); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /schematics/ng-add/install-dependencies.rule.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; 3 | 4 | export function installDependencies(): Rule { 5 | return (tree: Tree, context: SchematicContext) => { 6 | context.addTask(new NodePackageInstallTask()); 7 | context.addTask(new NodePackageInstallTask({packageName: 'lodash.get'})); 8 | context.addTask(new NodePackageInstallTask({packageName: 'lodash.set'})); 9 | context.addTask(new NodePackageInstallTask({packageName: 'lodash.merge'})); 10 | context.addTask(new NodePackageInstallTask({packageName: 'lodash.isequal'})); 11 | context.addTask(new NodePackageInstallTask({packageName: 'ts-debug'})); 12 | return tree; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/config/config.helper.ts: -------------------------------------------------------------------------------- 1 | export const CONFIG_PREFIX = 'NGX-STORE_'; 2 | 3 | export class ConfigHelper { 4 | public static getItem(key: string): any { 5 | return localStorage.getItem(CONFIG_PREFIX + key); 6 | } 7 | 8 | public static setItem(key: string, item: any): void { 9 | return localStorage.setItem(CONFIG_PREFIX + key, item); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/config/config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface WebStorageConfigInterface { 2 | prefix?: string; 3 | previousPrefix?: string; 4 | clearType?: ClearType; 5 | mutateObjects?: boolean; 6 | cookiesScope?: string; 7 | cookiesCheckInterval?: number; 8 | debugMode?: boolean; 9 | } 10 | 11 | export type ClearType = 'decorators' | 'prefix' | 'all'; 12 | -------------------------------------------------------------------------------- /src/lib/config/config.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageConfigInterface } from './config.interface'; 2 | import { ConfigHelper } from './config.helper'; 3 | import { Debugger } from 'ts-debug'; 4 | 5 | // TODO allow to set different config for local and session storage 6 | // TODO check if NGXSTORE_CONFIG implements WebStorageConfigInterface 7 | // TODO allow to set configuration in node-config (`config` on npm) 8 | 9 | export { CONFIG_PREFIX } from './config.helper'; 10 | 11 | const DefaultConfig: WebStorageConfigInterface = { 12 | prefix: 'ngx_', 13 | previousPrefix: 'angular2ws_', 14 | clearType: 'prefix', 15 | mutateObjects: true, 16 | cookiesScope: '', 17 | cookiesCheckInterval: 0, 18 | debugMode: false, 19 | }; 20 | 21 | // take configuration provided as a global variable 22 | declare const NGXSTORE_CONFIG: WebStorageConfigInterface; 23 | 24 | let ConfigFills: WebStorageConfigInterface = {}; 25 | const localStoragePrefix = ConfigHelper.getItem('prefix'); 26 | 27 | if (typeof NGXSTORE_CONFIG === 'object') { 28 | ConfigFills = Object.assign({}, NGXSTORE_CONFIG); 29 | } 30 | 31 | if (localStoragePrefix !== undefined && localStoragePrefix !== null) { 32 | ConfigFills.previousPrefix = localStoragePrefix; 33 | } else if (ConfigFills.previousPrefix === undefined) { 34 | ConfigFills.previousPrefix = DefaultConfig.previousPrefix; 35 | } 36 | 37 | // merge default config, deprecated config and global config all together 38 | export const Config: WebStorageConfigInterface = 39 | Object.assign({}, DefaultConfig, ConfigFills); 40 | 41 | export const debug = new Debugger(console, Config.debugMode, '[ngx-store] '); 42 | ConfigHelper.setItem('prefix', Config.prefix); 43 | -------------------------------------------------------------------------------- /src/lib/decorator/cache.interface.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageServiceInterface } from '../service/webstorage.interface'; 2 | import { WebStorageUtility } from '../utility/webstorage.utility'; 3 | import { DecoratorConfig } from '../ngx-store.types'; 4 | 5 | export interface CacheItemInterface { 6 | key: string; 7 | name: string; 8 | targets: Array; 9 | services: Array; 10 | utilities: Array; 11 | } 12 | 13 | export interface UtilityEntry { 14 | utility: WebStorageUtility; 15 | config?: DecoratorConfig; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/decorator/cache.ts: -------------------------------------------------------------------------------- 1 | import { Config, debug } from '../config/config'; 2 | import { CacheItemInterface, UtilityEntry } from './cache.interface'; 3 | import { WebStorageServiceInterface } from '../service/webstorage.interface'; 4 | import { DecoratorConfig } from '../ngx-store.types'; 5 | import { WebStorageUtility } from '../utility/webstorage.utility'; 6 | 7 | const isEqual = require('lodash.isequal'); 8 | 9 | export class Cache { 10 | public static items: Map = new Map(); 11 | 12 | public static getCacheFor(cacheCandidate: CacheItemInterface): CacheItem { 13 | let cacheItem = Cache.get(cacheCandidate.key); 14 | if (!cacheItem) { 15 | cacheItem = new CacheItem(cacheCandidate); 16 | debug.log(`Created new CacheItem for ${cacheCandidate.name} for ${cacheItem.utilities[0].utility.getStorageName()}`); 17 | Cache.set(cacheItem); 18 | return cacheItem; 19 | } 20 | debug.log(`Loaded prior CacheItem of ${cacheItem.name} 21 | for ${cacheCandidate.utilities[0].utility.getStorageName()}`); 22 | cacheItem.addTargets(cacheCandidate.targets); 23 | cacheItem.addServices(cacheCandidate.services); 24 | cacheItem.addUtilities(cacheCandidate.utilities); 25 | Cache.set(cacheItem); 26 | return cacheItem; 27 | } 28 | 29 | public static remove(cacheItem: CacheItem): boolean { 30 | return Cache.items.delete(cacheItem.key); 31 | } 32 | 33 | public static get(key: string): CacheItem { 34 | return Cache.items.get(key) as CacheItem; 35 | } 36 | 37 | protected static set(cacheItem: CacheItem): void { 38 | if (!Cache.get(cacheItem.key)) { 39 | debug.log('CacheItem for ' + cacheItem.key, cacheItem); 40 | } 41 | Cache.items.set(cacheItem.key, cacheItem); 42 | } 43 | } 44 | 45 | // tslint:disable:only-arrow-functions 46 | export class CacheItem implements CacheItemInterface { 47 | public name: string = ''; 48 | public targets: Array = []; 49 | public services: Array = []; 50 | public utilities: Array = []; 51 | public currentTarget: object = {}; 52 | protected proxy: any = null; 53 | protected _key: string = ''; 54 | protected initializedTargets: Set = new Set(); 55 | 56 | constructor(cacheItem: CacheItemInterface) { 57 | this._key = cacheItem.key; 58 | this.name = cacheItem.name; 59 | this.addTargets(cacheItem.targets); 60 | this.addServices(cacheItem.services); 61 | this.addUtilities(cacheItem.utilities); 62 | } 63 | 64 | public get key(): string { 65 | return this._key; 66 | } 67 | 68 | public saveValue(value: any, config: DecoratorConfig = {}): any { 69 | debug.groupCollapsed('CacheItem#saveValue for ' + this.key + ' in ' + this.currentTarget.constructor.name); 70 | debug.log('new value: ', value); 71 | // if (value === false && this.readValue() === true) debugger; 72 | debug.log('previous value: ', this.readValue()); 73 | debug.log('targets.length: ', this.targets.length); 74 | debug.log('currentTarget:', this.currentTarget); 75 | debug.groupEnd(); 76 | 77 | // prevent overwriting value by initializators 78 | if (!this.initializedTargets.has(this.currentTarget)) { 79 | this.initializedTargets.add(this.currentTarget); 80 | let readValue = this.readValue(); 81 | if (config.migrateKey) { 82 | this.migrate(config, this.utilities[0].utility); 83 | readValue = this.readValue(); 84 | } 85 | const savedValue = (readValue !== null && readValue !== undefined) ? readValue : value; 86 | let proxy = this.getProxy(savedValue, config); 87 | proxy = (proxy !== null) ? proxy : value; 88 | debug.log('initial value for ' + this.key + ' in ' + this.currentTarget.constructor.name, proxy); 89 | this.propagateChange(savedValue); 90 | return proxy; 91 | } 92 | this.propagateChange(value); 93 | return this.getProxy(value, config); 94 | } 95 | 96 | public getProxy(value?: any, config: DecoratorConfig = {}): any { 97 | if (value === undefined && this.proxy) { 98 | return this.proxy; 99 | } // return cached proxy if value hasn't changed 100 | value = (value === undefined) ? this.readValue() : value; 101 | if (typeof value !== 'object' || value === null) { 102 | this.proxy = value; 103 | return value; 104 | } 105 | if ((!Config.mutateObjects && !config.mutate) || config.mutate === false) { 106 | return value; 107 | } 108 | 109 | const _self = this; // alias to use in standard function expressions 110 | const prototype: any = Object.assign(new value.constructor(), value.__proto__); 111 | 112 | prototype.save = function(): void { // add method for triggering force save 113 | _self.saveValue(value, config); 114 | }; 115 | 116 | // TODO set prototype for Array.prototype or something 117 | if (Array.isArray(value)) { // handle methods that could change value of array 118 | const methodsToOverwrite = [ 119 | 'pop', 'push', 'reverse', 'shift', 'unshift', 'splice', 120 | 'filter', 'forEach', 'map', 'fill', 'sort', 'copyWithin', 121 | ]; 122 | for (const method of methodsToOverwrite) { 123 | prototype[method] = function(): void { 124 | const readValue = _self.readValue(); 125 | // @ts-ignore 126 | const result = Array.prototype[method].apply(readValue, arguments); 127 | debug.log('Saving value for ' + _self.key + ' by method ' 128 | + prototype.constructor.name + '.' + method); 129 | _self.saveValue(readValue, config); 130 | return result; 131 | }; 132 | } 133 | } 134 | Object.setPrototypeOf(value, prototype); 135 | this.proxy = value; 136 | return value; 137 | } 138 | 139 | public readValue(config: DecoratorConfig = {}): any { 140 | const entry = this.utilities[0]; 141 | const value = entry ? entry.utility.get(this.key, entry.config) : null; 142 | return (typeof value !== 'object') ? value : JSON.parse(JSON.stringify(this.getProxy(value, entry.config))); 143 | } 144 | 145 | public addTargets(targets: Array): void { 146 | targets.forEach(target => { 147 | if (this.targets.indexOf(target) === -1) { 148 | if (typeof target === 'object') { // handle Angular Component destruction 149 | const originalFunction = target.ngOnDestroy; 150 | const _self = this; 151 | target.ngOnDestroy = function(): any { 152 | if (typeof originalFunction === 'function') { 153 | originalFunction.apply(this, arguments); 154 | } 155 | target.ngOnDestroy = originalFunction || function(): any {}; 156 | 157 | _self.initializedTargets.delete(target); 158 | _self.targets = _self.targets.filter(t => t !== target); 159 | if (!_self.targets.length) { 160 | _self.services.forEach(service => { 161 | service.keys = service.keys.filter(key => key !== _self._key); 162 | }); 163 | _self.resetProxy(); 164 | Cache.remove(_self); 165 | } 166 | debug.groupCollapsed(`${_self.key} OnDestroy handler:`); 167 | debug.log('removed target:', target.constructor.name); 168 | debug.log('remaining targets:', _self.targets); 169 | debug.log('cacheItem:', Cache.get(_self.key)); 170 | debug.groupEnd(); 171 | }; 172 | this.targets.push(target); 173 | } 174 | } 175 | }); 176 | } 177 | 178 | public addServices(services: Array): void { 179 | services.forEach(service => { 180 | if (this.services.indexOf(service) === -1) { 181 | service.keys.push(this._key); 182 | this.services.push(service); 183 | } 184 | }); 185 | } 186 | 187 | public addUtilities(utilityEntries: Array): void { 188 | utilityEntries.forEach(entry => { 189 | if (this.utilities.findIndex(e => e.utility === entry.utility) === -1) { 190 | this.utilities.push(entry); 191 | entry.utility.set(this.key, this.readValue()); 192 | } 193 | }); 194 | } 195 | 196 | public resetProxy(): void { 197 | this.proxy = null; 198 | } 199 | 200 | public propagateChange(value: any, source?: WebStorageUtility): void { 201 | if (isEqual(value, this.readValue())) { 202 | return; 203 | } 204 | this.utilities.forEach(entry => { 205 | const utility = entry.utility; 206 | // updating service which the change came from would affect in a cycle 207 | if (utility === source) { 208 | return; 209 | } 210 | debug.log(`propagating change on ${this.key} to:`, utility); 211 | utility.set(this._key, value, entry.config); 212 | }); 213 | } 214 | 215 | protected migrate(config: DecoratorConfig, utility: WebStorageUtility): void { 216 | const prefix = (config.prefix || Config.prefix) || ''; 217 | const keyExists = (key: string): boolean => key in utility.getStorage(); 218 | const migrateKey = keyExists(prefix + config.migrateKey!) 219 | ? prefix + config.migrateKey! 220 | : config.migrateKey!; 221 | if (!keyExists(migrateKey)) { 222 | return; 223 | } 224 | debug.log('Migrating', migrateKey, 'to', config.key, 'in', utility.getStorageName()); 225 | const value = utility.get(migrateKey, {...config, prefix: ''}); 226 | utility.set(this._key, value); 227 | utility.remove(migrateKey, {prefix: ''}); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/lib/decorator/webstorage.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { CookieStorage, LocalStorage, SessionStorage, SharedStorage } from './webstorage'; 3 | import { SharedStorageService } from '../service/shared-storage.service'; 4 | import { LocalStorageService } from '../service/local-storage.service'; 5 | import { SessionStorageService } from '../service/session-storage.service'; 6 | import { CookiesStorageService } from '../service/cookies-storage.service'; 7 | import { WebstorableArray, WebstorableObject } from '../ngx-store.types'; 8 | import { NgxStoreModule } from '../ngx-store.module'; 9 | import { OnDestroy } from '@angular/core'; 10 | 11 | sessionStorage.setItem('ngx_twoDecorators', '128'); 12 | 13 | class TestClass { 14 | @LocalStorage() localStorageVariable: string = ''; 15 | @SessionStorage() sessionStorageVariable: number = 42; 16 | @CookieStorage() cookieStorageVariable: boolean = false; 17 | @SharedStorage() sharedStorageVariable: any = null; 18 | @LocalStorage() arrayVariable: Array = []; 19 | @SessionStorage('customKeyVariable') customKey: WebstorableArray = ['some', 'values'] as any; 20 | // TODO make it working with {key: 'customObject', prefix: ''} 21 | @CookieStorage('customObject') customCookie: WebstorableObject = {} as any; 22 | 23 | @SharedStorage() @LocalStorage() arrayOfObjects: Array = [{}, {}, {}]; 24 | @SharedStorage() @SessionStorage() twoDecorators: number = 0; 25 | } 26 | 27 | class TestClass2 { 28 | @LocalStorage() localStorageVariable: string = ''; 29 | @SessionStorage() sessionStorageVariable: number = 0; 30 | @CookieStorage() cookieStorageVariable: boolean = false; 31 | @SharedStorage() sharedStorageVariable: any = null; 32 | } 33 | 34 | const testClass: TestClass = new TestClass(); 35 | let testClass2: TestClass2; 36 | 37 | describe('Decorators', () => { 38 | let localStorageService: LocalStorageService; 39 | let sessionStorageService: SessionStorageService; 40 | let cookiesStorageService: CookiesStorageService; 41 | let sharedStorageService: SharedStorageService; 42 | 43 | beforeEach(() => { 44 | TestBed.configureTestingModule({ 45 | imports: [NgxStoreModule], 46 | }); 47 | }); 48 | 49 | beforeEach(inject([ 50 | LocalStorageService, 51 | SessionStorageService, 52 | CookiesStorageService, 53 | SharedStorageService], 54 | ( 55 | localStorageServiceInjection: LocalStorageService, 56 | sessionStorageServiceInjection: SessionStorageService, 57 | cookiesStorageServiceInjection: CookiesStorageService, 58 | sharedStorageServiceInjection: SharedStorageService, 59 | ) => { 60 | localStorageService = localStorageServiceInjection; 61 | sessionStorageService = sessionStorageServiceInjection; 62 | cookiesStorageService = cookiesStorageServiceInjection; 63 | sharedStorageService = sharedStorageServiceInjection; 64 | })); 65 | 66 | it('initial data should be as set in TestClass', () => { 67 | expect(testClass.localStorageVariable).toBe(''); 68 | expect(testClass.sessionStorageVariable).toBe(42); 69 | expect(testClass.cookieStorageVariable).toBe(false); 70 | expect(testClass.sharedStorageVariable).toBe(null); 71 | expect(testClass.twoDecorators).toBe(128); 72 | expect(testClass.arrayVariable).toEqual([]); 73 | expect(testClass.customKey).toEqual(['some', 'values']); 74 | expect(testClass.customCookie).toEqual({} as any); 75 | expect(testClass.arrayOfObjects.length).toBe(3); 76 | }); 77 | 78 | it('data in services should be equal like in decorators', () => { 79 | expect(localStorageService.get('localStorageVariable')).toBe(''); 80 | expect(sessionStorageService.get('sessionStorageVariable')).toBe(42); 81 | expect(cookiesStorageService.get('cookieStorageVariable')).toBe(false); 82 | expect(sharedStorageService.get('sharedStorageVariable')).toBe(null); 83 | expect(sharedStorageService.get('twoDecorators')).toBe(128); 84 | expect(localStorageService.get('arrayVariable')).toEqual([]); 85 | expect(sessionStorageService.get('customKeyVariable')).toEqual(['some', 'values']); 86 | expect(cookiesStorageService.get('customObject')).toEqual({}); 87 | expect(sharedStorageService.get('arrayOfObjects').length).toBe(3); 88 | expect(localStorageService.get('arrayOfObjects').length).toBe(3); 89 | }); 90 | 91 | it('changes in decorators should be reflected in services', () => { 92 | testClass.localStorageVariable = '43'; 93 | testClass.sessionStorageVariable++; 94 | testClass.cookieStorageVariable = true; 95 | testClass.sharedStorageVariable = {}; 96 | testClass.twoDecorators = 43; 97 | testClass.arrayVariable.push(1); 98 | testClass.arrayVariable.push(2); 99 | testClass.arrayVariable.unshift(0); 100 | testClass.customKey.reverse(); 101 | testClass.customCookie.newProperty = true; 102 | const anotherProperty = { 103 | a: 'a', 104 | b: 'b', 105 | c: false, 106 | d: 3, 107 | e: [3, 4, 5], 108 | }; 109 | testClass.customCookie.anotherProperty = anotherProperty; 110 | testClass.customCookie.save(); 111 | testClass.arrayOfObjects.map(object => { 112 | object.property = true; 113 | }); 114 | testClass.arrayOfObjects.pop(); 115 | testClass.arrayOfObjects.shift(); 116 | expect(testClass.localStorageVariable).toBe('43'); 117 | expect(testClass.sessionStorageVariable).toBe(43); 118 | expect(testClass.cookieStorageVariable).toBe(true); 119 | expect(testClass.sharedStorageVariable).toEqual({}); 120 | expect(testClass.twoDecorators).toBe(43); 121 | expect(testClass.arrayVariable).toEqual([0, 1, 2]); 122 | expect(testClass.customKey).toEqual(['values', 'some']); 123 | expect(testClass.customCookie).toEqual({newProperty: true, anotherProperty} as any); 124 | expect(testClass.arrayOfObjects).toEqual([{property: true}]); 125 | expect(localStorageService.get('localStorageVariable')).toBe('43'); 126 | expect(sessionStorageService.get('sessionStorageVariable')).toBe(43); 127 | expect(cookiesStorageService.get('cookieStorageVariable')).toBe(true); 128 | expect(sharedStorageService.get('sharedStorageVariable')).toEqual({}); 129 | expect(sharedStorageService.get('twoDecorators')).toBe(43); 130 | expect(localStorageService.get('arrayVariable')).toEqual([0, 1, 2]); 131 | expect(sessionStorageService.get('customKeyVariable')).toEqual(['values', 'some']); 132 | expect(cookiesStorageService.get('customObject')).toEqual({newProperty: true, anotherProperty}); 133 | expect(sharedStorageService.get('arrayOfObjects')).toEqual([{property: true}]); 134 | expect(localStorageService.get('arrayOfObjects')).toEqual([{property: true}]); 135 | }); 136 | 137 | it('changes in services should be reflected in decorators', () => { 138 | localStorageService.set('localStorageVariable', '44'); 139 | sessionStorageService.set('sessionStorageVariable', 44); 140 | cookiesStorageService.remove('cookieStorageVariable'); 141 | sharedStorageService.set('sharedStorageVariable', {a: 4}); 142 | sharedStorageService.set('twoDecorators', 44); 143 | sessionStorageService.set('customKeyVariable', ['']); 144 | cookiesStorageService.update('customObject', {anotherProperty: []}); 145 | sharedStorageService.set('arrayOfObjects', []); 146 | expect(testClass.localStorageVariable).toBe('44'); 147 | expect(testClass.sessionStorageVariable).toBe(44); 148 | expect(testClass.cookieStorageVariable).toBeUndefined(); 149 | expect(testClass.sharedStorageVariable).toEqual({a: 4}); 150 | expect(testClass.twoDecorators).toBe(44); 151 | expect(testClass.customKey).toEqual(['']); 152 | expect(testClass.customCookie).toEqual({newProperty: true, anotherProperty: []} as any); 153 | expect(testClass.arrayOfObjects).toEqual([]); 154 | localStorageService.set('arrayOfObjects', [{a: 1}]); 155 | expect(testClass.arrayOfObjects).toEqual([{a: 1}]); 156 | }); 157 | 158 | it('values should be initially set in another class', () => { 159 | testClass.localStorageVariable = 'string'; 160 | testClass.sessionStorageVariable = 2018; 161 | testClass.cookieStorageVariable = true; 162 | testClass.sharedStorageVariable = {a: 4}; 163 | testClass2 = new TestClass2(); 164 | expect(testClass2.localStorageVariable).toBe('string'); 165 | expect(testClass2.sessionStorageVariable).toBe(2018); 166 | expect(testClass2.cookieStorageVariable).toBe(true); 167 | expect(testClass2.sharedStorageVariable).toEqual({a: 4}); 168 | }); 169 | 170 | it('changes from one class should be visible in the other one', () => { 171 | testClass2 = new TestClass2(); 172 | testClass2.localStorageVariable = 'localStorage'; 173 | testClass2.sessionStorageVariable = 911; 174 | testClass2.cookieStorageVariable = false; 175 | testClass2.sharedStorageVariable = {b: 5}; 176 | expect(testClass.localStorageVariable).toBe('localStorage'); 177 | expect(testClass.sessionStorageVariable).toBe(911); 178 | expect(testClass.cookieStorageVariable).toBe(false); 179 | expect(testClass.sharedStorageVariable).toEqual({b: 5}); 180 | }); 181 | 182 | it('migration should work properly', () => { 183 | class MigrationTest implements OnDestroy { 184 | @LocalStorage({migrateKey: 'oldKey', key: 'newKey'}) migration = 'migration'; 185 | public ngOnDestroy(): void {} 186 | } 187 | const migrationTest = new MigrationTest(); 188 | expect(migrationTest.migration).toBe('migration'); 189 | expect(localStorageService.get('oldKey')).toBe(null); 190 | expect(localStorageService.get('newKey')).toBe('migration'); 191 | migrationTest.ngOnDestroy(); 192 | localStorageService.set('oldKey', 'migrated'); 193 | class MigratedTest { 194 | @LocalStorage({migrateKey: 'oldKey', key: 'newKey'}) migration = 'migration'; 195 | } 196 | expect(new MigratedTest().migration).toBe('migrated'); 197 | expect(localStorageService.get('oldKey')).toBe(null); 198 | expect(localStorageService.get('newKey')).toBe('migrated'); 199 | }); 200 | 201 | afterAll(() => { 202 | localStorageService.clear('all'); 203 | sessionStorageService.clear('all'); 204 | cookiesStorageService.clear('all'); 205 | sharedStorageService.clear('all'); 206 | }); 207 | 208 | }); 209 | -------------------------------------------------------------------------------- /src/lib/decorator/webstorage.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:only-arrow-functions 2 | import { LocalStorageService } from '../service/local-storage.service'; 3 | import { SessionStorageService } from '../service/session-storage.service'; 4 | import { CookiesStorageService } from '../service/cookies-storage.service'; 5 | import { WebStorageServiceInterface } from '../service/webstorage.interface'; 6 | import { cookiesStorageUtility, localStorageUtility, sessionStorageUtility, sharedStorageUtility } from '../utility'; 7 | import { SharedStorageService } from '../service/shared-storage.service'; 8 | import { WebStorageUtility } from '../utility/webstorage.utility'; 9 | import { Cache } from './cache'; 10 | import { 11 | CookieStorageDecoratorConfig, 12 | DecoratorConfig, 13 | LocalStorageDecoratorConfig, 14 | SessionStorageDecoratorConfig, 15 | WebStorageDecoratorConfig, 16 | } from '../ngx-store.types'; 17 | 18 | export type DecoratorReturn = (target: any, propertyName: string) => void; 19 | 20 | export function LocalStorage(keyOrConfig?: string | LocalStorageDecoratorConfig, 21 | config?: LocalStorageDecoratorConfig): DecoratorReturn { 22 | return WebStorage(localStorageUtility, LocalStorageService, keyOrConfig!, config); 23 | } 24 | 25 | export function SessionStorage(keyOrConfig?: string | SessionStorageDecoratorConfig, 26 | config?: SessionStorageDecoratorConfig): DecoratorReturn { 27 | return WebStorage(sessionStorageUtility, SessionStorageService, keyOrConfig!, config); 28 | } 29 | 30 | export function CookieStorage(keyOrConfig?: string | CookieStorageDecoratorConfig, 31 | config?: CookieStorageDecoratorConfig): DecoratorReturn { 32 | return WebStorage(cookiesStorageUtility, CookiesStorageService, keyOrConfig!, config); 33 | } 34 | 35 | export function SharedStorage(keyOrConfig?: string | WebStorageDecoratorConfig, 36 | config?: WebStorageDecoratorConfig): DecoratorReturn { 37 | return WebStorage(sharedStorageUtility, SharedStorageService, keyOrConfig!, config); 38 | } 39 | 40 | function WebStorage( 41 | webStorageUtility: WebStorageUtility, 42 | service: WebStorageServiceInterface, 43 | keyOrConfig: string | DecoratorConfig, 44 | config: DecoratorConfig = {}, 45 | ): DecoratorReturn { 46 | return (target: any, propertyName: string): void => { 47 | let key: string = ''; 48 | if (typeof keyOrConfig === 'object') { 49 | key = keyOrConfig.key || ''; 50 | config = keyOrConfig; 51 | } else if (typeof keyOrConfig === 'string') { 52 | key = keyOrConfig; 53 | } 54 | key = key || config.key || propertyName; 55 | 56 | let cacheItem = Cache.getCacheFor({ 57 | key, 58 | name: propertyName, 59 | targets: [target], 60 | services: [service], 61 | utilities: [{ 62 | utility: webStorageUtility, 63 | config, 64 | }], 65 | }); 66 | 67 | Object.defineProperty(target, propertyName, { 68 | get: function(): any { // tslint:disable-line 69 | return cacheItem.getProxy(undefined, config); 70 | }, 71 | set: function(value: any): void { // tslint:disable-line 72 | if (!Cache.get(cacheItem.key)) { 73 | cacheItem = Cache.getCacheFor(cacheItem); 74 | } 75 | cacheItem.addTargets([target]); 76 | cacheItem.currentTarget = target; 77 | cacheItem.saveValue(value, config); 78 | }, 79 | configurable: true, 80 | }); 81 | return target; 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/ngx-store.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { LocalStorageService } from './service/local-storage.service'; 3 | import { SessionStorageService } from './service/session-storage.service'; 4 | import { CookiesStorageService } from './service/cookies-storage.service'; 5 | import { SharedStorageService } from './service/shared-storage.service'; 6 | 7 | @NgModule({ 8 | declarations: [], 9 | imports: [], 10 | exports: [], 11 | providers: [ 12 | LocalStorageService, 13 | SessionStorageService, 14 | CookiesStorageService, 15 | SharedStorageService, 16 | ], 17 | }) 18 | export class NgxStoreModule { 19 | // methods for future use 20 | public static forRoot(): ModuleWithProviders { 21 | return { 22 | ngModule: NgxStoreModule, 23 | }; 24 | } 25 | 26 | public static forChild(): typeof NgxStoreModule { 27 | return NgxStoreModule; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/ngx-store.types.ts: -------------------------------------------------------------------------------- 1 | export interface Webstorable { 2 | save(): void; 3 | } 4 | 5 | export type WebstorableObject = Webstorable & { [prop: string]: any }; 6 | export type WebstorableArray = Webstorable & Array; 7 | 8 | // TODO create config interface for service methods 9 | export interface WebStorageDecoratorConfig { 10 | key?: string; 11 | mutate?: boolean; 12 | } 13 | 14 | export interface StorageDecoratorConfig extends WebStorageDecoratorConfig { 15 | prefix?: string; 16 | } 17 | 18 | export interface SessionStorageDecoratorConfig extends StorageDecoratorConfig { 19 | } 20 | 21 | export interface LocalStorageDecoratorConfig extends StorageDecoratorConfig { 22 | migrateKey?: string; 23 | } 24 | 25 | export interface CookieStorageDecoratorConfig extends LocalStorageDecoratorConfig { 26 | expires?: Date; 27 | } 28 | 29 | export type DecoratorConfig = CookieStorageDecoratorConfig; // should contain all fields 30 | -------------------------------------------------------------------------------- /src/lib/service/README.md: -------------------------------------------------------------------------------- 1 | # Angular Storage 2 | ## Injectable Services 3 | Angular-Injectable services included in this library: 4 | - `LocalStorageService` - for managing of localStorage 5 | - `SessionStorageService` - for managing of sessionStorage 6 | - `CookiesStorageService` - for managing of cookies 7 | - `SharedStorageService` - for managing of shared variables (stored in usual browser's temporary memory) 8 | 9 | All of them provide common methods: 10 | + `get(key: string): any`: returns JSON-parsed data 11 | + `load(key: string): Resource`: returns an instance of [`Resource`](https://github.com/zoomsphere/ngx-store/src/service/resource.ts) class for given key exposing API based on builder design pattern (see [#Builder pattern](https://github.com/zoomsphere/ngx-store/tree/master/src/service#builder-pattern) section below) 12 | + `set(key: string, value: T): T`: sets data in Storage 13 | + `update(key: string, changes: any): any`: updates object stored under `key` by deep merge, throws an error if stored value exists and is not an object 14 | + `remove(key: string): void`: removes variable with given key 15 | + `clear(): void`: clears Storage in mode provided by `Config.clearType` (`'prefix'` by default) 16 | + `clear('all'): void`: clears whole Storage 17 | + `clear('prefix', prefix?: string): void`: clears Storage keys starting by passed `prefix` (or `Config.prefix` if not provided) 18 | + `clear('decorators', target?: Object): void`: clears Storage keys created by decorators, all or only from given target class 19 | + `observe(key?: string, exactMatch?: boolean): Observable`: returns an observable emitting [`NgxStorageEvent`](https://github.com/zoomsphere/ngx-store/blob/master/src/utility/storage/storage-event.ts#L1)s (see [#Listening for changes](https://github.com/zoomsphere/ngx-store/tree/master/src/service#listening-for-changes) section below) 20 | + `config: WebStorageConfigInterface`: getter for module config (read only) 21 | + `keys: Array`: keys of values stored by `ngx-store` (determined by prefix and decorators) 22 | + `utility: WebStorageUtility`: access to [`WebStorageUtility`](https://github.com/zoomsphere/ngx-store/src/utility/webstorage-utility.ts) class for advanced usage 23 | 24 | ## Builder pattern 25 | `WebStorage.load(key: string)` method exposes API based on builder design pattern. Following methods are allowed in a chain: 26 | + `setPath(path: string)` - sets path of object property, e.g. if we have `{ nested: { property: true }}` under "object" key in localStorage, then `localStorageService.load('object').path('nested.property').value` will be equal to `true` 27 | + `appendPath(path: string)` - appends current path, e.g. if path('key') and appendPath('nested'), the path will be "key.nested" 28 | + `truncatePath()` - removes last item of path, e.g. if path('key.nested') and truncatePath(), the path will be "key" 29 | + `resetPath()` - resets set path 30 | + `setPrefix(prefix: string)` - sets prefix, e.g. `localStorageService.load('key').prefix('abc_').value` will read value of item stored under "abc_key" key in LS 31 | + `changePrefix(prefix: string)` - moves storage item to new key using given prefix 32 | + `setDefaultValue(value: any)` - sets default value for both reading and saving, will be used in case when real value is `null` or `undefined` 33 | + `save(value: any)` - saves given value in chosen place - as a new entry or an existing object property depending on `path` 34 | + `update(value: any)` - updates object stored under current path using `lodash.merge` 35 | + `remove()` - removes item stored under current key 36 | + there are also "non-chainable" getters for: `value`, `defaultValue`, `path` and `prefix` 37 | 38 | See [`Resource`](https://github.com/zoomsphere/ngx-store/src/service/resource.ts) class for details. 39 | 40 | #### Code example: 41 | ```typescript 42 | this.localStorageService.set('object', { nested: { property: false }}); 43 | let objectResource = this.localStorageService.load('object'); 44 | console.log(objectResource.path('nested.property').value); // false 45 | console.log(objectResource.path('nested.property').save(true).value); // true 46 | console.log(objectResource.path('nested.new_key').defaultValue(8).save(null).value); // 8 47 | console.log(objectResource.path('nested').value); // { property: true, new_key: 8 } 48 | console.log(objectResource.update({new_key: true})); // { nested: { property: false }, new_key: true } 49 | ``` 50 | Real-life usage in a class: 51 | ```typescript 52 | import { LocalStorageService, NgxResource } from 'ngx-store'; 53 | 54 | interface ModuleSettings { 55 | viewType?: string; 56 | notificationsCount: number; 57 | displayName: string; 58 | } 59 | 60 | class ModuleService { 61 | constructor(public localStorageService: LocalStorageService) {} 62 | 63 | public get settings(): NgxResource { 64 | return this.localStorageService 65 | .load(`userSettings`) 66 | .setPath(`modules`) 67 | .setDefaultValue({}) // we have to set {} as default value, because numeric `moduleId` would create an array 68 | .appendPath(this.moduleId) 69 | .setDefaultValue({}); 70 | } 71 | 72 | public saveModuleSettings(settings: ModuleSettings) { 73 | this.settings.save(settings); 74 | } 75 | 76 | public updateModuleSettings(settings: Partial) { 77 | this.settings.update(settings); 78 | } 79 | } 80 | ``` 81 | 82 | ## Listening to changes 83 | `WebstorageService.observe()` method allows to watch storage changes and can take up to 2 parameters: 84 | ```typescript 85 | public observe(key?: string, exactMatch?: boolean): Observable; 86 | ``` 87 | `key` specifies filter pattern for `event.key`, by default it's enough to just contain it, but we can easily change the behaviour by passing `exactMatch = true` - in this case prefix is automatically added to the passed key if not included. Returned value is an RxJS Observable of `NgxStorageEvent`, which is just a wrapper of native [`StorageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/StorageEvent) with added `isInternal = true` property for changes made by `ngx-store`, so we can filter out e.g. localStorage events from other tab by this code: 88 | ```typescript 89 | this.localStorageService.observe() 90 | .filter(event => !event.isInternal) 91 | .subscribe((event) => { 92 | // events here are equal like would be in: 93 | // window.addEventListener('storage', (event) => {}); 94 | // excluding sessionStorage events 95 | // and event.type will be set to 'localStorage' (instead of 'storage') 96 | }); 97 | ``` 98 | In order to listen for changes in cookies constantly (i.e. if they can get changed from server side or external library), we have to specify `Config.cookiesCheckInterval`. It's recommended setting it to 1000 ms as it will be fast enough in most of cases and doesn't cause noticeably CPU usage. 99 | -------------------------------------------------------------------------------- /src/lib/service/cookies-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { cookiesStorage } from '../utility/storage/cookies-storage'; 2 | import { WebStorageService } from './webstorage.service'; 3 | import { cookiesStorageUtility } from '../utility'; 4 | import { merge } from 'rxjs'; 5 | import { Injectable } from '@angular/core'; 6 | 7 | @Injectable() 8 | export class CookiesStorageService extends WebStorageService { 9 | public static keys: Array = []; 10 | 11 | constructor() { 12 | super(cookiesStorageUtility); 13 | this._changes = !cookiesStorage.externalChanges ? cookiesStorageUtility.changes 14 | : merge(cookiesStorage.externalChanges?.asObservable(), cookiesStorageUtility.changes); 15 | } 16 | 17 | public set(key: string, value: T, expirationDate?: Date): T { 18 | return this.utility.set(key, value, {expires: expirationDate}) as T; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/service/local-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageService } from './webstorage.service'; 2 | import { localStorageUtility } from '../utility/index'; 3 | import { Injectable } from '@angular/core'; 4 | import { fromEvent, merge } from 'rxjs'; 5 | import { filter, map } from 'rxjs/operators'; 6 | import { NgxStorageEvent } from '../utility/storage/storage-event'; 7 | 8 | @Injectable() 9 | export class LocalStorageService extends WebStorageService { 10 | public static keys: Array = []; 11 | 12 | constructor() { 13 | super(localStorageUtility); 14 | this._changes = 15 | merge(fromEvent(window, 'storage') 16 | .pipe( 17 | filter((event: NgxStorageEvent) => event.storageArea === localStorage), 18 | map((event: NgxStorageEvent) => this.mapNativeEvent(event)), 19 | ), 20 | localStorageUtility.changes); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/service/resource.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageService } from './webstorage.service'; 2 | import { Config } from '../config/config'; 3 | 4 | const _get = require('lodash.get'); 5 | const _set = require('lodash.set'); 6 | const _merge = require('lodash.merge'); 7 | 8 | export class Resource { 9 | constructor(protected service: WebStorageService, protected readonly key: string) { 10 | } 11 | 12 | protected _defaultValue: any = null; 13 | 14 | /** 15 | * Returns default value 16 | * @returns {T} 17 | */ 18 | public get defaultValue(): T { 19 | return this._defaultValue; 20 | } 21 | 22 | protected _path: Array = []; 23 | 24 | /** 25 | * Returns current path as a string 26 | * @returns {string} 27 | */ 28 | public get path(): string { 29 | return this.pathString; 30 | } 31 | 32 | protected _prefix = Config.prefix; 33 | 34 | /** 35 | * Returns currently set prefix 36 | * @returns {string} 37 | */ 38 | public get prefix(): string { 39 | return this._prefix || ''; 40 | } 41 | 42 | /** 43 | * Returns value taking path into account 44 | * @returns {T} 45 | */ 46 | public get value(): T { 47 | return this.considerDefault(this.readValue()); 48 | } 49 | 50 | protected get fullValue(): T { 51 | return this.considerDefault(this.service.utility.get(this.key, {prefix: this._prefix})); 52 | } 53 | 54 | protected get pathString(): string { 55 | return this._path.join('.'); 56 | } 57 | 58 | /** 59 | * Sets path of object property 60 | * @param {string} path 61 | * @returns {this} 62 | */ 63 | public setPath(path: string): this { 64 | this._path = path.split('.'); 65 | return this; 66 | } 67 | 68 | /** 69 | * Appends current path 70 | * e.g. if path('key') and appendPath('nested'), the path will be "key.nested" 71 | * @param {string} path 72 | * @returns {this} 73 | */ 74 | public appendPath(path: string): this { 75 | this._path.push(path); 76 | return this; 77 | } 78 | 79 | /** 80 | * Removes last item of path 81 | * e.g. if path('key.nested') and truncatePath(), the path will be "key" 82 | * @returns {this} 83 | */ 84 | public truncatePath(): this { 85 | this._path.pop(); 86 | return this; 87 | } 88 | 89 | /** 90 | * Resets set path 91 | * @returns {this} 92 | */ 93 | public resetPath(): this { 94 | this._path = []; 95 | return this; 96 | } 97 | 98 | /** 99 | * Sets prefix 100 | * @param {string} prefix 101 | * @returns {this} 102 | */ 103 | public setPrefix(prefix: string): this { 104 | this._prefix = prefix; 105 | return this; 106 | } 107 | 108 | /** 109 | * Moves storage item to new key using given prefix 110 | * @param {string} prefix 111 | * @returns {this} 112 | */ 113 | public changePrefix(prefix: string): this { 114 | this.service.utility.set(this.key, this.fullValue, {prefix}); 115 | this.service.utility.remove(this.key, {prefix: this._prefix}); 116 | return this.setPrefix(prefix); 117 | } 118 | 119 | /** 120 | * Sets default value for both reading and saving operations 121 | * @param defaultValue 122 | * @returns {this} 123 | */ 124 | public setDefaultValue(defaultValue: T): this { 125 | this._defaultValue = defaultValue; 126 | const value = this.readValue(); 127 | if (this.isNullOrUndefined(value)) { 128 | this.save(defaultValue); 129 | } 130 | return this; 131 | } 132 | 133 | /** 134 | * Creates or overrides value as a new entry or existing object property depending on path 135 | * @param value 136 | * @returns {this} 137 | */ 138 | public save(value: T): this { 139 | if (this.pathString) { 140 | value = _set(this.fullValue as any as object, this.pathString, this.considerDefault(value)) as any as T; 141 | } 142 | this.service.utility.set(this.key, this.considerDefault(value), {prefix: this._prefix}); 143 | return this; 144 | } 145 | 146 | /** 147 | * Updates existing object property using current path 148 | * @param {T} value 149 | * @returns {this} 150 | */ 151 | public update(value: T): this { 152 | return this.save(_merge(this.readValue(), value)); 153 | } 154 | 155 | /** 156 | * Removes item stored under current key 157 | * @returns {this} 158 | */ 159 | public remove(): this { 160 | this.service.utility.remove(this.key); 161 | return this; 162 | } 163 | 164 | protected considerDefault(value: S): S { 165 | return this.isNullOrUndefined(value) ? this._defaultValue : value; 166 | } 167 | 168 | protected isNullOrUndefined(value: any): boolean { 169 | return (value === null || value === undefined); 170 | } 171 | 172 | protected readValue(): T { 173 | const value = this.service.utility.get(this.key, {prefix: this._prefix}); 174 | if (this.pathString) { 175 | return _get(value, this.pathString); 176 | } 177 | return value; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/lib/service/session-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageService } from './webstorage.service'; 2 | import { sessionStorageUtility } from '../utility/index'; 3 | import { Injectable } from '@angular/core'; 4 | import { fromEvent, merge } from 'rxjs'; 5 | import { filter, map } from 'rxjs/operators'; 6 | import { NgxStorageEvent } from '../utility/storage/storage-event'; 7 | 8 | @Injectable() 9 | export class SessionStorageService extends WebStorageService { 10 | public static keys: Array = []; 11 | 12 | constructor() { 13 | super(sessionStorageUtility); 14 | this._changes = 15 | merge(fromEvent(window, 'storage') 16 | .pipe( 17 | filter((event: NgxStorageEvent) => event.storageArea === sessionStorage), 18 | map((event: NgxStorageEvent) => this.mapNativeEvent(event)), 19 | ), 20 | sessionStorageUtility.changes); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/service/shared-storage.service.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageService } from './webstorage.service'; 2 | import { sharedStorageUtility } from '../utility'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | @Injectable() 6 | export class SharedStorageService extends WebStorageService { 7 | public static keys: Array = []; 8 | 9 | constructor() { 10 | super(sharedStorageUtility); 11 | this._changes = sharedStorageUtility.changes; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/service/web-storage.mock.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageService } from './webstorage.service'; 2 | 3 | export const entries: { [key: string]: any } = { 4 | true: true, 5 | false: false, 6 | number: 42, 7 | string: '43', 8 | object: { 9 | property: 0, 10 | nested: { 11 | property: null, 12 | }, 13 | }, 14 | array: [7, 6, 5, 4], 15 | }; 16 | 17 | export const fillWithData = (service: WebStorageService) => { 18 | Object.entries(entries).forEach(([key, value]) => service.set(key, value)); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/service/web-storage.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { entries, fillWithData } from './web-storage.mock'; 3 | import { LocalStorageService } from './local-storage.service'; 4 | import { SessionStorageService } from './session-storage.service'; 5 | import { CookiesStorageService } from './cookies-storage.service'; 6 | import { SharedStorageService } from './shared-storage.service'; 7 | import { WebStorageService } from './webstorage.service'; 8 | 9 | // Test common methods of WebStorageService children classes 10 | test(LocalStorageService); 11 | test(SessionStorageService); 12 | test(CookiesStorageService); 13 | test(SharedStorageService); 14 | 15 | function test(storageService: typeof WebStorageService): void { 16 | describe(storageService.name, () => { 17 | let service: WebStorageService; 18 | 19 | beforeEach(() => { 20 | TestBed.configureTestingModule({ 21 | providers: [storageService], 22 | }); 23 | }); 24 | 25 | beforeEach(inject([storageService], (serviceInjection: typeof storageService) => { 26 | service = (serviceInjection as any as WebStorageService); 27 | fillWithData(service); 28 | })); 29 | 30 | describe('should set and save value', () => { 31 | function testSave(key: string): void { 32 | it(key, () => { 33 | expect(service.get(key)).toEqual(entries[key]); 34 | expect(service.utility.get(key)).toEqual(entries[key]); 35 | }); 36 | } 37 | 38 | Object.entries(entries).forEach(([key, value]) => { 39 | testSave(key); 40 | }); 41 | }); 42 | 43 | describe('#update() method', () => { 44 | it('should merge changes with saved object', () => { 45 | // debugger; 46 | service.update('object', {property: 8}); 47 | expect(service.get('object')).toEqual({ 48 | property: 8, 49 | nested: {property: null}, 50 | }, 'non-nested change'); 51 | service.update('object', {nested: {property: true}}); 52 | expect(service.get('object')).toEqual({ 53 | property: 8, 54 | nested: {property: true}, 55 | }, 'nested change'); 56 | }); 57 | 58 | it('should create new object if it does not exists', () => { 59 | service.update('newObject', {a: 1}); 60 | expect(service.get('newObject')).toEqual({a: 1}); 61 | }); 62 | 63 | it('should throw an error when trying to update non-object', () => { 64 | Object.entries(entries).forEach(([value, key]) => { 65 | if (typeof value === 'object') { 66 | return; 67 | } 68 | expect(service.update.bind(null, key.toString(), {b: 2})).toThrowError(); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('load() builder pattern', () => { 74 | it('chain should read proper value', () => { 75 | expect(service.load('object').setPath('nested.property').value).toBe(null); 76 | expect(service.load('non-existing').setDefaultValue('default').value).toBe('default'); 77 | expect(service.load('object2') 78 | .setPath('non-existing') 79 | .setDefaultValue('default') 80 | .value).toBe('default'); 81 | }); 82 | it('chain should save value', () => { 83 | expect(service.load('new_key').save('==').value).toBe('=='); 84 | expect(service.load('new_key').setDefaultValue(8).save(undefined).value).toBe(8); 85 | expect(service.load('object').setPath('nested.new_key').save(123).value).toBe(123); 86 | const expectedObject = { 87 | property: 0, 88 | nested: { 89 | property: null, 90 | new_key: 123, 91 | }, 92 | }; 93 | expect(service.get('object')).toEqual(expectedObject); 94 | expectedObject.nested.new_key = 125; 95 | expect(service.load('object') 96 | .setPath('nested') 97 | .update({new_key: 125}) 98 | .value).toEqual(expectedObject.nested, 'update'); 99 | }); 100 | it('set and get values should be equal', () => { 101 | const resource = service.load('object').setDefaultValue(1).setPrefix('pre').setPath('nested'); 102 | expect(resource.defaultValue).toBe(1); 103 | expect(resource.prefix).toBe('pre'); 104 | expect(resource.path).toBe('nested'); 105 | expect(resource.appendPath('new_key').path).toBe('nested.new_key'); 106 | expect(resource.truncatePath().path).toBe('nested'); 107 | expect(resource.resetPath().path).toBe(''); 108 | }); 109 | it('should change prefix', () => { 110 | const resource = service.load('prefix_change').save(true).changePrefix('pre_'); 111 | expect(resource.value).toBe(true); 112 | expect(resource.prefix).toBe('pre_'); 113 | expect(service.get('prefix_change')).toMatch(/null|undefined/); 114 | expect(service.utility.get('prefix_change', {prefix: 'pre_'})).toBe(true); 115 | }); 116 | }); 117 | 118 | describe('should delete specified data', () => { 119 | const expectation = /null|undefined/; 120 | it('from Resource', () => { 121 | expect(service.load('something_unique').save(true).remove().value).toMatch(expectation); 122 | }); 123 | it('by key', () => { 124 | Object.entries(entries).forEach(([key, value]) => { 125 | service.remove(key); 126 | expect(service.get(key)).toMatch(expectation); 127 | }); 128 | }); 129 | 130 | it('by prefix', () => { 131 | service.clear('prefix'); 132 | Object.entries(entries).forEach(([key, value]) => { 133 | expect(service.get(key)).toMatch(expectation); 134 | }); 135 | }); 136 | 137 | it('all', () => { 138 | let expectedCount: RegExp; 139 | switch (service.constructor) { 140 | case LocalStorageService: 141 | // NGX-STORE_prefix will remain 142 | expectedCount = /1/; 143 | break; 144 | case CookiesStorageService: 145 | // HTTP-only cookies cannot be removed by JS 146 | // so any entries count is expected 147 | expectedCount = /[0-9]+/; 148 | break; 149 | default: 150 | expectedCount = /0/; 151 | } 152 | service.clear('all'); 153 | // tslint:disable-next-line 154 | expect(service.utility['_storage'].length).toMatch(expectedCount); 155 | }); 156 | }); 157 | afterAll(() => { 158 | service.clear('all'); 159 | }); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /src/lib/service/webstorage.interface.ts: -------------------------------------------------------------------------------- 1 | import { ClearType, WebStorageConfigInterface } from '../config/config.interface'; 2 | 3 | export interface WebStorageServiceInterface { 4 | keys: Array; 5 | 6 | new(): { 7 | keys: Array; 8 | config: WebStorageConfigInterface; 9 | get(key: string): any; 10 | set(key: string, value: any): void; 11 | remove(key: string): void; 12 | clear(clearType?: ClearType, secondParam?: any): void; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/service/webstorage.service.ts: -------------------------------------------------------------------------------- 1 | import { Config, debug } from '../config/config'; 2 | import { ClearType, WebStorageConfigInterface } from '../config/config.interface'; 3 | import { WebStorageUtility } from '../utility/webstorage.utility'; 4 | import { WebStorageServiceInterface } from './webstorage.interface'; 5 | import { Cache } from '../decorator/cache'; 6 | import { Observable } from 'rxjs'; 7 | import { delay, filter } from 'rxjs/operators'; 8 | import { NgxStorageEvent } from '../utility/storage/storage-event'; 9 | import { Resource } from './resource'; 10 | import merge from 'lodash.merge'; 11 | 12 | // const merge = require('lodash.merge'); 13 | 14 | export abstract class WebStorageService { 15 | public static keys: Array = []; 16 | // @ts-ignore 17 | protected _changes: Observable; 18 | 19 | protected constructor(public utility: WebStorageUtility) { 20 | } 21 | 22 | /** 23 | * Gets keys for stored variables created by ngx-store, 24 | * ignores keys that have not been created by decorators and have no prefix at once 25 | */ 26 | public get keys(): Array { 27 | // get prefixed key if prefix is defined 28 | const prefixKeys = this.utility.keys.filter(key => { 29 | if (this.utility.prefix && this.utility.prefix.length) { 30 | return key.startsWith(this.utility.prefix); 31 | } 32 | return true; 33 | }); 34 | const decoratorKeys = (this.constructor as WebStorageServiceInterface).keys; 35 | return prefixKeys.concat(decoratorKeys); 36 | } 37 | 38 | public get config(): WebStorageConfigInterface { 39 | return Config; 40 | } 41 | 42 | public get(key: string): any { 43 | return this.utility.get(key); 44 | } 45 | 46 | /** 47 | * Returns new data Resource for given key exposing builder design pattern 48 | * designed for complex nested data structures 49 | */ 50 | public load(key: string): Resource { 51 | return new Resource(this, key); 52 | } 53 | 54 | public set(key: string, value: T): T { 55 | return this.utility.set(key, value) as T; 56 | } 57 | 58 | public update(key: string, changes: any): any { 59 | const value = this.get(key); 60 | if (value !== undefined && typeof value !== 'object') { 61 | debug.throw(new Error(`Value stored under "${key}" key is not an object and tried to be updated.`)); 62 | return value; 63 | } 64 | return this.set(key, merge({}, value, changes)); 65 | } 66 | 67 | // TODO return true if item existed and false otherwise (?) 68 | public remove(key: string): void { 69 | return this.utility.remove(key); 70 | } 71 | 72 | public observe(key?: string, exactMatch?: boolean): Observable { 73 | return this._changes.pipe( 74 | filter((event: NgxStorageEvent) => { 75 | if (!key) { 76 | return true; 77 | } 78 | if (exactMatch) { 79 | if (Config.prefix && key.startsWith(Config.prefix)) { 80 | return event.key === key; 81 | } 82 | return event.key === Config.prefix + key; 83 | } else { 84 | return event.key?.indexOf(key) !== -1; 85 | } 86 | }), 87 | delay(30), // event should come after actual data change and propagation 88 | ); 89 | } 90 | 91 | /** 92 | * Clears chosen data from Storage 93 | * @param clearType 'prefix' | 'decorators' | 'all' 94 | * @param prefixOrClass defines the prefix or class (not its instance) whose decorators should be cleared 95 | */ 96 | public clear(clearType?: ClearType, prefixOrClass?: string | object): void { 97 | clearType = clearType || Config.clearType; 98 | if (clearType === 'decorators') { 99 | let keys = []; 100 | if (typeof prefixOrClass === 'object') { 101 | keys = this.keys.filter(key => Cache.get(key).targets.includes(prefixOrClass as object)); 102 | debug.log(this.utility.getStorageName() + ' > Removing decorated data from ' 103 | + prefixOrClass.constructor.name + ':', keys); 104 | } else { 105 | keys = this.keys; 106 | debug.log(this.utility.getStorageName() + ' > Removing decorated data:', keys); 107 | } 108 | keys.forEach(key => this.remove(key)); 109 | } else if (clearType === 'prefix') { 110 | prefixOrClass = prefixOrClass || this.utility.prefix; 111 | this.utility.forEach((value, key) => { 112 | if (key.startsWith(prefixOrClass as string)) { 113 | this.remove(this.utility.trimPrefix(key)); 114 | } 115 | }); 116 | } else if (clearType === 'all') { 117 | this.utility.clear(); 118 | } 119 | } 120 | 121 | protected generateEvent(key: string, newValue: any, oldValue?: any): NgxStorageEvent { 122 | const type = this.utility.getStorageName().charAt(0).toLowerCase() + this.utility.getStorageName().slice(1); 123 | const event = new NgxStorageEvent(type, key, this.utility.getStorage()); 124 | event.oldValue = (oldValue !== undefined) ? oldValue : this.get(key); 125 | event.newValue = newValue; 126 | return event; 127 | } 128 | 129 | protected mapNativeEvent(ev: StorageEvent): NgxStorageEvent { 130 | const event = this.generateEvent( 131 | ev.key as string, 132 | this.utility.getGettable(ev.newValue as string), 133 | this.utility.getGettable(ev.oldValue as string)); 134 | event.isInternal = false; 135 | return event; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/lib/tools.ts: -------------------------------------------------------------------------------- 1 | export const findDescriptor = (proto: any, key: string): PropertyDescriptor | undefined => { 2 | if (!proto) { 3 | return undefined; 4 | } 5 | const descriptor = Object.getOwnPropertyDescriptor(proto, key); 6 | if (descriptor) { 7 | return descriptor; 8 | } else { 9 | return findDescriptor(Object.getPrototypeOf(proto), key); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/utility/index.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '../config/config'; 2 | import { WebStorageUtility } from './webstorage.utility'; 3 | import { cookiesStorage } from './storage/cookies-storage'; 4 | import { SharedStorageUtility } from './shared-storage.utility'; 5 | import { sharedStorage } from './storage/shared-storage'; 6 | 7 | export const localStorageUtility: WebStorageUtility = 8 | new WebStorageUtility(localStorage, Config.prefix, Config.previousPrefix); 9 | export const sessionStorageUtility: WebStorageUtility = 10 | new WebStorageUtility(sessionStorage, Config.prefix, Config.previousPrefix); 11 | export const cookiesStorageUtility: WebStorageUtility = 12 | new WebStorageUtility(cookiesStorage, Config.prefix, Config.previousPrefix); 13 | export const sharedStorageUtility: SharedStorageUtility = 14 | new SharedStorageUtility(sharedStorage, Config.prefix, Config.prefix); 15 | -------------------------------------------------------------------------------- /src/lib/utility/shared-storage.utility.ts: -------------------------------------------------------------------------------- 1 | import { WebStorageUtility } from './webstorage.utility'; 2 | 3 | export class SharedStorageUtility extends WebStorageUtility { 4 | public getSettable(value: any): any { 5 | return value; 6 | } 7 | 8 | public getGettable(value: any): any { 9 | return value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/utility/storage/cookies-storage.ts: -------------------------------------------------------------------------------- 1 | import { Config, debug } from '../../config/config'; 2 | import { NgxStorage } from './storage'; 3 | import { StorageName, WebStorageUtility } from '../webstorage.utility'; 4 | import { interval } from 'rxjs'; 5 | 6 | export interface WebStorage extends Storage { 7 | setItem(key: string, data: string, expirationDate?: Date): void; 8 | } 9 | 10 | export class CookiesStorage extends NgxStorage { 11 | protected cachedCookieString: string = ''; 12 | protected cachedItemsMap: Map = new Map(); 13 | 14 | constructor() { 15 | super(); 16 | this.getAllItems(); 17 | if (Config.cookiesCheckInterval) { 18 | interval(Config.cookiesCheckInterval) 19 | .subscribe(() => { 20 | if (!this.externalChanges?.observers.length) { 21 | return; // don't run if there are no set subscriptions 22 | } 23 | this.getAllItems(); 24 | }); 25 | } 26 | } 27 | 28 | public get type(): StorageName { 29 | return 'cookiesStorage'; 30 | } 31 | 32 | public get length(): number { 33 | return this.getAllKeys().length; 34 | } 35 | 36 | public key(index: number): string | any { 37 | return this.getAllKeys()[index]; 38 | } 39 | 40 | public getItem(key: string): string | any { 41 | return this.getAllItems().get(key); 42 | } 43 | 44 | public removeItem(key: string): void { 45 | if (typeof document === 'undefined') { 46 | return; 47 | } 48 | let domain = this.resolveDomain(Config.cookiesScope); 49 | domain = (domain) ? 'domain=' + domain + ';' : ''; 50 | document.cookie = key + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;' + domain; 51 | this.cachedItemsMap.delete(key); 52 | } 53 | 54 | /** 55 | * @param key 56 | * @param value 57 | * @param expirationDate passing null affects in lifetime cookie 58 | */ 59 | public setItem(key: string, value: string, expirationDate?: Date): void { 60 | if (typeof document === 'undefined') { 61 | return; 62 | } 63 | let domain = this.resolveDomain(Config.cookiesScope); 64 | debug.log('Cookies domain:', domain); 65 | domain = (domain) ? 'domain=' + domain + ';' : ''; 66 | let utcDate = ''; 67 | if (expirationDate instanceof Date) { 68 | utcDate = expirationDate.toUTCString(); 69 | } else if (expirationDate === null) { 70 | utcDate = 'Fri, 18 Dec 2099 12:00:00 GMT'; 71 | } 72 | const expires = utcDate ? '; expires=' + utcDate : ''; 73 | const cookie = key + '=' + value + expires + ';path=/;' + domain; 74 | debug.log('Cookie`s set instruction:', cookie); 75 | this.cachedItemsMap.set(key, value); 76 | document.cookie = cookie; 77 | } 78 | 79 | public clear(): void { 80 | this.getAllKeys().forEach(key => this.removeItem(key)); 81 | } 82 | 83 | public forEach(callbackFn: (value: string, key: string) => any): void { 84 | return this.getAllItems().forEach((value, key) => callbackFn(value, key)); 85 | } 86 | 87 | protected getAllKeys(): Array { 88 | return Array.from(this.getAllItems().keys()); 89 | } 90 | 91 | // TODO: consider getting cookies from all paths 92 | protected getAllItems(): Map { 93 | if (this.cachedCookieString === document.cookie) { // No changes 94 | return this.cachedItemsMap; 95 | } 96 | const map = new Map(); 97 | if (typeof document === 'undefined') { 98 | return map; 99 | } 100 | const cookies = document.cookie.split(';'); 101 | 102 | // tslint:disable-next-line:prefer-for-of 103 | for (let i = 0; i < cookies.length; i++) { 104 | const cookie = cookies[i].trim(); 105 | const eqPos = cookie.indexOf('='); 106 | const key = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; 107 | const value = eqPos > -1 ? cookie.substr(eqPos + 1, cookie.length) : cookie; 108 | map.set(key, value); 109 | } 110 | // detect changes and emit events 111 | if (this.cachedItemsMap) { 112 | map.forEach((value, key) => { 113 | let cachedValue: any = this.cachedItemsMap.get(key); 114 | cachedValue = (cachedValue !== undefined) ? cachedValue : null; 115 | if (value !== cachedValue) { 116 | this.emitEvent( 117 | key, 118 | WebStorageUtility.getGettable(value), 119 | WebStorageUtility.getGettable(cachedValue), 120 | ); 121 | } 122 | }); 123 | this.cachedItemsMap.forEach((value, key) => { 124 | if (!map.has(key)) { 125 | this.emitEvent(key, null, WebStorageUtility.getGettable(value)); 126 | } 127 | }); 128 | } 129 | this.cachedCookieString = document.cookie; 130 | return this.cachedItemsMap = map; 131 | } 132 | 133 | /** 134 | * domain.com + path="." = .domain.com 135 | * domain.com + path=".sub." = .sub.domain.com 136 | * sub.domain.com + path="sub." = sub.domain.com 137 | * www.sub.domain.com + path="." = .sub.domain.com 138 | * localhost + path=".whatever." = localhost 139 | * @param path 140 | */ 141 | protected resolveDomain(path?: string): string { 142 | if (!path) { 143 | return ''; 144 | } 145 | const hostname = document.domain; 146 | if ((hostname.match(/\./g) || []).length < 1) { 147 | return ''; 148 | } 149 | const www = (path[0] !== '.' && hostname.indexOf('www.') === 0) ? 'www.' : ''; 150 | return www + path + this.getDomain(); 151 | } 152 | 153 | /** 154 | * This function determines base domain by setting cookie at the highest level possible 155 | * @url http://rossscrivener.co.uk/blog/javascript-get-domain-exclude-subdomain 156 | */ 157 | protected getDomain(): string { 158 | let i = 0; 159 | let domain = document.domain; 160 | const domainParts = domain.split('.'); 161 | const s = '_gd' + (new Date()).getTime(); 162 | while (i < (domainParts.length - 1) && document.cookie.indexOf(s + '=' + s) === -1) { 163 | domain = domainParts.slice(-1 - (++i)).join('.'); 164 | document.cookie = s + '=' + s + ';domain=' + domain + ';'; 165 | } 166 | document.cookie = s + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=' + domain + ';'; 167 | return domain; 168 | } 169 | } 170 | 171 | export const cookiesStorage = new CookiesStorage(); 172 | -------------------------------------------------------------------------------- /src/lib/utility/storage/shared-storage.ts: -------------------------------------------------------------------------------- 1 | import { NgxStorage } from './storage'; 2 | import { StorageName } from '../webstorage.utility'; 3 | 4 | export class SharedStorage extends NgxStorage { 5 | protected sharedMap: Map = new Map(); 6 | 7 | constructor() { 8 | super(); 9 | delete this.externalChanges; 10 | } 11 | 12 | public get type(): StorageName { 13 | return 'sharedStorage'; 14 | } 15 | 16 | public get length(): number { 17 | return this.getAllKeys().length; 18 | } 19 | 20 | public key(index: number): string | any { 21 | return this.getAllKeys()[index]; 22 | } 23 | 24 | public getItem(key: string): any { 25 | const value = this.sharedMap.get(key); 26 | return (value !== undefined) ? value : null; 27 | } 28 | 29 | public removeItem(key: string): void { 30 | this.sharedMap.delete(key); 31 | } 32 | 33 | public setItem(key: string, value: any): void { 34 | this.sharedMap.set(key, value); 35 | } 36 | 37 | public clear(): void { 38 | this.sharedMap.clear(); 39 | } 40 | 41 | public forEach(func: (value: string, key: any) => any): void { 42 | return this.sharedMap.forEach((value, key) => func(value, key)); 43 | } 44 | 45 | protected getAllKeys(): Array { 46 | return Array.from(this.sharedMap.keys()); 47 | } 48 | } 49 | 50 | export const sharedStorage = new SharedStorage(); 51 | -------------------------------------------------------------------------------- /src/lib/utility/storage/storage-event.ts: -------------------------------------------------------------------------------- 1 | export class NgxStorageEvent implements Omit { 2 | protected static initTimeStamp = Date.now(); 3 | public oldValue!: T; 4 | public newValue!: T; 5 | public NONE: any; 6 | public timeStamp = (Date.now() - NgxStorageEvent.initTimeStamp); 7 | public readonly bubbles = false; 8 | public readonly cancelBubble = false; 9 | public readonly cancelable = false; 10 | public readonly composed = false; 11 | public readonly currentTarget = window; 12 | public readonly defaultPrevented = false; 13 | public readonly eventPhase = 2; 14 | public readonly isTrusted = true; 15 | public readonly path = [window]; 16 | public readonly returnValue = true; 17 | public readonly srcElement = window as any; 18 | public readonly target = window; 19 | public readonly url = window.location.href; 20 | public isInternal = true; 21 | 22 | constructor(public type: string, public key: string, public storageArea: Storage) { 23 | } 24 | 25 | /** 26 | * Methods below exist only to satisfy TypeScript compiler 27 | */ 28 | // tslint:disable:typedef 29 | public get initEvent() { 30 | return StorageEvent.prototype.initEvent.bind(this); 31 | } 32 | 33 | public get preventDefault() { 34 | return StorageEvent.prototype.preventDefault.bind(this); 35 | } 36 | 37 | public get stopImmediatePropagation() { 38 | return StorageEvent.prototype.stopImmediatePropagation.bind(this); 39 | } 40 | 41 | public get stopPropagation() { 42 | return StorageEvent.prototype.stopPropagation.bind(this); 43 | } 44 | 45 | public get composedPath() { 46 | return StorageEvent.prototype.composedPath.bind(this); 47 | } 48 | 49 | public get AT_TARGET() { 50 | return StorageEvent.prototype.AT_TARGET; 51 | } 52 | 53 | public get BUBBLING_PHASE() { 54 | return StorageEvent.prototype.BUBBLING_PHASE; 55 | } 56 | 57 | public get CAPTURING_PHASE() { 58 | return StorageEvent.prototype.BUBBLING_PHASE; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/utility/storage/storage.ts: -------------------------------------------------------------------------------- 1 | import { NgxStorageEvent } from './storage-event'; 2 | import { Subject } from 'rxjs'; 3 | import { StorageName } from '../webstorage.utility'; 4 | 5 | // TODO: in the future use ES6 Proxy to handle indexers 6 | export abstract class NgxStorage implements Storage { 7 | [key: string]: any; 8 | [index: number]: string; 9 | public externalChanges?: Subject = new Subject(); 10 | public abstract setItem(key: string, value: any): void; 11 | public abstract removeItem(key: string): void; 12 | public abstract getItem(key: string): any; 13 | public abstract key(index: number): any; 14 | public abstract clear(): void; 15 | public abstract get length(): number; 16 | public abstract get type(): StorageName; 17 | 18 | protected emitEvent(key: string, newValue: any, oldValue?: any): void { 19 | const event = new NgxStorageEvent(this.type, key, this); 20 | event.oldValue = (oldValue !== undefined) ? oldValue : this.getItem(key); 21 | event.newValue = newValue; 22 | event.isInternal = false; 23 | this.externalChanges?.next(event); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/utility/webstorage.utility.spec.ts: -------------------------------------------------------------------------------- 1 | import { cookiesStorageUtility, localStorageUtility, sessionStorageUtility, sharedStorageUtility } from '.'; 2 | 3 | describe('WebStorageUtility', () => { 4 | it('getStorageName() should give proper values', () => { 5 | expect(localStorageUtility.getStorageName()).toBe('localStorage'); 6 | expect(sessionStorageUtility.getStorageName()).toBe('sessionStorage'); 7 | expect(cookiesStorageUtility.getStorageName()).toBe('cookiesStorage'); 8 | expect(sharedStorageUtility.getStorageName()).toBe('sharedStorage'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/utility/webstorage.utility.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorConfig } from '../ngx-store.types'; 2 | import { WebStorage } from './storage/cookies-storage'; 3 | import { Cache } from '../decorator/cache'; 4 | import { CONFIG_PREFIX, debug } from '../config/config'; 5 | import { Observable, Subject } from 'rxjs'; 6 | import { NgxStorageEvent } from './storage/storage-event'; 7 | 8 | export type StorageName = 'localStorage' | 'sessionStorage' | 'cookiesStorage' | 'sharedStorage'; 9 | 10 | export class WebStorageUtility { 11 | protected _storage: WebStorage; 12 | 13 | public constructor(storage: WebStorage, prefix: string = '', previousPrefix?: string) { 14 | this._storage = storage; 15 | this._prefix = prefix; 16 | 17 | // handle previousPrefix for backward-compatibility and safe config changes below 18 | if (prefix === previousPrefix) { 19 | return; 20 | } 21 | if (previousPrefix === null) { 22 | return; 23 | } 24 | if (previousPrefix === undefined) { 25 | return; 26 | } 27 | debug.log(this.getStorageName() + ' > Detected prefix change from ' + previousPrefix + ' to ' + prefix); 28 | this.forEach((value, key) => { 29 | // ignore config settings when previousPrefix = '' 30 | if (key.startsWith(previousPrefix) && !key.startsWith(CONFIG_PREFIX)) { 31 | const nameWithoutPrefix = this.trimPrefix(key); 32 | this.set(nameWithoutPrefix, this._storage.getItem(key)); 33 | 34 | if (previousPrefix !== '') { 35 | this._storage.removeItem(key); 36 | } 37 | } 38 | }); 39 | } 40 | 41 | protected _prefix: string = ''; 42 | 43 | public get prefix(): string { 44 | return this._prefix; 45 | } 46 | 47 | protected _changes: Subject = new Subject(); 48 | 49 | public get changes(): Observable { 50 | return this._changes.asObservable(); 51 | } 52 | 53 | public get keys(): Array { 54 | const keys: Array = []; 55 | this.forEach((value, key) => keys.push(key)); 56 | return keys; 57 | } 58 | 59 | public static getSettable(value: any): string { 60 | return JSON.stringify(value); 61 | } 62 | 63 | public static getGettable(value: string): any { 64 | if (value === 'undefined') { 65 | return null; 66 | } 67 | try { 68 | return JSON.parse(value); 69 | } catch (e) { 70 | return value; 71 | } 72 | } 73 | 74 | public getStorage(): WebStorage { 75 | return this._storage; 76 | } 77 | 78 | public getStorageKey(key: string, prefix?: string): string { 79 | prefix = (typeof prefix === 'string') ? prefix : this.prefix; 80 | return `${prefix}${key}`; 81 | } 82 | 83 | public getStorageName(): StorageName { 84 | return this._storage.type || ((this._storage === localStorage) ? 'localStorage' : 'sessionStorage'); 85 | } 86 | 87 | public get(key: string, config: DecoratorConfig = {}): any { 88 | const storageKey = this.getStorageKey(key, config.prefix); 89 | const value = this._storage.getItem(storageKey); 90 | // TODO return undefined if no key 91 | /*if (value === null && !(storageKey in this._storage)) { 92 | return undefined; 93 | }*/ 94 | return this.getGettable(value as string); 95 | } 96 | 97 | public set(key: string, value: T, config: DecoratorConfig = {}): T | null { 98 | if (value === null || value === undefined) { 99 | this.remove(key); 100 | return null; 101 | } 102 | try { 103 | const storageKey = this.getStorageKey(key, config.prefix); 104 | const storable = this.getSettable(value); 105 | this.emitEvent(key, value); 106 | this._storage.setItem(storageKey, storable, config.expires); 107 | const cacheItem = Cache.get(key); 108 | if (cacheItem) { 109 | debug.log(`updating following CacheItem from ${this.constructor.name}:`, cacheItem); 110 | cacheItem.resetProxy(); 111 | cacheItem.propagateChange(value, this); 112 | } 113 | } catch (error) { 114 | console.warn(`[ngx-store] ${this.getStorageName()}: 115 | following error occurred while trying to save ${key} =`, value); 116 | console.error(error); 117 | } 118 | return value; 119 | } 120 | 121 | // TODO return true if item existed and false otherwise (?) 122 | public remove(key: string, config: DecoratorConfig = {}): void { 123 | const storageKey = this.getStorageKey(key, config.prefix); 124 | this._storage.removeItem(storageKey); 125 | const cacheItem = Cache.get(key); 126 | if (cacheItem) { 127 | cacheItem.resetProxy(); 128 | } 129 | } 130 | 131 | public clear(): void { 132 | this.emitEvent(null as any, null, null); 133 | this.forEach((value, key) => { 134 | if (key.startsWith(CONFIG_PREFIX)) { 135 | return; 136 | } 137 | this.remove(key, {prefix: ''}); 138 | }); 139 | } 140 | 141 | public forEach(callbackFn: (value: any, key: string) => any): void { 142 | if (typeof this._storage.forEach === 'function') { 143 | return this._storage.forEach((value: any, key: string) => { 144 | callbackFn(this.getGettable(value), key); 145 | }); 146 | } 147 | Object.keys(this._storage).forEach((key) => { 148 | callbackFn(this.getGettable(this._storage[key]), key); 149 | }); 150 | } 151 | 152 | public getSettable(value: any): string { 153 | return WebStorageUtility.getSettable(value); 154 | } 155 | 156 | public getGettable(value: string): any { 157 | return WebStorageUtility.getGettable(value); 158 | } 159 | 160 | public trimPrefix(key: string): string { 161 | return key.replace(this.prefix, ''); 162 | } 163 | 164 | protected emitEvent(key: string, newValue: any, oldValue?: any): void { 165 | const event = new NgxStorageEvent(this.getStorageName(), key, this._storage); 166 | event.oldValue = (oldValue !== undefined) ? oldValue : this.get(key); 167 | event.newValue = newValue; 168 | this._changes.next(event); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-store 3 | */ 4 | 5 | // Public classes. 6 | export { 7 | CookieStorage, LocalStorage, SessionStorage, SharedStorage, SharedStorage as TempStorage, 8 | } from './lib/decorator/webstorage'; 9 | export { WebStorageService } from './lib/service/webstorage.service'; 10 | export { 11 | CookiesStorageService, CookiesStorageService as CookieStorageService, 12 | } from './lib/service/cookies-storage.service'; 13 | export { SharedStorageService, SharedStorageService as TempStorageService } from './lib/service/shared-storage.service'; 14 | export { LocalStorageService } from './lib/service/local-storage.service'; 15 | export { SessionStorageService } from './lib/service/session-storage.service'; 16 | export { WebStorageConfigInterface } from './lib/config/config.interface'; 17 | export { Webstorable, WebstorableArray, WebstorableObject } from './lib/ngx-store.types'; 18 | export { NgxStorageEvent } from './lib/utility/storage/storage-event'; 19 | export { Resource as NgxResource } from './lib/service/resource'; 20 | export { NgxStoreModule, NgxStoreModule as WebStorageModule } from './lib/ngx-store.module'; 21 | export * from './lib/ngx-store.types'; 22 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 7 | 8 | declare const require: { 9 | context(path: string, deep?: boolean, filter?: RegExp): { 10 | keys(): string[]; 11 | (id: string): T; 12 | }; 13 | }; 14 | 15 | // First, initialize the Angular testing environment. 16 | getTestBed().initTestEnvironment( 17 | BrowserDynamicTestingModule, 18 | platformBrowserDynamicTesting(), 19 | ); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "target": "es6", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "inlineSources": true, 12 | "strictNullChecks": false, 13 | "types": ["node"], 14 | "lib": [ 15 | "dom", 16 | "es2018" 17 | ] 18 | }, 19 | "angularCompilerOptions": { 20 | "skipTemplateCodegen": true, 21 | "strictMetadataEmit": true, 22 | "enableResourceInlining": true, 23 | }, 24 | "exclude": [ 25 | "src/test.ts", 26 | "**/*.spec.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.schematics.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": [ 5 | "es2018", 6 | "dom" 7 | ], 8 | "declaration": true, 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noUnusedParameters": true, 16 | "noUnusedLocals": true, 17 | "rootDir": "schematics", 18 | "outDir": "../../dist/ngx-store/schematics", 19 | "skipDefaultLibCheck": true, 20 | "skipLibCheck": true, 21 | "sourceMap": true, 22 | "strictNullChecks": true, 23 | "target": "es6", 24 | "types": [ 25 | "jasmine", 26 | "node" 27 | ] 28 | }, 29 | "include": [ 30 | "schematics/**/*" 31 | ], 32 | "exclude": [ 33 | "schematics/*/files/**/*" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ], 16 | "curly": [true, "ignore-same-line"], 17 | "ban": [true, "fit", "fdescribe"], 18 | "max-line-length": [true, { 19 | "limit": 120, 20 | "ignore-pattern": "/import/" 21 | }], 22 | "no-console": { 23 | "options": ["log", "time", "timeEnd", "trace", "assert", "clear", 24 | "count", "debug", "profile", "profileEnd", "select", "table", "info"], 25 | "severity": "warning" 26 | }, 27 | "no-empty-interface": false, 28 | "no-inferrable-types": false, 29 | "no-redundant-jsdoc": false, 30 | "no-non-null-assertion": false 31 | } 32 | } 33 | --------------------------------------------------------------------------------