├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .prettierignore ├── .yarnrc ├── CHANGELOG.md ├── README.md ├── angular.json ├── data.iml ├── docs ├── README.md └── pages │ ├── computed.md │ ├── data-action.md │ ├── entity.md │ ├── extension-api.md │ ├── immutability.md │ ├── lifecycle.md │ ├── persistence-state.md │ ├── quick-start.md │ ├── state-repository.md │ └── testing.md ├── integration ├── app │ ├── assets │ │ ├── .gitkeep │ │ └── img │ │ │ ├── logo.png │ │ │ └── stackblitz.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── json │ │ └── person.json │ ├── main.ts │ ├── polyfills.ts │ ├── src │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── examples │ │ │ ├── amount │ │ │ │ ├── amount.component.html │ │ │ │ ├── amount.component.ts │ │ │ │ ├── amount.module.ts │ │ │ │ ├── amount.state.ts │ │ │ │ └── price.state.ts │ │ │ ├── article │ │ │ │ ├── article-entities.state.ts │ │ │ │ ├── article.component.html │ │ │ │ ├── article.component.ts │ │ │ │ ├── article.module.ts │ │ │ │ ├── article.ts │ │ │ │ └── dialog │ │ │ │ │ ├── article-dialog.component.html │ │ │ │ │ └── article-dialog.component.ts │ │ │ ├── count │ │ │ │ ├── count-model.ts │ │ │ │ ├── count-sub.state.ts │ │ │ │ ├── count.component.html │ │ │ │ ├── count.component.ts │ │ │ │ ├── count.module.ts │ │ │ │ ├── count.state.ts │ │ │ │ └── parent-count-model.ts │ │ │ ├── person │ │ │ │ ├── person-model.ts │ │ │ │ ├── person.component.html │ │ │ │ ├── person.component.ts │ │ │ │ ├── person.module.ts │ │ │ │ ├── person.resolver.ts │ │ │ │ ├── person.service.ts │ │ │ │ └── person.state.ts │ │ │ └── todo │ │ │ │ ├── todo.component.html │ │ │ │ ├── todo.component.ts │ │ │ │ ├── todo.module.ts │ │ │ │ └── todo.state.ts │ │ └── utils │ │ │ └── generate-uid.ts │ └── styles.scss └── tests │ ├── common-extensions │ ├── action-decorator.spec.ts │ ├── argument-decorators.spec.ts │ ├── computed-observable.spec.ts │ ├── computed.spec.ts │ ├── counter-lifecycle.spec.ts │ ├── counter.spec.ts │ ├── debounce.spec.ts │ ├── deep-data-action-type.spec.ts │ ├── name-creator.spec.ts │ ├── ngxs-meta.spec.ts │ ├── registration.spec.ts │ ├── state-context.spec.ts │ ├── two-state.spec.ts │ └── zone.spec.ts │ ├── entity │ ├── entity.spec.ts │ ├── observables.spec.ts │ ├── primary-key-or-unique-id.spec.ts │ ├── sort-by.spec.ts │ └── uuidv4.spec.ts │ ├── immutability │ ├── immutable.spec.ts │ ├── mutate-selectors.spec.ts │ └── ngxs-data.spec.ts │ ├── inheritance-extensions │ ├── children.spec.ts │ └── inheritance.spec.ts │ ├── reset-extensions │ └── reset.spec.ts │ ├── setupJest.ts │ ├── storage-extensions │ └── storage.spec.ts │ ├── testing │ ├── testing-v1.spec.ts │ ├── testing-v2.spec.ts │ └── testing-v3.spec.ts │ └── utils-extensions │ └── mutable.spec.ts ├── jest.config.js ├── lib ├── decorators │ ├── package.json │ └── src │ │ ├── computed │ │ └── computed.ts │ │ ├── data-action │ │ ├── data-action.config.ts │ │ └── data-action.ts │ │ ├── debounce │ │ └── debounce.ts │ │ ├── named │ │ └── named.ts │ │ ├── payload │ │ └── payload.ts │ │ ├── persistence │ │ └── persistence.ts │ │ ├── public_api.ts │ │ └── state-repository │ │ └── state-repository.ts ├── internals │ ├── package.json │ └── src │ │ ├── decorators │ │ ├── validate-action.ts │ │ └── validate-computed-method.ts │ │ ├── exceptions │ │ ├── invalid-args-names.exception.ts │ │ └── invalid-children.exception.ts │ │ ├── public_api.ts │ │ ├── services │ │ ├── ngxs-data-computed-stream.service.ts │ │ ├── ngxs-data-factory.service.ts │ │ └── ngxs-data-injector.service.ts │ │ ├── storage │ │ └── init-storage.ts │ │ └── utils │ │ ├── action │ │ ├── action-name-creator.ts │ │ └── dynamic-action.ts │ │ ├── common │ │ ├── build-defaults-graph.ts │ │ ├── check-exist-ng-zone.ts │ │ ├── combine-stream.ts │ │ └── computed-key.ts │ │ ├── computed │ │ ├── ensure-computed-cache.ts │ │ ├── get-computed-cache.ts │ │ ├── global-sequence-id.ts │ │ └── it-observable.ts │ │ ├── method-args-registry │ │ ├── ensure-method-args-registry.ts │ │ ├── get-method-args-registry.ts │ │ └── method-args-registry.ts │ │ ├── repository │ │ ├── create-repository-metadata.ts │ │ ├── define-default-repository-meta.ts │ │ ├── ensure-repository.ts │ │ ├── ensure-snapshot.ts │ │ └── get-repository.ts │ │ └── state-context │ │ ├── create-context.ts │ │ ├── create-state-selector.ts │ │ ├── ensure-data-state-context.ts │ │ ├── ensure-state-metadata.ts │ │ ├── get-state-metadata.ts │ │ └── get-store-options.ts ├── ng-package.json ├── ngcc.config.js ├── package.json ├── public_api.ts ├── repositories │ ├── package.json │ └── src │ │ ├── common │ │ └── abstract-repository.ts │ │ ├── ngxs-data-entity-collections │ │ └── ngxs-data-entity-collections.repository.ts │ │ ├── ngxs-data │ │ └── ngxs-data.repository.ts │ │ ├── ngxs-immutable-data │ │ └── ngxs-immutable-data.repository.ts │ │ └── public_api.ts ├── src │ └── ngxs-data.module.ts ├── storage │ ├── package.json │ └── src │ │ ├── exceptions │ │ ├── invalid-data-value.exception.ts │ │ ├── invalid-last-changed.exception.ts │ │ ├── invalid-structure-data.exception.ts │ │ ├── invalid-version.exception.ts │ │ ├── not-declare-engine.exception.ts │ │ └── not-implemented-storage.exception.ts │ │ ├── ngxs-data-storage-container.ts │ │ ├── ngxs-data-storage-plugin.service.ts │ │ ├── public_api.ts │ │ ├── tokens │ │ ├── storage-container-provider.ts │ │ ├── storage-container-token.ts │ │ ├── storage-decode-type-token.ts │ │ ├── storage-decode-type.ts │ │ ├── storage-extension-provider.ts │ │ ├── storage-prefix-token.ts │ │ ├── storage-prefix.ts │ │ ├── storage-ttl-delay.ts │ │ └── storage-use-factory.ts │ │ └── utils │ │ ├── can-be-pull-from-storage.ts │ │ ├── create-default.ts │ │ ├── create-ttl-interval.ts │ │ ├── deserialize-by-storage-meta.ts │ │ ├── ensure-key.ts │ │ ├── ensure-path.ts │ │ ├── ensure-providers.ts │ │ ├── ensure-serialize-data.ts │ │ ├── exist-ttl.ts │ │ ├── expose-engine.ts │ │ ├── fire-state-when-expired.ts │ │ ├── is-expired.ts │ │ ├── is-init-action.ts │ │ ├── is-storage-event.ts │ │ ├── merge-options.ts │ │ ├── parse-storage-meta.ts │ │ ├── register-storage-providers.ts │ │ ├── rehydrate.ts │ │ ├── silent-deserialize-warning.ts │ │ ├── silent-serialize-warning.ts │ │ ├── ttl-handler.ts │ │ ├── ttl-strategy-handler.ts │ │ └── validate-path-in-provider.ts ├── testing │ ├── package.json │ └── src │ │ ├── platform │ │ ├── internal │ │ │ ├── create-internal-ngxs-root-element.ts │ │ │ ├── remove-internal-ngxs-root-element.ts │ │ │ └── types.ts │ │ ├── ngxs-app-mock.component.ts │ │ ├── ngxs-app-mock.module.ts │ │ ├── ngxs-data-testing.module.ts │ │ ├── ngxs-platform.ts │ │ └── reset-platform-after-bootrapping.ts │ │ └── public_api.ts ├── tokens │ ├── package.json │ └── src │ │ ├── public_api.ts │ │ └── symbols │ │ ├── need-sync-type-action.ts │ │ ├── ngxs-computed-options.ts │ │ ├── ngxs-data-exceptions.ts │ │ ├── ngxs-data-meta.ts │ │ ├── ngxs-meta-key.ts │ │ └── ngxs-meta-payload.ts └── typings │ ├── package.json │ └── src │ ├── common │ ├── actions-properties.ts │ ├── computed-cache-map.ts │ ├── computed-options.ts │ ├── data-state-class.ts │ ├── dispatched-result.ts │ ├── extension.ts │ ├── mapped-state.ts │ ├── ngxs-data-lifecycle.ts │ └── repository.ts │ ├── entity │ ├── entity-context.ts │ └── entity-repository.ts │ ├── public_api.ts │ └── storage │ └── storage.ts ├── package.json ├── renovate.json ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.spec.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 4 7 | end_of_line = lf 8 | indent_style = space 9 | max_line_length = 120 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.lint.ts 2 | **/*.d.ts 3 | **/dist/** 4 | **/node_modules/** 5 | **/coverage/** 6 | **/docs/** 7 | **/*.spec.ts 8 | **/*.md 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@angular-ru/eslint-config' 3 | }; 4 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: NGXS DATA CI 5 | 6 | on: [push] 7 | 8 | jobs: 9 | build-and-deploy: 10 | if: "!contains(github.event.head_commit.message , 'ci skip')" 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [14.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | persist-credentials: false 22 | - run: git fetch --prune --unshallow --tags 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - uses: actions/cache@v2 30 | with: 31 | path: '**/node_modules' 32 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 33 | 34 | - name: Run install step 35 | run: | 36 | yarn install --frozen-lockfile --non-interactive 37 | 38 | - name: Run building libraries step 39 | run: | 40 | yarn build:lib 41 | 42 | - name: Run lint step 43 | run: | 44 | yarn lint 45 | 46 | - name: Run test step 47 | run: | 48 | yarn test --coverage 49 | 50 | - name: Run building integration apps step 51 | run: yarn build:app 52 | 53 | - name: Deploy integration apps 54 | env: 55 | GH_TOKEN: ${{ secrets.GH_DEPLOY }} 56 | if: contains('refs/heads/master', github.ref) 57 | run: echo "yarn ng deploy" # yarn ng deploy 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | **/dist/** 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | **/node_modules/** 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | .eslintcache 43 | .cache 44 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | .github/** 3 | .idea/** 4 | .cache/** 5 | .vscode/** 6 | coverage/** 7 | **/dist/** 8 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Deprecating registry.yarnpkg.com 2 | # Issue: https://github.com/yarnpkg/yarn/issues/5891 3 | --registry "https://registry.npmjs.org/" 4 | --scripts-prepend-node-path true 5 | --ignore-engines true 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This repo is deprecated! 2 | 3 | Actual: 4 | https://github.com/Angular-RU/angular-ru-sdk/tree/master/libs/ngxs 5 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "integration": { 7 | "root": "", 8 | "sourceRoot": "integration", 9 | "projectType": "application", 10 | "prefix": "", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/integration", 17 | "index": "integration/app/index.html", 18 | "main": "integration/app/main.ts", 19 | "polyfills": "integration/app/polyfills.ts", 20 | "tsConfig": "./tsconfig.app.json", 21 | "assets": ["integration/app/favicon.ico", "integration/app/assets", "integration/app/json"], 22 | "styles": ["integration/app/styles.scss"], 23 | "scripts": [], 24 | "vendorChunk": true, 25 | "extractLicenses": false, 26 | "buildOptimizer": false, 27 | "sourceMap": true, 28 | "optimization": false, 29 | "namedChunks": true 30 | }, 31 | "configurations": { 32 | "production": { 33 | "budgets": [ 34 | { 35 | "type": "anyComponentStyle", 36 | "maximumWarning": "6kb" 37 | } 38 | ], 39 | "fileReplacements": [ 40 | { 41 | "replace": "integration/app/environments/environment.ts", 42 | "with": "integration/app/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "namedChunks": false, 49 | "extractLicenses": true, 50 | "buildOptimizer": true 51 | } 52 | }, 53 | "defaultConfiguration": "" 54 | }, 55 | "serve": { 56 | "builder": "@angular-devkit/build-angular:dev-server", 57 | "options": { 58 | "browserTarget": "integration:build" 59 | }, 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "integration:build:production" 63 | } 64 | } 65 | }, 66 | "deploy": { 67 | "builder": "angular-cli-ghpages:deploy", 68 | "options": { 69 | "noSilent": true, 70 | "noBuild": true, 71 | "name": "splincode", 72 | "email": "splincodewd@yandex.ru", 73 | "repo": "https://github.com/ngxs-labs/data.git" 74 | } 75 | } 76 | } 77 | }, 78 | "library": { 79 | "root": "", 80 | "prefix": "", 81 | "sourceRoot": "", 82 | "projectType": "library", 83 | "architect": { 84 | "build": { 85 | "builder": "@angular-devkit/build-angular:ng-packagr", 86 | "options": { 87 | "tsConfig": "tsconfig.lib.json", 88 | "project": "lib/ng-package.json" 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "defaultProject": "integration" 95 | } 96 | -------------------------------------------------------------------------------- /data.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Table of contents: 2 | 3 | 1. [📖 Changelog](https://github.com/ngxs-labs/data/blob/master/CHANGELOG.md) 4 | 2. [🚀 Quick Start](pages/quick-start.md) 5 | 3. [📦 Advanced](#table-of-contents) 6 | 7 | - [(@)StateRepository](pages/state-repository.md) 8 | - [(@)DataAction](pages/data-action.md) 9 | - [(@)Computed](pages/computed.md) 10 | - [(@)Persistence](pages/persistence-state.md) 11 | - [Entity state adapter](pages/entity.md) 12 | - [Unit Testing](pages/testing.md) 13 | - [Lifecycle](pages/lifecycle.md) 14 | - [Immutability](pages/immutability.md) 15 | - [Extension API](pages/extension-api.md) 16 | -------------------------------------------------------------------------------- /docs/pages/extension-api.md: -------------------------------------------------------------------------------- 1 | ## Extension API 2 | 3 | ```ts 4 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 5 | // .. 6 | 7 | @NgModule({ 8 | imports: [ 9 | NgxsDataPluginModule.forRoot([ 10 | MY_FIRST_EXTENSION, 11 | MY_SECOND_EXTENSION, 12 | MY_THIRD_EXTENSION, 13 | MY_FOURTH_EXTENSION, 14 | MY_FIFTH_EXTENSION 15 | ]) 16 | ] 17 | }) 18 | export class AppModule {} 19 | ``` 20 | 21 | `my-extensions.ts` - you can define any providers in your module: 22 | 23 | ```ts 24 | import { NgxsDataExtension } from '@ngxs-labs/data/typings'; 25 | // .. 26 | 27 | export const MY_FIRST_EXTENSION: NgxsDataExtension = { 28 | provide: NGXS_PLUGINS, 29 | useClass: MySuperService, 30 | multi: true 31 | }; 32 | 33 | export const MY_SECOND_EXTENSION: NgxsDataExtension = [ 34 | { 35 | provide: NGXS_PLUGINS, 36 | useClass: FeatureService, 37 | multi: true 38 | }, 39 | { 40 | provide: MyInjectableToken, 41 | useFactory: (): MyFactory => new MyFactory() 42 | } 43 | ]; 44 | 45 | export const MY_THIRD_EXTENSION: NgxsDataExtension = [MySuperPluginA.forRoot(), MySuperPluginB.forRoot()]; 46 | 47 | export const MY_FOURTH_EXTENSION: NgxsDataExtension = MyInjectableService; 48 | 49 | export const MY_FIFTH_EXTENSION: NgxsDataExtension = { 50 | provide: MY_TOKEN, 51 | useValue: 'VALUE' 52 | }; 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/pages/lifecycle.md: -------------------------------------------------------------------------------- 1 | ## Lifecycle sequence 2 | 3 | After creating the state by calling its constructor, NGXS calls the lifecycle hook methods in the following sequence at 4 | specific moments: 5 | 6 | | Hook | Purpose and Timing | 7 | | -------------------- | --------------------------------------------------------------------------------------------------------- | 8 | | ngxsOnChanges() | Called _before_ `ngxsOnInit()` and whenever state changes. | 9 | | ngxsOnInit() | Called _once_, after the _first_ `ngxsOnChanges()` and _before_ the `APP_INITIALIZER` token is resolved. | 10 | | ngxsAfterBootstrap() | Called _once_, after the root view and all its children have been rendered. | 11 | | ngxsDataDoCheck() | Called _after_ `ngxsAfterBootstrap()` and called every time a state is reinitialized after a state reset. | 12 | | ngxsDataAfterReset() | Called every time _after_ `reset()` | 13 | 14 | ### `ngxsDataDoCheck` and `ngxsDataAfterReset` 15 | 16 | ```ts 17 | @StateRepository() 18 | @State({ 19 | name: 'customers', 20 | defaults: [] 21 | }) 22 | @Injectable() 23 | class CustomersStates extends NgxsDataRepository implements NgxsDataDoCheck, NgxsDataAfterReset { 24 | private subscription: Subscription; 25 | 26 | constructor(private customer: CustomerService) { 27 | super(); 28 | } 29 | 30 | /** 31 | * @description: 32 | * This method guarantees that it will be called after the application is rendered 33 | * and all services of the Angular are loaded, so you can subscribe to the necessary 34 | * data streams (any observables) in this method and unsubscribe later in the method `ngxsDataAfterReset` 35 | */ 36 | public ngxsDataDoCheck(): void { 37 | console.log(this.isInitialised); // true 38 | console.log(this.isBootstrapped); // true 39 | this.subscription = this.customer.events.subscribe((e) => console.log(e)); 40 | } 41 | 42 | public ngxsDataAfterReset(): void { 43 | this.subscription?.unsubscribe(); 44 | } 45 | } 46 | ``` 47 | 48 | ### `ngxsOnChanges`, `ngxsOnInit` and `ngxsAfterBootstrap` 49 | 50 | ```ts 51 | @StateRepository() 52 | @State({ 53 | name: 'counter', 54 | defaults: 0 55 | }) 56 | @Injectable() 57 | class CounterState extends NgxsDataRepository implements NgxsOnChanges, NgxsOnInit, NgxsAfterBootstrap { 58 | public ngxsOnChanges(): void { 59 | super.ngxsOnChanges(); // be sure to call the parent method 60 | // your logic 61 | } 62 | 63 | public ngxsOnInit(): void { 64 | super.ngxsOnInit(); // be sure to call the parent method 65 | // your logic 66 | } 67 | 68 | public ngxsAfterBootstrap(): void { 69 | super.ngxsAfterBootstrap(); // be sure to call the parent method 70 | // your logic 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/pages/quick-start.md: -------------------------------------------------------------------------------- 1 | ## Quick Start 2 | 3 | `app.module.ts` 4 | 5 | ```ts 6 | import { NgxsModule } from '@ngxs/store'; 7 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | // .. 12 | NgxsModule.forRoot([AppState]), 13 | NgxsDataPluginModule.forRoot() 14 | ] 15 | // .. 16 | }) 17 | export class AppModule {} 18 | ``` 19 | 20 | `count.state.ts` 21 | 22 | ```ts 23 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 24 | import { Computed, DataAction, StateRepository } from '@ngxs-labs/data/decorators'; 25 | import { State } from '@ngxs/store'; 26 | // .. 27 | 28 | export interface CountModel { 29 | val: number; 30 | } 31 | 32 | @StateRepository() 33 | @State({ 34 | name: 'count', 35 | defaults: { val: 0 } 36 | }) 37 | @Injectable() 38 | export class CountState extends NgxsDataRepository { 39 | @Computed() 40 | public get values$() { 41 | return this.state$.pipe(map((state) => state.countSub)); 42 | } 43 | 44 | @DataAction() 45 | public increment(): void { 46 | this.ctx.setState((state) => ({ val: state.val + 1 })); 47 | } 48 | 49 | @DataAction() 50 | public decrement(): void { 51 | this.ctx.setState((state) => ({ val: state.val - 1 })); 52 | } 53 | 54 | @Debounce() 55 | @DataAction() 56 | public setValueFromInput(@Payload('value') val: string | number): void { 57 | this.ctx.setState({ val: parseFloat(val) || 0 }); 58 | } 59 | } 60 | ``` 61 | 62 | `app.component.ts` 63 | 64 | ```ts 65 | ... 66 | 67 | @Component({ 68 | selector: 'app', 69 | template: ` 70 | Selection: 71 | counter.state$ = {{ counter.state$ | async | json }}
72 | counter.values$ = {{ counter.values$ | async }}
73 | 74 | Actions: 75 | 76 | 77 | 78 | 79 | ngModel: 80 | 84 | 85 | (delay: 300ms) 86 | ` 87 | }) 88 | export class AppComponent { 89 | constructor(public counter: CountState) {} 90 | } 91 | ``` 92 | 93 | #### Debugging 94 | 95 | `Need provide logger-plugin` 96 | 97 | ```ts 98 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 99 | 100 | @NgModule({ 101 | imports: [ 102 | // .. 103 | NgxsLoggerPluginModule.forRoot() 104 | ] 105 | // .. 106 | }) 107 | export class AppModule {} 108 | ``` 109 | 110 | ```ts 111 | @StateRepository() 112 | @State({ 113 | name: 'todo', 114 | defaults: [] 115 | }) 116 | @Injectable() 117 | export class TodoState extends NgxsDataRepository { 118 | @DataAction() 119 | public addTodo(@Payload('todo') todo: string): void { 120 | if (todo) { 121 | this.ctx.setState((state) => state.concat(todo)); 122 | } 123 | } 124 | 125 | @DataAction() 126 | public removeTodo(@Payload('idx') idx: number): void { 127 | this.ctx.setState((state) => state.filter((_: string, index: number): boolean => index !== idx)); 128 | } 129 | } 130 | ``` 131 | 132 | ![](https://habrastorage.org/webt/6t/f5/ku/6tf5kuw5naqpgvyphebd4el_ync.png) 133 | -------------------------------------------------------------------------------- /integration/app/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/data/bfdccaccc04d4c62c65537be64cf66547cd137ff/integration/app/assets/.gitkeep -------------------------------------------------------------------------------- /integration/app/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/data/bfdccaccc04d4c62c65537be64cf66547cd137ff/integration/app/assets/img/logo.png -------------------------------------------------------------------------------- /integration/app/assets/img/stackblitz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/data/bfdccaccc04d4c62c65537be64cf66547cd137ff/integration/app/assets/img/stackblitz.png -------------------------------------------------------------------------------- /integration/app/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment: { production: boolean } = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /integration/app/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment: { production: boolean } = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /integration/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/data/bfdccaccc04d4c62c65537be64cf66547cd137ff/integration/app/favicon.ico -------------------------------------------------------------------------------- /integration/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxsData 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /integration/app/json/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "title": "My json title", 4 | "description": "My json description" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /integration/app/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { isTrue } from '@angular-ru/common/utils'; 4 | 5 | import { environment } from './environments/environment'; 6 | import { AppModule } from './src/app.module'; 7 | 8 | if (isTrue(environment.production)) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic() 13 | .bootstrapModule(AppModule) 14 | .catch((err: Error): void => console.error(err)); 15 | -------------------------------------------------------------------------------- /integration/app/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/proposals/reflect-metadata'; 2 | import 'zone.js'; 3 | -------------------------------------------------------------------------------- /integration/app/src/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/typedef,@typescript-eslint/explicit-function-return-type */ 2 | import { NgModule } from '@angular/core'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | @NgModule({ 6 | imports: [ 7 | RouterModule.forRoot( 8 | [ 9 | { 10 | path: '', 11 | redirectTo: 'count', 12 | pathMatch: 'full' 13 | }, 14 | { 15 | path: 'count', 16 | loadChildren: () => import('./examples/count/count.module').then((m) => m.CountModule) 17 | }, 18 | { 19 | path: 'todo', 20 | loadChildren: () => import('./examples/todo/todo.module').then((m) => m.TodoModule) 21 | }, 22 | { 23 | path: 'person', 24 | loadChildren: () => import('./examples/person/person.module').then((m) => m.PersonModule) 25 | }, 26 | { 27 | path: 'amount', 28 | loadChildren: () => import('./examples/amount/amount.module').then((m) => m.AmountModule) 29 | }, 30 | { 31 | path: 'article', 32 | loadChildren: () => import('./examples/article/article.module').then((m) => m.ArticleModule) 33 | } 34 | ], 35 | { useHash: true, relativeLinkResolution: 'legacy' } 36 | ) 37 | ], 38 | exports: [RouterModule] 39 | }) 40 | export class AppRoutingModule {} 41 | -------------------------------------------------------------------------------- /integration/app/src/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |

Store:

6 |
{{ snapshot | async | json }}
7 |
8 |
9 | 10 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /integration/app/src/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, isDevMode, OnInit } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class AppComponent implements OnInit { 11 | public snapshot: Observable = this.store.select((state: unknown): unknown => state); 12 | 13 | constructor(private readonly store: Store) {} 14 | 15 | public ngOnInit(): void { 16 | // eslint-disable-next-line no-console 17 | console.log('[isDevMode]', isDevMode()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integration/app/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 6 | import { NgxsModule, NoopNgxsExecutionStrategy } from '@ngxs/store'; 7 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 8 | import { NGXS_DATA_STORAGE_CONTAINER, NGXS_DATA_STORAGE_EXTENSION } from '@ngxs-labs/data/storage'; 9 | 10 | import { environment } from '../environments/environment'; 11 | import { AppComponent } from './app.component'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | 14 | @NgModule({ 15 | declarations: [AppComponent], 16 | imports: [ 17 | BrowserModule, 18 | FormsModule, 19 | AppRoutingModule, 20 | BrowserAnimationsModule, 21 | ReactiveFormsModule, 22 | NgxsModule.forRoot([], { 23 | developmentMode: !environment.production, 24 | executionStrategy: NoopNgxsExecutionStrategy 25 | }), 26 | NgxsLoggerPluginModule.forRoot(), 27 | NgxsDataPluginModule.forRoot([NGXS_DATA_STORAGE_EXTENSION, NGXS_DATA_STORAGE_CONTAINER]) 28 | ], 29 | providers: [], 30 | bootstrap: [AppComponent] 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /integration/app/src/examples/amount/amount.component.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | 6 | 9 | 10 |

Sum: {{ amount.sum }}

11 | -------------------------------------------------------------------------------- /integration/app/src/examples/amount/amount.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { AmountState } from './amount.state'; 4 | import { PriceState } from './price.state'; 5 | 6 | @Component({ 7 | selector: 'amount', 8 | templateUrl: './amount.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class AmountComponent { 12 | constructor(public price: PriceState, public amount: AmountState) {} 13 | } 14 | -------------------------------------------------------------------------------- /integration/app/src/examples/amount/amount.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgxsModule } from '@ngxs/store'; 6 | 7 | import { AmountComponent } from './amount.component'; 8 | import { AmountState } from './amount.state'; 9 | import { PriceState } from './price.state'; 10 | 11 | @NgModule({ 12 | declarations: [AmountComponent], 13 | imports: [ 14 | FormsModule, 15 | CommonModule, 16 | NgxsModule.forFeature([AmountState, PriceState]), 17 | RouterModule.forChild([{ path: '', component: AmountComponent }]) 18 | ] 19 | }) 20 | export class AmountModule {} 21 | -------------------------------------------------------------------------------- /integration/app/src/examples/amount/amount.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State } from '@ngxs/store'; 3 | import { Computed, StateRepository } from '@ngxs-labs/data/decorators'; 4 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 5 | 6 | import { PriceState } from './price.state'; 7 | 8 | @StateRepository() 9 | @State({ 10 | name: 'amount', 11 | defaults: 20 12 | }) 13 | @Injectable() 14 | export class AmountState extends NgxsDataRepository { 15 | constructor(private readonly price: PriceState) { 16 | super(); 17 | } 18 | 19 | @Computed() 20 | public get sum(): number { 21 | return this.snapshot + this.price.snapshot; 22 | } 23 | 24 | public setAmount(value: string): void { 25 | this.setState(parseFloat(value)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /integration/app/src/examples/amount/price.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State } from '@ngxs/store'; 3 | import { StateRepository } from '@ngxs-labs/data/decorators'; 4 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 5 | 6 | @StateRepository() 7 | @State({ 8 | name: 'price', 9 | defaults: 10 10 | }) 11 | @Injectable() 12 | export class PriceState extends NgxsDataRepository { 13 | public setPrice(value: string): void { 14 | this.setState(parseFloat(value)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/article-entities.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { createEntityCollections } from '@angular-ru/common/entity'; 3 | import { State } from '@ngxs/store'; 4 | import { Persistence, StateRepository } from '@ngxs-labs/data/decorators'; 5 | import { NgxsDataEntityCollectionsRepository } from '@ngxs-labs/data/repositories'; 6 | 7 | import { Article } from './article'; 8 | 9 | @Persistence({ 10 | existingEngine: localStorage 11 | }) 12 | @StateRepository() 13 | @State({ 14 | name: 'articles', 15 | defaults: createEntityCollections() 16 | }) 17 | @Injectable() 18 | export class ArticleEntitiesState extends NgxsDataEntityCollectionsRepository { 19 | public primaryKey: string = 'uid'; 20 | } 21 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/article.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 |
UID{{ articleEntities.entities![id]?.uid }}Title{{ articleEntities.entities![id]?.title }}Category{{ articleEntities.entities![id]?.category }} 31 | 32 | 33 |
39 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/article.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import { Sort } from '@angular/material/sort'; 4 | import { isNotNil } from '@angular-ru/common/utils'; 5 | import { Observable } from 'rxjs'; 6 | import { filter } from 'rxjs/operators'; 7 | 8 | import { generateUid } from '../../utils/generate-uid'; 9 | import { Article } from './article'; 10 | import { ArticleEntitiesState } from './article-entities.state'; 11 | import { ArticleDialogComponent } from './dialog/article-dialog.component'; 12 | 13 | @Component({ 14 | selector: 'article', 15 | templateUrl: './article.component.html', 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class ArticleComponent { 19 | constructor(public dialog: MatDialog, public articleEntities: ArticleEntitiesState) {} 20 | 21 | public createArticle(): void { 22 | this.ensureDialog({ uid: generateUid(), title: '', category: '' }).subscribe((article: Article): void => 23 | this.articleEntities.addOne(article) 24 | ); 25 | } 26 | 27 | public editById(id: string): void { 28 | const entity: Article = this.articleEntities.selectOne(id)!; 29 | this.ensureDialog(entity).subscribe((article: Article): void => 30 | this.articleEntities.updateOne({ id, changes: article }) 31 | ); 32 | } 33 | 34 | public deleteById(id: string): void { 35 | this.articleEntities.removeOne(id); 36 | } 37 | 38 | public sortData(event: Sort): void { 39 | this.articleEntities.sort({ sortBy: event.active as keyof Article, sortByOrder: event.direction }); 40 | } 41 | 42 | private ensureDialog(entity: Article): Observable
{ 43 | return this.dialog 44 | .open(ArticleDialogComponent, { 45 | width: '300px', 46 | disableClose: true, 47 | data: entity 48 | }) 49 | .afterClosed() 50 | .pipe(filter((article?: Article): article is Article => isNotNil(article))); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { MatSortModule } from '@angular/material/sort'; 9 | import { MatTableModule } from '@angular/material/table'; 10 | import { RouterModule } from '@angular/router'; 11 | import { NgxsModule } from '@ngxs/store'; 12 | 13 | import { ArticleComponent } from './article.component'; 14 | import { ArticleEntitiesState } from './article-entities.state'; 15 | import { ArticleDialogComponent } from './dialog/article-dialog.component'; 16 | 17 | @NgModule({ 18 | declarations: [ArticleComponent, ArticleDialogComponent], 19 | entryComponents: [ArticleDialogComponent], 20 | imports: [ 21 | CommonModule, 22 | MatDialogModule, 23 | MatButtonModule, 24 | ReactiveFormsModule, 25 | MatTableModule, 26 | MatInputModule, 27 | MatIconModule, 28 | NgxsModule.forFeature([ArticleEntitiesState]), 29 | RouterModule.forChild([{ path: '', component: ArticleComponent }]), 30 | MatSortModule 31 | ] 32 | }) 33 | export class ArticleModule {} 34 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/article.ts: -------------------------------------------------------------------------------- 1 | export interface Article { 2 | uid: string; 3 | title: string; 4 | category: string; 5 | } 6 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/dialog/article-dialog.component.html: -------------------------------------------------------------------------------- 1 |

Article

2 | 3 |
4 |
5 | 6 | Article title 7 | 8 | 9 |
10 | 11 |
12 | 13 | Article category 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /integration/app/src/examples/article/dialog/article-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | 5 | import { Article } from '../article'; 6 | 7 | @Component({ 8 | selector: 'article-dialog', 9 | templateUrl: './article-dialog.component.html', 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class ArticleDialogComponent { 13 | public form: FormGroup; 14 | 15 | constructor( 16 | public dialogRef: MatDialogRef, 17 | @Inject(MAT_DIALOG_DATA) public data: Article, 18 | private readonly fb: FormBuilder 19 | ) { 20 | this.form = this.fb.group({ 21 | uid: this.fb.control(data.uid, [Validators.required]), 22 | title: this.fb.control(data.title, [Validators.required]), 23 | category: this.fb.control(data.category, [Validators.required]) 24 | }); 25 | } 26 | 27 | public cancel(): void { 28 | this.dialogRef.close(null); 29 | } 30 | 31 | public save(): void { 32 | this.dialogRef.close(this.form.getRawValue()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count-model.ts: -------------------------------------------------------------------------------- 1 | export interface CountModel { 2 | val: number; 3 | } 4 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count-sub.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State } from '@ngxs/store'; 3 | import { DataAction, Debounce, Named, Payload, Persistence, StateRepository } from '@ngxs-labs/data/decorators'; 4 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 5 | 6 | import { CountModel } from './count-model'; 7 | 8 | @Persistence({ 9 | path: 'count.countSub.val', 10 | existingEngine: sessionStorage 11 | }) 12 | @StateRepository() 13 | @State({ 14 | name: 'countSub', 15 | defaults: { val: 100 } 16 | }) 17 | @Injectable() 18 | export class CountSubState extends NgxsImmutableDataRepository { 19 | @Debounce() 20 | @DataAction() 21 | public setDebounceSubValue(@Payload('value') @Named('val') val: number): void { 22 | this.ctx.setState({ val }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count.component.html: -------------------------------------------------------------------------------- 1 | 5 | stackblitz 10 | 11 | 12 |

CountState

13 | 14 | PS: CountSubState will be persistence in sessionStorage
15 | 16 |
Selection: 17 |
18 | 19 | counter.state$ = {{ counter.state$ | async | json }} 20 |
21 | 22 | counter.values$ = {{ counter.values$ | async | json }} 23 | 24 |

25 | 26 |
Actions: 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 | 36 | counter model: 37 | 38 | 39 | (delay 300ms) 40 | 41 |
42 |
43 | 44 | subCount model: 45 | 46 | 47 | (delay 300ms) 48 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { CountState } from './count.state'; 4 | import { CountSubState } from './count-sub.state'; 5 | 6 | @Component({ 7 | selector: 'count', 8 | templateUrl: './count.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class CountComponent { 12 | constructor(public counter: CountState, public subCount: CountSubState) {} 13 | } 14 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgxsModule } from '@ngxs/store'; 6 | 7 | import { CountComponent } from './count.component'; 8 | import { CountState } from './count.state'; 9 | import { CountSubState } from './count-sub.state'; 10 | 11 | @NgModule({ 12 | declarations: [CountComponent], 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | NgxsModule.forFeature([CountState, CountSubState]), 17 | RouterModule.forChild([{ path: '', component: CountComponent }]) 18 | ] 19 | }) 20 | export class CountModule {} 21 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/count.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Immutable } from '@angular-ru/common/typings'; 3 | import { State, StateToken } from '@ngxs/store'; 4 | import { Computed, DataAction, Debounce, Payload, StateRepository } from '@ngxs-labs/data/decorators'; 5 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 6 | import { Observable } from 'rxjs'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | import { CountModel } from './count-model'; 10 | import { CountSubState } from './count-sub.state'; 11 | import { ParentCountModel } from './parent-count-model'; 12 | 13 | const COUNT_TOKEN: StateToken = new StateToken('count'); 14 | 15 | @StateRepository() 16 | @State({ 17 | name: COUNT_TOKEN, 18 | defaults: { val: 0 }, 19 | children: [CountSubState] 20 | }) 21 | @Injectable() 22 | export class CountState extends NgxsImmutableDataRepository { 23 | @Computed() 24 | public get values$(): Observable { 25 | return this.state$.pipe(map((state: Immutable): CountModel | undefined => state.countSub)); 26 | } 27 | 28 | @DataAction() 29 | public increment(): void { 30 | this.ctx.setState( 31 | (state: Immutable): Immutable => ({ ...state, val: state.val + 1 }) 32 | ); 33 | } 34 | 35 | @DataAction() 36 | public countSubIncrement(): void { 37 | this.ctx.setState( 38 | (state: Immutable): Immutable => ({ 39 | ...state, 40 | countSub: { val: state.countSub!.val + 1 } 41 | }) 42 | ); 43 | } 44 | 45 | @DataAction() 46 | public decrement(): void { 47 | this.setState( 48 | (state: Immutable): Immutable => ({ ...state, val: state.val - 1 }) 49 | ); 50 | } 51 | 52 | @Debounce() 53 | @DataAction() 54 | public setDebounceValue(@Payload('val') val: string | number): void { 55 | this.ctx.setState( 56 | (state: Immutable): Immutable => ({ 57 | ...state, 58 | val: parseFloat(val as string) || 0 59 | }) 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integration/app/src/examples/count/parent-count-model.ts: -------------------------------------------------------------------------------- 1 | import { CountModel } from './count-model'; 2 | 3 | export interface ParentCountModel { 4 | val: number; 5 | countSub?: CountModel; 6 | } 7 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person-model.ts: -------------------------------------------------------------------------------- 1 | export interface PersonModel { 2 | title: string; 3 | description: string; 4 | } 5 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.component.html: -------------------------------------------------------------------------------- 1 | 5 | stackblitz 10 | 11 | 12 |

Person state

13 | 14 |
15 |

{{ person.title }}

16 |

{{ person.description }}

17 |
18 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { PersonState } from './person.state'; 4 | 5 | @Component({ 6 | selector: 'person', 7 | templateUrl: './person.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class PersonComponent { 11 | constructor(public person: PersonState) {} 12 | } 13 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NgModule } from '@angular/core'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgxsModule } from '@ngxs/store'; 6 | 7 | import { PersonComponent } from './person.component'; 8 | import { PersonResolver } from './person.resolver'; 9 | import { PersonService } from './person.service'; 10 | import { PersonState } from './person.state'; 11 | 12 | @NgModule({ 13 | declarations: [PersonComponent], 14 | imports: [ 15 | CommonModule, 16 | HttpClientModule, 17 | NgxsModule.forFeature([PersonState]), 18 | RouterModule.forChild([ 19 | { 20 | path: '', 21 | component: PersonComponent, 22 | resolve: { 23 | content: PersonResolver 24 | } 25 | } 26 | ]) 27 | ], 28 | providers: [PersonService, PersonResolver] 29 | }) 30 | export class PersonModule {} 31 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Resolve } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { PersonState } from './person.state'; 6 | import { PersonModel } from './person-model'; 7 | 8 | @Injectable() 9 | export class PersonResolver implements Resolve { 10 | constructor(private readonly personState: PersonState) {} 11 | 12 | public resolve(): Observable { 13 | return this.personState.getContent(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { PersonModel } from './person-model'; 7 | 8 | @Injectable() 9 | export class PersonService { 10 | constructor(private readonly httpService: HttpClient) {} 11 | 12 | public fetchAll(): Observable { 13 | return this.httpService 14 | .get<{ data: PersonModel }>('./app/json/person.json') 15 | .pipe(map((response: { data: PersonModel }): PersonModel => response.data)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /integration/app/src/examples/person/person.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State } from '@ngxs/store'; 3 | import { StateRepository } from '@ngxs-labs/data/decorators'; 4 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 5 | import { Observable } from 'rxjs'; 6 | import { tap } from 'rxjs/operators'; 7 | 8 | import { PersonService } from './person.service'; 9 | import { PersonModel } from './person-model'; 10 | 11 | @StateRepository() 12 | @State({ 13 | name: 'person', 14 | defaults: { title: null!, description: null! } 15 | }) 16 | @Injectable() 17 | export class PersonState extends NgxsImmutableDataRepository { 18 | constructor(private readonly personService: PersonService) { 19 | super(); 20 | } 21 | 22 | public getContent(): Observable { 23 | return this.personService.fetchAll().pipe(tap((content: PersonModel): void => this.setState(content))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /integration/app/src/examples/todo/todo.component.html: -------------------------------------------------------------------------------- 1 | 5 | stackblitz 10 | 11 | 12 |

TodoState

13 | 14 | PS: TodoState will be persistence in localStorage (ttl: 30sec) 15 | 16 |
17 | 18 | 19 | 20 |
21 |
    22 |
  • 23 | {{ todoItem }} 24 |
  • 25 |
26 | -------------------------------------------------------------------------------- /integration/app/src/examples/todo/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { TodoState } from './todo.state'; 4 | 5 | @Component({ 6 | selector: 'todo', 7 | templateUrl: './todo.component.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class TodoComponent { 11 | constructor(public todo: TodoState) {} 12 | } 13 | -------------------------------------------------------------------------------- /integration/app/src/examples/todo/todo.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NgxsModule } from '@ngxs/store'; 6 | 7 | import { TodoComponent } from './todo.component'; 8 | import { TodoState } from './todo.state'; 9 | 10 | @NgModule({ 11 | declarations: [TodoComponent], 12 | imports: [ 13 | CommonModule, 14 | MatSnackBarModule, 15 | NgxsModule.forFeature([TodoState]), 16 | RouterModule.forChild([{ path: '', component: TodoComponent }]) 17 | ] 18 | }) 19 | export class TodoModule {} 20 | -------------------------------------------------------------------------------- /integration/app/src/examples/todo/todo.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { Immutable } from '@angular-ru/common/typings'; 4 | import { State } from '@ngxs/store'; 5 | import { DataAction, Payload, Persistence, StateRepository } from '@ngxs-labs/data/decorators'; 6 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 7 | import { 8 | NgxsDataAfterExpired, 9 | NgxsDataAfterStorageEvent, 10 | NgxsDataExpiredEvent, 11 | NgxsDataStorageEvent, 12 | PersistenceProvider 13 | } from '@ngxs-labs/data/typings'; 14 | import { Subject } from 'rxjs'; 15 | 16 | @Persistence({ 17 | ttlDelay: 5000, 18 | fireInit: false, 19 | ttl: 30000, // 30 * 1000 = 30sec, 20 | existingEngine: localStorage 21 | }) 22 | @StateRepository() 23 | @State({ 24 | name: 'todo', 25 | defaults: [] 26 | }) 27 | @Injectable() 28 | export class TodoState 29 | extends NgxsImmutableDataRepository 30 | implements NgxsDataAfterExpired, NgxsDataAfterStorageEvent 31 | { 32 | public expired$: Subject = new Subject(); 33 | 34 | constructor(private readonly snackBar: MatSnackBar) { 35 | super(); 36 | } 37 | 38 | public ngxsDataAfterExpired(event: NgxsDataExpiredEvent, _provider: PersistenceProvider): void { 39 | this.snackBar.open('Expired', event.key, { 40 | duration: 5000, 41 | verticalPosition: 'top', 42 | horizontalPosition: 'right' 43 | }); 44 | } 45 | 46 | public ngxsDataAfterStorageEvent(event: NgxsDataStorageEvent): void { 47 | // eslint-disable-next-line no-console 48 | console.log('event', event); 49 | } 50 | 51 | @DataAction() 52 | public addTodo(@Payload('todo') todo: string): void { 53 | if (todo) { 54 | this.ctx.setState((state: Immutable): Immutable => state.concat(todo)); 55 | } 56 | } 57 | 58 | @DataAction() 59 | public removeTodo(@Payload('idx') idx: number): void { 60 | this.ctx.setState( 61 | (state: Immutable): Immutable => 62 | state.filter((_: string, index: number): boolean => index !== idx) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /integration/app/src/utils/generate-uid.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-magic-numbers */ 2 | export function generateUid(): string { 3 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c: string): string { 4 | const r: number = (Math.random() * 16) | 0; 5 | const v: number = c === 'x' ? r : (r & 0x3) | 0x8; 6 | return v.toString(16); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /integration/app/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/icon?family=Material+Icons'); 2 | @import '@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | 4 | html { 5 | height: 100%; 6 | width: 100%; 7 | display: flex; 8 | background: #f5f5f5; 9 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 10 | line-height: 1.4em; 11 | color: #4d4d4d; 12 | margin: 0 auto; 13 | -webkit-font-smoothing: antialiased; 14 | font-weight: 300; 15 | } 16 | 17 | body { 18 | width: calc(100% - 50px); 19 | margin: auto; 20 | padding-left: 50px; 21 | } 22 | 23 | table { 24 | width: 100%; 25 | table-layout: fixed; 26 | } 27 | 28 | th, 29 | td { 30 | overflow: hidden; 31 | width: 200px; 32 | text-overflow: ellipsis; 33 | white-space: nowrap; 34 | } 35 | ul, 36 | li { 37 | padding: 0; 38 | margin: 0; 39 | list-style: none; 40 | } 41 | 42 | button { 43 | background: none; 44 | } 45 | 46 | .add-todo { 47 | position: relative; 48 | padding: 1em; 49 | display: flex; 50 | 51 | input { 52 | display: inline-flex; 53 | width: 100%; 54 | margin-right: 5px; 55 | } 56 | } 57 | 58 | .container { 59 | position: relative; 60 | display: flex; 61 | background: #fff; 62 | font-size: 20px; 63 | margin: auto; 64 | min-height: 40vh; 65 | border: 1px solid rgba(0, 0, 0, 0.2); 66 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); 67 | min-width: 900px; 68 | } 69 | 70 | .content { 71 | padding: 0.5em 1em; 72 | width: 100%; 73 | box-sizing: border-box; 74 | } 75 | 76 | .my-code { 77 | cursor: help; 78 | } 79 | 80 | .title { 81 | margin-right: 10px; 82 | } 83 | 84 | .stackblitz { 85 | opacity: 0.5; 86 | position: absolute; 87 | right: 5px; 88 | width: 35px; 89 | top: 5px; 90 | cursor: pointer; 91 | transition: opacity 0.3s; 92 | 93 | &:hover { 94 | opacity: 1; 95 | } 96 | } 97 | 98 | .panel { 99 | overflow: auto; 100 | padding: 20px; 101 | font-size: 12px; 102 | position: absolute; 103 | left: 0; 104 | top: 0; 105 | height: calc(100% - 40px); 106 | min-width: 210px; 107 | background-color: #fff; 108 | border-right: 1px solid rgba(0, 0, 0, 0.2); 109 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); 110 | z-index: 100; 111 | 112 | .logo { 113 | margin-top: 20px; 114 | width: 180px; 115 | } 116 | 117 | .panel-content { 118 | font-size: 14px; 119 | padding: 10px; 120 | } 121 | } 122 | 123 | .link { 124 | margin-right: 10px; 125 | } 126 | 127 | .todo { 128 | display: block; 129 | position: relative; 130 | text-align: center; 131 | padding: 1em; 132 | margin: 0 auto; 133 | border-top: solid 1px #ddd; 134 | } 135 | 136 | .menu { 137 | margin-left: 10px; 138 | min-width: 900px; 139 | a { 140 | margin-right: 5px; 141 | } 142 | 143 | .menu-top { 144 | margin-bottom: 10px; 145 | } 146 | } 147 | 148 | .button-actions { 149 | button { 150 | margin-right: 10px; 151 | } 152 | } 153 | 154 | .ng-icon { 155 | position: absolute; 156 | margin-left: 10px; 157 | margin-top: 0; 158 | 159 | .img { 160 | height: 16px; 161 | display: block; 162 | float: left; 163 | margin-right: 10px; 164 | } 165 | } 166 | 167 | .menu, 168 | .container { 169 | max-width: 800px; 170 | margin: auto; 171 | } 172 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/counter-lifecycle.spec.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { AfterViewInit, ApplicationRef, Component, Injectable, NgModule, OnInit } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { BrowserModule, ɵBrowserDomAdapter as BrowserDomAdapter } from '@angular/platform-browser'; 5 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 6 | import { StateRepository } from '@ngxs-labs/data/decorators'; 7 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 8 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 9 | import { NgxsAfterBootstrap, NgxsModule, NgxsOnInit, State, Store } from '@ngxs/store'; 10 | 11 | describe('Complex lifecycle', () => { 12 | @Injectable() 13 | class MyApiService {} 14 | 15 | it('should be throw when use context before app initial', () => { 16 | @Injectable() 17 | @StateRepository() 18 | @State({ 19 | name: 'count', 20 | defaults: 0 21 | }) 22 | class CountState extends NgxsImmutableDataRepository { 23 | public value: number | null = null; 24 | constructor(public myService: MyApiService) { 25 | super(); 26 | this.value = 1; 27 | this.ctx.setState(this.value); 28 | } 29 | } 30 | 31 | TestBed.configureTestingModule({ 32 | imports: [NgxsModule.forRoot([CountState]), NgxsDataPluginModule.forRoot()], 33 | providers: [MyApiService] 34 | }); 35 | 36 | try { 37 | TestBed.inject(CountState); 38 | TestBed.inject(Store); 39 | } catch (e) { 40 | expect(e.message).toEqual(NGXS_DATA_EXCEPTIONS.NGXS_DATA_MODULE_EXCEPTION); 41 | } 42 | }); 43 | 44 | it('should be correct lifecycle', () => { 45 | const hooks: string[] = []; 46 | 47 | @Injectable() 48 | @StateRepository() 49 | @State({ 50 | name: 'count', 51 | defaults: 0 52 | }) 53 | class CountState extends NgxsImmutableDataRepository implements NgxsOnInit, NgxsAfterBootstrap { 54 | constructor(public myService: MyApiService) { 55 | super(); 56 | hooks.push('CountState - create'); 57 | } 58 | 59 | public ngxsOnInit(): void { 60 | hooks.push('CountState - ngxsOnInit'); 61 | } 62 | 63 | public ngxsAfterBootstrap(): void { 64 | hooks.push('CountState - ngxsAfterBootstrap'); 65 | } 66 | } 67 | 68 | @Component({ 69 | selector: 'app-root', 70 | template: '' 71 | }) 72 | class NgxsTestComponent implements OnInit, AfterViewInit { 73 | public ngOnInit(): void { 74 | hooks.push('NgxsTestComponent - ngOnInit'); 75 | } 76 | public ngAfterViewInit(): void { 77 | hooks.push('NgxsTestComponent - ngAfterViewInit'); 78 | } 79 | } 80 | 81 | @NgModule({ 82 | imports: [BrowserModule], 83 | declarations: [NgxsTestComponent], 84 | entryComponents: [NgxsTestComponent] 85 | }) 86 | class AppTestModule { 87 | public static ngDoBootstrap(app: ApplicationRef): void { 88 | AppTestModule.createRootNode(); 89 | app.bootstrap(NgxsTestComponent); 90 | } 91 | 92 | private static createRootNode(selector = 'app-root'): void { 93 | const document = TestBed.inject(DOCUMENT); 94 | const adapter = new BrowserDomAdapter(); 95 | const root = adapter.createElement(selector); 96 | document.body.appendChild(root); 97 | } 98 | } 99 | 100 | TestBed.configureTestingModule({ 101 | imports: [AppTestModule, NgxsModule.forRoot([CountState]), NgxsDataPluginModule.forRoot()], 102 | providers: [MyApiService] 103 | }); 104 | 105 | AppTestModule.ngDoBootstrap(TestBed.inject(ApplicationRef)); 106 | 107 | expect(hooks).toEqual([ 108 | 'CountState - create', 109 | 'CountState - ngxsOnInit', 110 | 'NgxsTestComponent - ngOnInit', 111 | 'NgxsTestComponent - ngAfterViewInit', 112 | 'CountState - ngxsAfterBootstrap' 113 | ]); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/debounce.spec.ts: -------------------------------------------------------------------------------- 1 | import { DataAction, Debounce, StateRepository } from '@ngxs-labs/data/decorators'; 2 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 3 | import { Injectable } from '@angular/core'; 4 | import { NgxsModule, State } from '@ngxs/store'; 5 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 6 | import { fakeAsync, TestBed, tick } from '@angular/core/testing'; 7 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 8 | 9 | describe('[TEST]: Debounce', () => { 10 | it('should be check ngZone', () => { 11 | let message: string | null = null; 12 | try { 13 | class App { 14 | @Debounce() 15 | public invoke(): void {} 16 | } 17 | 18 | new App().invoke(); 19 | } catch (e) { 20 | message = e.message; 21 | } 22 | 23 | expect(message).toEqual(NGXS_DATA_EXCEPTIONS.NGXS_DATA_MODULE_EXCEPTION); 24 | }); 25 | 26 | it('should be correct invoke', fakeAsync(() => { 27 | const spy = jest.spyOn(console, 'warn').mockImplementation(); 28 | 29 | @StateRepository() 30 | @State({ name: 'count', defaults: 0 }) 31 | @Injectable() 32 | class DebounceState extends NgxsImmutableDataRepository { 33 | @Debounce() 34 | @DataAction() 35 | public increment(): void { 36 | this.setState((state: number): number => ++state); 37 | } 38 | 39 | @Debounce(50) 40 | @DataAction() 41 | public decrement(): void { 42 | this.setState((state: number): number => --state); 43 | } 44 | 45 | @Debounce(50) 46 | @DataAction() 47 | public incrementByValue(val: number): number { 48 | this.setState((state: number): number => state + val); 49 | return this.getState(); 50 | } 51 | } 52 | 53 | TestBed.configureTestingModule({ 54 | imports: [NgxsModule.forRoot([DebounceState]), NgxsDataPluginModule.forRoot()] 55 | }); 56 | 57 | const state: DebounceState = TestBed.inject(DebounceState); 58 | 59 | expect(state.getState()).toEqual(0); 60 | 61 | state.increment(); 62 | state.increment(); 63 | state.increment(); 64 | state.increment(); 65 | state.increment(); 66 | state.increment(); 67 | state.increment(); 68 | 69 | expect(state.getState()).toEqual(0); 70 | 71 | tick(300); 72 | 73 | expect(state.getState()).toEqual(1); 74 | 75 | state.decrement(); 76 | state.decrement(); 77 | state.decrement(); 78 | state.decrement(); 79 | 80 | expect(state.getState()).toEqual(1); 81 | 82 | tick(50); 83 | 84 | expect(state.getState()).toEqual(0); 85 | 86 | state.decrement(); 87 | state.decrement(); 88 | state.decrement(); 89 | state.decrement(); 90 | 91 | expect(state.getState()).toEqual(0); 92 | 93 | tick(50); 94 | 95 | expect(state.getState()).toEqual(-1); 96 | 97 | const val1 = state.incrementByValue(6); 98 | const val2 = state.incrementByValue(6); 99 | const val3 = state.incrementByValue(6); 100 | 101 | tick(50); 102 | 103 | expect(val1).toEqual(undefined); 104 | expect(val2).toEqual(undefined); 105 | expect(val3).toEqual(undefined); 106 | 107 | expect(spy).toHaveBeenCalledTimes(1); 108 | expect(console.warn).toHaveBeenLastCalledWith(NGXS_DATA_EXCEPTIONS.NGXS_DATA_ASYNC_ACTION_RETURN_TYPE, -1); 109 | })); 110 | }); 111 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/deep-data-action-type.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { fakeAsync, TestBed, tick } from '@angular/core/testing'; 3 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 4 | import { DataAction, Payload, StateRepository } from '@ngxs-labs/data/decorators'; 5 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 6 | import { Actions, NgxsModule, State } from '@ngxs/store'; 7 | import { take } from 'rxjs/operators'; 8 | 9 | describe('[TEST]: Deep data action type', () => { 10 | it('should use deeply nested @DataAction() names', fakeAsync(() => { 11 | @StateRepository() 12 | @State({ name: 'c', defaults: { prop: undefined } }) 13 | @Injectable() 14 | class C extends NgxsDataRepository<{ prop: string }> { 15 | @DataAction() 16 | public setProp(@Payload('prop') prop: string) { 17 | this.ctx.setState({ prop }); 18 | } 19 | } 20 | 21 | @StateRepository() 22 | @State({ name: 'b', defaults: { prop: undefined }, children: [C] }) 23 | @Injectable() 24 | class B extends NgxsDataRepository<{ prop: string }> { 25 | @DataAction() 26 | public setProp(@Payload('prop') prop: string) { 27 | this.ctx.setState({ prop }); 28 | } 29 | } 30 | 31 | @StateRepository() 32 | @State({ name: 'a', defaults: { prop: undefined }, children: [B] }) 33 | @Injectable() 34 | class A extends NgxsDataRepository<{ prop: string }> { 35 | @DataAction() 36 | public setProp(@Payload('prop') prop: string) { 37 | this.ctx.setState({ prop }); 38 | } 39 | } 40 | 41 | TestBed.configureTestingModule({ 42 | imports: [NgxsModule.forRoot([A, B, C]), NgxsDataPluginModule.forRoot()] 43 | }); 44 | 45 | const stateA: A = TestBed.inject(A); 46 | expect(stateA.getState()).toEqual({ 47 | prop: undefined, 48 | b: { 49 | prop: undefined, 50 | c: { prop: undefined } 51 | } 52 | }); 53 | 54 | const stateB: B = TestBed.inject(B); 55 | expect(stateB.getState()).toEqual({ 56 | prop: undefined, 57 | c: { prop: undefined } 58 | }); 59 | 60 | const stateC: C = TestBed.inject(C); 61 | expect(stateC.getState()).toEqual({ 62 | prop: undefined 63 | }); 64 | 65 | const actions: any[] = []; 66 | const actions$: Actions = TestBed.inject(Actions); 67 | actions$.pipe(take(6)).subscribe((action) => actions.push(action)); 68 | 69 | stateA.setProp('A'); 70 | stateB.setProp('B'); 71 | stateC.setProp('C'); 72 | 73 | tick(100); 74 | 75 | expect(actions[0].action.constructor.type).toEqual('@a.setProp(prop)'); 76 | expect(actions[2].action.constructor.type).toEqual('@a/b.setProp(prop)'); 77 | expect(actions[4].action.constructor.type).toEqual('@a/b/c.setProp(prop)'); 78 | 79 | expect(stateA.snapshot).toEqual({ 80 | prop: 'A', 81 | b: { 82 | prop: 'B', 83 | c: { prop: 'C' } 84 | } 85 | }); 86 | })); 87 | 88 | afterEach(() => { 89 | TestBed.resetTestingModule(); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/name-creator.spec.ts: -------------------------------------------------------------------------------- 1 | import { actionNameCreator, MethodArgsRegistry } from '@ngxs-labs/data/internals'; 2 | 3 | describe('[TEST]: actionNameCreator', () => { 4 | it('should be correct', () => { 5 | expect(actionNameCreator({ statePath: 'A', methodName: 'a', argumentsNames: [] })).toEqual('@A.a()'); 6 | 7 | expect(actionNameCreator({ statePath: 'A', methodName: 'a', argumentsNames: ['x', 'y', 'z'] })).toEqual( 8 | '@A.a($arg0, $arg1, $arg2)' 9 | ); 10 | }); 11 | 12 | it('should be correct create nested statePath', () => { 13 | expect(actionNameCreator({ statePath: 'A.B.C', methodName: 'a', argumentsNames: [] })).toEqual('@A/B/C.a()'); 14 | expect(actionNameCreator({ statePath: 'A.B', methodName: 'a', argumentsNames: ['x', 'y', 'z'] })).toEqual( 15 | '@A/B.a($arg0, $arg1, $arg2)' 16 | ); 17 | }); 18 | 19 | it('should be correct create payload type by payload and name', () => { 20 | const registry: MethodArgsRegistry = new MethodArgsRegistry(); 21 | registry.createPayloadType('X', 'a', 0); 22 | registry.createArgumentName('Z', 'a', 2); 23 | 24 | expect( 25 | actionNameCreator({ 26 | statePath: 'A', 27 | methodName: 'a', 28 | argumentsNames: ['x', 'y', 'z'], 29 | argumentRegistry: registry 30 | }) 31 | ).toEqual('@A.a(X, $arg1, Z)'); 32 | }); 33 | 34 | it('should be correct create payload type when override type by name', () => { 35 | const registry: MethodArgsRegistry = new MethodArgsRegistry(); 36 | registry.createPayloadType('X', 'a', 0); 37 | registry.createArgumentName('_X_', 'a', 0); 38 | registry.createArgumentName('Z', 'a', 2); 39 | 40 | expect( 41 | actionNameCreator({ 42 | statePath: 'A', 43 | methodName: 'a', 44 | argumentsNames: ['x', 'y', 'z'], 45 | argumentRegistry: registry 46 | }) 47 | ).toEqual('@A.a(_X_, $arg1, Z)'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/ngxs-meta.spec.ts: -------------------------------------------------------------------------------- 1 | import { State } from '@ngxs/store'; 2 | import { StateRepository } from '@ngxs-labs/data/decorators'; 3 | import { Injectable } from '@angular/core'; 4 | import { NGXS_ARGUMENT_REGISTRY_META, NGXS_DATA_META, NGXS_META_KEY } from '@ngxs-labs/data/tokens'; 5 | import { DataStateClass } from '@ngxs-labs/data/typings'; 6 | 7 | describe('[TEST]: NGXS_META', () => { 8 | it('DataStateClass', () => { 9 | @StateRepository() 10 | @State({ name: 'app' }) 11 | @Injectable() 12 | class AppState {} 13 | 14 | expect(NGXS_META_KEY).toEqual('NGXS_META'); 15 | expect(NGXS_DATA_META).toEqual('NGXS_DATA_META'); 16 | expect(NGXS_ARGUMENT_REGISTRY_META).toEqual('NGXS_ARGUMENT_REGISTRY_META'); 17 | 18 | expect((AppState as DataStateClass)[NGXS_META_KEY]).toEqual({ 19 | name: 'app', 20 | actions: {}, 21 | defaults: undefined, 22 | path: null, 23 | makeRootSelector: expect.any(Function), 24 | children: undefined 25 | }); 26 | 27 | expect((AppState as DataStateClass)[NGXS_DATA_META]).toEqual({ 28 | stateMeta: { 29 | name: 'app', 30 | actions: {}, 31 | defaults: undefined, 32 | path: null, 33 | makeRootSelector: expect.any(Function), 34 | children: undefined 35 | }, 36 | operations: {}, 37 | stateClass: AppState 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/registration.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, Injectable, OnInit } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 4 | import { DataAction, StateRepository } from '@ngxs-labs/data/decorators'; 5 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 6 | import { Any } from '@angular-ru/common/typings'; 7 | import { NgxsModule, Select, State, Store } from '@ngxs/store'; 8 | import { Observable } from 'rxjs'; 9 | 10 | describe('Check correct deep instance', () => { 11 | let component: AppComponent; 12 | let store: Store; 13 | let fixture: ComponentFixture; 14 | 15 | interface IFormState { 16 | dirty?: boolean; 17 | model: Any; 18 | } 19 | 20 | class FormState { 21 | public dirty?: boolean = false; 22 | public model: Any = undefined; 23 | } 24 | 25 | interface IRegistrationStateModel { 26 | address: IFormState; 27 | } 28 | 29 | class RegistrationStateModel { 30 | public address: FormState = new FormState(); 31 | } 32 | 33 | @StateRepository() 34 | @State({ 35 | name: 'registration', 36 | defaults: new RegistrationStateModel() 37 | }) 38 | @Injectable() 39 | class RegistrationState extends NgxsImmutableDataRepository { 40 | @Select((state: Any) => state.registration) 41 | public address$!: Observable; 42 | 43 | @DataAction() 44 | public addAddress(address: IFormState) { 45 | return this.ctx.setState(() => ({ address })); 46 | } 47 | } 48 | 49 | @Component({ 50 | selector: 'my-app', 51 | template: '' 52 | }) 53 | class AppComponent implements OnInit { 54 | public name = 'Angular + NGXS'; 55 | public result: Any = null; 56 | 57 | constructor(private readonly registration: RegistrationState) {} 58 | 59 | public ngOnInit() { 60 | this.result = this.registration.addAddress({ dirty: true, model: { hello: 'world' } }); 61 | } 62 | } 63 | 64 | beforeAll(() => { 65 | TestBed.configureTestingModule({ 66 | imports: [NgxsModule.forRoot([RegistrationState]), NgxsDataPluginModule.forRoot()], 67 | declarations: [AppComponent] 68 | }); 69 | 70 | fixture = TestBed.createComponent(AppComponent); 71 | component = fixture.componentInstance; 72 | store = TestBed.inject(Store); 73 | }); 74 | 75 | it('should be correct ngOnInit', () => { 76 | expect(component.name).toEqual('Angular + NGXS'); 77 | expect(store.snapshot()).toEqual({ registration: { address: { dirty: false } } }); 78 | 79 | component.ngOnInit(); 80 | 81 | expect(store.snapshot()).toEqual({ registration: { address: { dirty: true, model: { hello: 'world' } } } }); 82 | expect(component.result).toEqual(undefined); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /integration/tests/common-extensions/zone.spec.ts: -------------------------------------------------------------------------------- 1 | import { ngxsTestingPlatform } from '@ngxs-labs/data/testing'; 2 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 3 | import { Injectable, NgZone } from '@angular/core'; 4 | import { State } from '@ngxs/store'; 5 | import { DataAction, StateRepository } from '@ngxs-labs/data/decorators'; 6 | 7 | describe('[TEST]: Zone', () => { 8 | @StateRepository() 9 | @State({ 10 | name: 'counter', 11 | defaults: 0 12 | }) 13 | @Injectable() 14 | class CounterState extends NgxsDataRepository { 15 | public inside: number = 0; 16 | public outside: number = 0; 17 | 18 | @DataAction() 19 | public increment(): number { 20 | return this._increment(); 21 | } 22 | 23 | @DataAction({ insideZone: true }) 24 | public incrementInZone(): number { 25 | return this._increment(); 26 | } 27 | 28 | public incrementWithoutAction(): number { 29 | return this._increment(); 30 | } 31 | 32 | private _increment(): number { 33 | if (NgZone.isInAngularZone()) { 34 | this.inside += 1; 35 | } else { 36 | this.outside += 1; 37 | } 38 | 39 | const newState: number = this.getState() + 1; 40 | this.ctx.setState(newState); 41 | return newState; 42 | } 43 | } 44 | 45 | it( 46 | 'should be works inside/outside zone', 47 | ngxsTestingPlatform([CounterState], (_store, state) => { 48 | expect(state.increment()).toEqual(1); 49 | 50 | expect(state.outside).toEqual(1); 51 | expect(state.inside).toEqual(0); 52 | expect(state.getState()).toEqual(1); 53 | 54 | expect(state.incrementWithoutAction()).toEqual(2); 55 | 56 | expect(state.outside).toEqual(2); 57 | expect(state.inside).toEqual(0); 58 | expect(state.getState()).toEqual(2); 59 | 60 | expect(state.incrementInZone()).toEqual(3); 61 | 62 | expect(state.outside).toEqual(2); 63 | expect(state.inside).toEqual(1); 64 | expect(state.getState()).toEqual(3); 65 | }) 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /integration/tests/entity/observables.spec.ts: -------------------------------------------------------------------------------- 1 | import { StateRepository } from '@ngxs-labs/data/decorators'; 2 | import { State } from '@ngxs/store'; 3 | import { createEntityCollections } from '@angular-ru/common/entity'; 4 | import { NgxsDataEntityCollectionsRepository } from '@ngxs-labs/data/repositories'; 5 | import { ngxsTestingPlatform } from '@ngxs-labs/data/testing'; 6 | import { Injectable } from '@angular/core'; 7 | 8 | describe('[TEST]: Entity observables', () => { 9 | describe('entityArray$', () => { 10 | interface StudentEntity { 11 | id: number; 12 | name: string; 13 | } 14 | 15 | @StateRepository() 16 | @State({ 17 | name: 'student', 18 | defaults: createEntityCollections() 19 | }) 20 | @Injectable() 21 | class StudentEntitiesState extends NgxsDataEntityCollectionsRepository {} 22 | 23 | it( 24 | 'correct create entity arrays', 25 | ngxsTestingPlatform([StudentEntitiesState], (_store, studentEntities) => { 26 | const entityArrayEvents: StudentEntity[][] = []; 27 | 28 | studentEntities.entitiesArray$.subscribe((entities) => { 29 | entityArrayEvents.push(entities); 30 | }); 31 | 32 | studentEntities.setAll([ 33 | { 34 | id: 1, 35 | name: 'Maxim' 36 | }, 37 | { 38 | id: 2, 39 | name: 'Ivan' 40 | }, 41 | { 42 | id: 3, 43 | name: 'Nikola' 44 | }, 45 | { 46 | id: 4, 47 | name: 'Petr' 48 | } 49 | ]); 50 | 51 | studentEntities.reset(); 52 | 53 | studentEntities.addOne({ 54 | id: 1, 55 | name: 'Maxim' 56 | }); 57 | 58 | studentEntities.removeAll(); 59 | 60 | const entity: StudentEntity = { 61 | id: 4, 62 | name: 'Mark' 63 | }; 64 | 65 | studentEntities.upsertOne(entity); 66 | 67 | studentEntities.removeByEntity(entity); 68 | 69 | expect(entityArrayEvents).toEqual([ 70 | [], 71 | [ 72 | { 73 | id: 1, 74 | name: 'Maxim' 75 | }, 76 | { 77 | id: 2, 78 | name: 'Ivan' 79 | }, 80 | { 81 | id: 3, 82 | name: 'Nikola' 83 | }, 84 | { 85 | id: 4, 86 | name: 'Petr' 87 | } 88 | ], 89 | [], 90 | [ 91 | { 92 | id: 1, 93 | name: 'Maxim' 94 | } 95 | ], 96 | [], 97 | [ 98 | { 99 | id: 4, 100 | name: 'Mark' 101 | } 102 | ], 103 | [] 104 | ]); 105 | }) 106 | ); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /integration/tests/entity/uuidv4.spec.ts: -------------------------------------------------------------------------------- 1 | import { generateUid } from '../../app/src/utils/generate-uid'; 2 | 3 | describe('[TEST]: Entity uuidv4', () => { 4 | it('should be correct generate uui', () => { 5 | expect(generateUid()).toEqual(expect.any(String)); 6 | expect(generateUid()).not.toEqual(generateUid()); 7 | expect(generateUid().length).toEqual(36); 8 | expect(generateUid().split('-')).toEqual([ 9 | expect.any(String), 10 | expect.any(String), 11 | expect.any(String), 12 | expect.any(String), 13 | expect.any(String) 14 | ]); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /integration/tests/reset-extensions/reset.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 4 | import { StateRepository } from '@ngxs-labs/data/decorators'; 5 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 6 | import { NgxsModule, State, Store } from '@ngxs/store'; 7 | 8 | describe('[TEST]: Reset', () => { 9 | let store: Store; 10 | 11 | @StateRepository() 12 | @State({ 13 | name: 'D' 14 | }) 15 | @Injectable() 16 | class D {} 17 | 18 | @StateRepository() 19 | @State({ 20 | name: 'C', 21 | children: [D] 22 | }) 23 | @Injectable() 24 | class C {} 25 | 26 | @StateRepository() 27 | @State({ 28 | name: 'B', 29 | children: [] 30 | }) 31 | @Injectable() 32 | class B {} 33 | 34 | @StateRepository() 35 | @State({ 36 | name: 'A', 37 | children: [B, C] 38 | }) 39 | @Injectable() 40 | class A extends NgxsImmutableDataRepository {} 41 | 42 | beforeEach(() => { 43 | TestBed.configureTestingModule({ 44 | imports: [NgxsModule.forRoot([A, B, C, D]), NgxsDataPluginModule.forRoot()] 45 | }).compileComponents(); 46 | 47 | store = TestBed.inject(Store); 48 | }); 49 | 50 | it('should be correct reset A state', () => { 51 | const a: A = TestBed.inject(A); 52 | expect(store.snapshot()).toEqual({ A: { C: { D: {} }, B: {} } }); 53 | expect(a.getState()).toEqual({ C: { D: {} }, B: {} }); 54 | 55 | a.reset(); 56 | 57 | expect(store.snapshot()).toEqual({ A: { C: { D: {} }, B: {} } }); 58 | }); 59 | 60 | it('should be throw when incorrect children', () => { 61 | let message: string | null = null; 62 | 63 | try { 64 | @State({ name: 'foo' }) 65 | @Injectable() 66 | class FooState {} 67 | 68 | @StateRepository() 69 | @State({ name: 'bar', defaults: 'string', children: [FooState] }) 70 | @Injectable() 71 | class BarState {} 72 | 73 | new BarState(); 74 | } catch (e) { 75 | message = e.message; 76 | } 77 | 78 | expect(message).toEqual('Child states can only be added to an object. Cannot convert String to PlainObject'); 79 | 80 | try { 81 | @State({ name: 'foo' }) 82 | @Injectable() 83 | class FooState {} 84 | 85 | @StateRepository() 86 | @State({ name: 'bar', defaults: [], children: [FooState] }) 87 | @Injectable() 88 | class BarState {} 89 | 90 | new BarState(); 91 | } catch (e) { 92 | message = e.message; 93 | } 94 | 95 | expect(message).toEqual('Child states can only be added to an object. Cannot convert Array to PlainObject'); 96 | 97 | try { 98 | @State({ name: 'foo' }) 99 | @Injectable() 100 | class FooState {} 101 | 102 | @StateRepository() 103 | @State({ name: 'bar', defaults: null, children: [FooState] }) 104 | @Injectable() 105 | class BarState {} 106 | 107 | new BarState(); 108 | } catch (e) { 109 | message = e.message; 110 | } 111 | 112 | expect(message).toEqual('Child states can only be added to an object. Cannot convert null to PlainObject'); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /integration/tests/setupJest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /integration/tests/testing/testing-v1.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgxsDataTestingModule } from '@ngxs-labs/data/testing'; 3 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 4 | import { Injectable } from '@angular/core'; 5 | import { DataAction, StateRepository } from '@ngxs-labs/data/decorators'; 6 | import { State } from '@ngxs/store'; 7 | import { NgxsDataAfterReset, NgxsDataDoCheck } from '@ngxs-labs/data/typings'; 8 | 9 | describe('[TEST]: NgxsTestingModule', () => { 10 | describe('AppState', () => { 11 | const events: string[] = []; 12 | 13 | @StateRepository() 14 | @State({ 15 | name: 'app', 16 | defaults: 0 17 | }) 18 | @Injectable() 19 | class AppState extends NgxsDataRepository implements NgxsDataDoCheck, NgxsDataAfterReset { 20 | public ngxsOnChanges(): void { 21 | events.push(`${this.name}::ngxsOnChanges`); 22 | super.ngxsOnChanges(); 23 | } 24 | 25 | public ngxsOnInit(): void { 26 | events.push(`${this.name}::ngxsOnInit`); 27 | super.ngxsOnInit(); 28 | } 29 | 30 | public ngxsDataDoCheck(): void { 31 | events.push(`${this.name}::ngxsDataDoCheck`); 32 | } 33 | 34 | public ngxsDataAfterReset(): void { 35 | events.push(`${this.name}::ngxsDataAfterReset`); 36 | } 37 | 38 | public ngxsAfterBootstrap(): void { 39 | events.push(`${this.name}::ngxsAfterBootstrap`); 40 | super.ngxsAfterBootstrap(); 41 | } 42 | 43 | @DataAction() 44 | public increment(): void { 45 | this.ctx.setState((state) => ++state); 46 | events.push(`${this.name}::increment`); 47 | } 48 | } 49 | 50 | beforeEach(() => { 51 | TestBed.configureTestingModule({ 52 | imports: [NgxsDataTestingModule.forRoot([AppState])] 53 | }); 54 | }); 55 | 56 | it('should be correct bootstrap ngxs testing', () => { 57 | NgxsDataTestingModule.ngxsInitPlatform(); 58 | const app: AppState = TestBed.inject(AppState); 59 | 60 | app.increment(); 61 | app.increment(); 62 | 63 | expect(app.getState()).toEqual(2); 64 | 65 | app.reset(); 66 | 67 | app.increment(); 68 | app.increment(); 69 | app.increment(); 70 | 71 | expect(app.getState()).toEqual(3); 72 | 73 | app.reset(); 74 | 75 | expect(events).toEqual([ 76 | 'app::ngxsOnChanges', 77 | 'app::ngxsOnInit', 78 | 'app::ngxsAfterBootstrap', 79 | 'app::ngxsDataDoCheck', 80 | 'app::ngxsOnChanges', 81 | 'app::increment', 82 | 'app::ngxsOnChanges', 83 | 'app::increment', 84 | 'app::ngxsOnChanges', 85 | 'app::ngxsDataAfterReset', 86 | 'app::ngxsOnChanges', 87 | 'app::ngxsDataDoCheck', 88 | 'app::increment', 89 | 'app::ngxsOnChanges', 90 | 'app::increment', 91 | 'app::ngxsOnChanges', 92 | 'app::increment', 93 | 'app::ngxsOnChanges', 94 | 'app::ngxsDataAfterReset' 95 | ]); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /integration/tests/testing/testing-v3.spec.ts: -------------------------------------------------------------------------------- 1 | import { ngxsTestingPlatform } from '@ngxs-labs/data/testing'; 2 | import { NgxsDataRepository } from '@ngxs-labs/data/repositories'; 3 | import { Injectable } from '@angular/core'; 4 | import { State, Store } from '@ngxs/store'; 5 | import { StateRepository } from '@ngxs-labs/data/decorators'; 6 | 7 | describe('AppState', () => { 8 | @StateRepository() 9 | @State({ 10 | name: 'app', 11 | defaults: 'hello world' 12 | }) 13 | @Injectable() 14 | class AppState extends NgxsDataRepository {} 15 | 16 | it( 17 | 'should be correct ensure state from AppState', 18 | ngxsTestingPlatform([AppState], (store: Store, app: AppState) => { 19 | expect(store.snapshot()).toEqual({ app: 'hello world' }); 20 | expect(app.getState()).toEqual('hello world'); 21 | }) 22 | ); 23 | 24 | it('Invalid state', async () => { 25 | class InvalidState {} 26 | let message: string | null = null; 27 | 28 | try { 29 | await ngxsTestingPlatform([InvalidState], () => {})(); 30 | } catch (e) { 31 | message = e.message; 32 | } 33 | 34 | expect(message).toEqual('InvalidState class must be decorated with @State() decorator'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /integration/tests/utils-extensions/mutable.spec.ts: -------------------------------------------------------------------------------- 1 | import { MutableTypePipe, MutableTypePipeModule } from '@angular-ru/common/pipes'; 2 | import { Component, Injectable } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 5 | import { StateRepository } from '@ngxs-labs/data/decorators'; 6 | import { NgxsImmutableDataRepository } from '@ngxs-labs/data/repositories'; 7 | import { NgxsModule, State } from '@ngxs/store'; 8 | 9 | import { Immutable } from '@angular-ru/common/typings'; 10 | 11 | describe('Mutable', () => { 12 | interface A { 13 | a: number; 14 | b: number; 15 | } 16 | 17 | it('Immutable to A', () => { 18 | const a: Immutable = { a: 1, b: 2 }; 19 | const mutableA = new MutableTypePipe().transform(a); 20 | 21 | mutableA.b++; 22 | expect(a).toEqual({ a: 1, b: 3 }); 23 | }); 24 | 25 | it('Immutable[] to A[]', () => { 26 | const arr: Immutable = [ 27 | { a: 1, b: 2 }, 28 | { a: 2, b: 3 } 29 | ]; 30 | 31 | const mutableArr = new MutableTypePipe().transform(arr); 32 | 33 | mutableArr[0]!.a++; 34 | mutableArr[1]!.b++; 35 | 36 | expect(mutableArr).toEqual([ 37 | { a: 2, b: 2 }, 38 | { a: 2, b: 4 } 39 | ]); 40 | 41 | expect(mutableArr.reverse()).toEqual([ 42 | { a: 2, b: 4 }, 43 | { a: 2, b: 2 } 44 | ]); 45 | }); 46 | 47 | it('should be correct work pipe in template', () => { 48 | @StateRepository() 49 | @State({ name: 'app', defaults: 0 }) 50 | @Injectable() 51 | class AppState extends NgxsImmutableDataRepository {} 52 | 53 | @Component({ selector: 'app', template: '{{ appState.state$ | async | mutable }}' }) 54 | class AppComponent { 55 | constructor(public appState: AppState) {} 56 | } 57 | 58 | TestBed.configureTestingModule({ 59 | declarations: [AppComponent], 60 | imports: [NgxsModule.forRoot([AppState]), NgxsDataPluginModule.forRoot(), MutableTypePipeModule] 61 | }).compileComponents(); 62 | 63 | const app = TestBed.createComponent(AppComponent); 64 | app.autoDetectChanges(); 65 | 66 | expect(parseFloat(app.nativeElement.innerHTML)).toEqual(0); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { createTsJestConfig } = require('@angular-ru/jest-utils'); 2 | 3 | module.exports = createTsJestConfig({ 4 | tsConfig: './tsconfig.spec.json', 5 | isolatedModules: false, 6 | jestConfig: { 7 | rootDir: '.', 8 | displayName: '@ngxs-labs/data', 9 | modulePathIgnorePatterns: ['/dist/'], 10 | collectCoverageFrom: ['/lib/**/*.ts'], 11 | testMatch: ['/integration/tests/**/*.spec.ts'], 12 | setupFilesAfterEnv: ['/integration/tests/setupJest.ts'] 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /lib/decorators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-decorators", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens", 17 | "@angular-ru/common/function": "angular-ru-common-function", 18 | "@angular-ru/common/object": "angular-ru-common-object" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/decorators/src/computed/computed.ts: -------------------------------------------------------------------------------- 1 | import { Any, Descriptor } from '@angular-ru/common/typings'; 2 | import { isNil, isTrue } from '@angular-ru/common/utils'; 3 | import { ensureComputedCache, globalSequenceId, itObservable, validateComputedMethod } from '@ngxs-labs/data/internals'; 4 | import { ComputedCacheMap, ComputedOptions } from '@ngxs-labs/data/typings'; 5 | import { Observable } from 'rxjs'; 6 | 7 | // eslint-disable-next-line max-lines-per-function 8 | export function Computed(): MethodDecorator { 9 | // eslint-disable-next-line max-lines-per-function 10 | return (target: Any, key: string | symbol, descriptor: Descriptor): Descriptor => { 11 | validateComputedMethod(target, key); 12 | const originalMethod: Any = descriptor.get; 13 | 14 | descriptor.get = function (...args: Any[]): Observable | Any { 15 | const cacheMap: ComputedCacheMap = ensureComputedCache(this); 16 | const cache: ComputedOptions | undefined = cacheMap?.get(originalMethod); 17 | 18 | if (isTrue(cache?.isObservable)) { 19 | return cache?.value as Observable; 20 | } 21 | 22 | const invalidCache: boolean = isNil(cache) || cache.sequenceId !== globalSequenceId(); 23 | 24 | if (invalidCache) { 25 | cacheMap.delete(originalMethod); 26 | const value: Observable | Any = originalMethod.apply(this, args); 27 | 28 | cacheMap.set(originalMethod, { 29 | value, 30 | sequenceId: globalSequenceId(), 31 | isObservable: itObservable(value) 32 | }); 33 | 34 | return value; 35 | } else { 36 | return cache!.value; 37 | } 38 | }; 39 | 40 | return descriptor; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /lib/decorators/src/data-action/data-action.config.ts: -------------------------------------------------------------------------------- 1 | import { RepositoryActionOptions } from '@ngxs-labs/data/typings'; 2 | 3 | export const REPOSITORY_ACTION_OPTIONS: RepositoryActionOptions = { 4 | cancelUncompleted: true, 5 | insideZone: false 6 | }; 7 | -------------------------------------------------------------------------------- /lib/decorators/src/data-action/data-action.ts: -------------------------------------------------------------------------------- 1 | import { $args } from '@angular-ru/common/function'; 2 | import { Any, Descriptor, PlainObjectOf } from '@angular-ru/common/typings'; 3 | import { isNil, isTrue } from '@angular-ru/common/utils'; 4 | import { MappedStore, MetaDataModel } from '@ngxs/store/src/internal/internals'; 5 | import { 6 | actionNameCreator, 7 | combineStream, 8 | getMethodArgsRegistry, 9 | MethodArgsRegistry, 10 | NgxsDataFactory, 11 | NgxsDataInjector, 12 | validateAction 13 | } from '@ngxs-labs/data/internals'; 14 | import { 15 | ActionEvent, 16 | DataStateClass, 17 | DispatchedResult, 18 | ImmutableDataRepository, 19 | NgxsDataOperation, 20 | NgxsRepositoryMeta, 21 | RepositoryActionOptions 22 | } from '@ngxs-labs/data/typings'; 23 | import { isObservable, Observable, of } from 'rxjs'; 24 | import { map } from 'rxjs/operators'; 25 | 26 | import { REPOSITORY_ACTION_OPTIONS } from './data-action.config'; 27 | 28 | // eslint-disable-next-line max-lines-per-function 29 | export function DataAction(options: RepositoryActionOptions = REPOSITORY_ACTION_OPTIONS): MethodDecorator { 30 | // eslint-disable-next-line max-lines-per-function 31 | return (target: Any, name: string | symbol, descriptor: Descriptor): Descriptor => { 32 | validateAction(target, descriptor); 33 | 34 | const originalMethod: Any = descriptor.value; 35 | const key: string = name.toString(); 36 | 37 | // eslint-disable-next-line max-lines-per-function 38 | descriptor.value = function (...args: Any[]): DispatchedResult { 39 | const instance: ImmutableDataRepository = this as Any as ImmutableDataRepository; 40 | 41 | let result: DispatchedResult = null; 42 | const repository: NgxsRepositoryMeta = NgxsDataFactory.getRepositoryByInstance(instance); 43 | const operations: PlainObjectOf = repository.operations!; 44 | let operation: NgxsDataOperation | undefined = operations[key]; 45 | const stateMeta: MetaDataModel = repository.stateMeta!; 46 | const registry: MethodArgsRegistry | undefined = getMethodArgsRegistry(originalMethod); 47 | 48 | if (isNil(operation)) { 49 | // Note: late init operation when first invoke action method 50 | const argumentsNames: string[] = $args(originalMethod); 51 | const type: string = actionNameCreator({ 52 | statePath: stateMeta.path!, 53 | methodName: key, 54 | argumentsNames, 55 | argumentRegistry: registry 56 | }); 57 | 58 | operation = operations[key] = { 59 | type, 60 | options: { cancelUncompleted: options.cancelUncompleted } 61 | }; 62 | 63 | stateMeta.actions[operation.type] = [ 64 | { type: operation.type, options: operation.options, fn: operation.type } 65 | ]; 66 | } 67 | 68 | const mapped: MappedStore = NgxsDataFactory.ensureMappedState(stateMeta)!; 69 | const stateInstance: DataStateClass = mapped.instance; 70 | 71 | // Note: invoke only after store.dispatch(...) 72 | (stateInstance as Any)[operation.type] = (): Any => { 73 | if (isTrue(options.insideZone)) { 74 | NgxsDataInjector.ngZone?.run((): void => { 75 | result = originalMethod.apply(instance, args); 76 | }); 77 | } else { 78 | result = originalMethod.apply(instance, args); 79 | } 80 | 81 | // Note: store.dispatch automatically subscribes, but we don’t need it 82 | // We want to subscribe ourselves manually 83 | return isObservable(result) ? of(null).pipe(map((): Any => result)) : result; 84 | }; 85 | 86 | const event: ActionEvent = NgxsDataFactory.createAction(operation, args, registry); 87 | const dispatcher: Observable = NgxsDataInjector.store!.dispatch(event); 88 | 89 | if (isObservable(result)) { 90 | return combineStream(dispatcher, result); 91 | } else { 92 | return result; 93 | } 94 | }; 95 | 96 | return descriptor; 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /lib/decorators/src/debounce/debounce.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode } from '@angular/core'; 2 | import { Any, Descriptor } from '@angular-ru/common/typings'; 3 | import { isNotNil } from '@angular-ru/common/utils'; 4 | import { checkExistNgZone, NgxsDataInjector } from '@ngxs-labs/data/internals'; 5 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 6 | 7 | const DEFAULT_TIMEOUT: number = 300; 8 | 9 | export function Debounce(timeout: number = DEFAULT_TIMEOUT): MethodDecorator { 10 | let timeoutRef: number | null = null; 11 | return (_target: T, _name: string | symbol, descriptor: Descriptor): Descriptor => { 12 | const originalMethod: Any = descriptor.value; 13 | 14 | descriptor.value = function (...args: Any[]): Any { 15 | checkExistNgZone(); 16 | 17 | NgxsDataInjector.ngZone?.runOutsideAngular((): void => { 18 | window.clearTimeout(timeoutRef!); 19 | timeoutRef = window.setTimeout((): void => { 20 | const result: Any = originalMethod.apply(this, args); 21 | 22 | if (isDevMode() && isNotNil(result)) { 23 | console.warn(NGXS_DATA_EXCEPTIONS.NGXS_DATA_ASYNC_ACTION_RETURN_TYPE, result); 24 | } 25 | }, timeout); 26 | }); 27 | }; 28 | 29 | return descriptor; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /lib/decorators/src/named/named.ts: -------------------------------------------------------------------------------- 1 | import { ensureMethodArgsRegistry, MethodArgsRegistry } from '@ngxs-labs/data/internals'; 2 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 3 | import { DataStateClass, StateArgumentDecorator } from '@ngxs-labs/data/typings'; 4 | 5 | export function Named(name: string): StateArgumentDecorator { 6 | return (stateClass: DataStateClass, methodName: string | symbol, parameterIndex: number): void => { 7 | const key: string = name.trim(); 8 | 9 | if (!key) { 10 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_INVALID_ARG_NAME); 11 | } 12 | 13 | const registry: MethodArgsRegistry = ensureMethodArgsRegistry(stateClass, methodName); 14 | registry.createArgumentName(key, methodName as string, parameterIndex); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/decorators/src/payload/payload.ts: -------------------------------------------------------------------------------- 1 | import { ensureMethodArgsRegistry, MethodArgsRegistry } from '@ngxs-labs/data/internals'; 2 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 3 | import { DataStateClass, StateArgumentDecorator } from '@ngxs-labs/data/typings'; 4 | 5 | export function Payload(name: string): StateArgumentDecorator { 6 | return (stateClass: DataStateClass, methodName: string | symbol, parameterIndex: number): void => { 7 | const key: string = name.trim(); 8 | 9 | if (!key) { 10 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_INVALID_PAYLOAD_NAME); 11 | } 12 | 13 | const registry: MethodArgsRegistry = ensureMethodArgsRegistry(stateClass, methodName); 14 | registry.createPayloadType(key, methodName as string, parameterIndex); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /lib/decorators/src/persistence/persistence.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isFalsy, isNil } from '@angular-ru/common/utils'; 3 | import { MetaDataModel } from '@ngxs/store/src/internal/internals'; 4 | import { ensureStateMetadata, getRepository, STORAGE_INIT_EVENT } from '@ngxs-labs/data/internals'; 5 | import { ensureProviders, registerStorageProviders } from '@ngxs-labs/data/storage'; 6 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 7 | import { DataStateClass, NgxsRepositoryMeta, PersistenceProvider, ProviderOptions } from '@ngxs-labs/data/typings'; 8 | 9 | export function Persistence(options?: ProviderOptions): Any { 10 | return (stateClass: DataStateClass): void => { 11 | const stateMeta: MetaDataModel = ensureStateMetadata(stateClass); 12 | const repositoryMeta: NgxsRepositoryMeta = getRepository(stateClass); 13 | const isUndecoratedClass: boolean = isNil(stateMeta.name) || isNil(repositoryMeta); 14 | 15 | if (isUndecoratedClass) { 16 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_PERSISTENCE_STATE); 17 | } 18 | 19 | STORAGE_INIT_EVENT.events$.subscribe((): void => { 20 | if (isFalsy(STORAGE_INIT_EVENT.firstInitialized)) { 21 | STORAGE_INIT_EVENT.firstInitialized = true; 22 | } 23 | 24 | const providers: PersistenceProvider[] = ensureProviders(repositoryMeta, stateClass, options); 25 | registerStorageProviders(providers); 26 | }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/decorators/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { Computed } from './computed/computed'; 2 | export { DataAction } from './data-action/data-action'; 3 | export { Debounce } from './debounce/debounce'; 4 | export { Named } from './named/named'; 5 | export { Payload } from './payload/payload'; 6 | export { Persistence } from './persistence/persistence'; 7 | export { StateRepository } from './state-repository/state-repository'; 8 | -------------------------------------------------------------------------------- /lib/decorators/src/state-repository/state-repository.ts: -------------------------------------------------------------------------------- 1 | import { deepClone } from '@angular-ru/common/object'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { isNil } from '@angular-ru/common/utils'; 4 | import { MetaDataModel } from '@ngxs/store/src/internal/internals'; 5 | import { 6 | buildDefaultsGraph, 7 | createContext, 8 | createRepositoryMetadata, 9 | createStateSelector, 10 | ensureStateMetadata 11 | } from '@ngxs-labs/data/internals'; 12 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 13 | import { DataStateClass, StateClassDecorator } from '@ngxs-labs/data/typings'; 14 | 15 | export function StateRepository(): StateClassDecorator { 16 | return (stateClass: DataStateClass): void => { 17 | const stateMeta: MetaDataModel = ensureStateMetadata(stateClass); 18 | 19 | if (isNil(stateMeta.name)) { 20 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_STATE); 21 | } 22 | 23 | createRepositoryMetadata(stateClass, stateMeta); 24 | const cloneDefaults: Any = buildDefaultsGraph(stateClass); 25 | 26 | defineProperties(stateClass, stateMeta, cloneDefaults); 27 | createStateSelector(stateClass); 28 | }; 29 | } 30 | 31 | function defineProperties(stateClass: DataStateClass, stateMeta: MetaDataModel, cloneDefaults: Any): void { 32 | Object.defineProperties(stateClass.prototype, { 33 | name: { 34 | enumerable: true, 35 | configurable: true, 36 | value: stateMeta.name 37 | }, 38 | initialState: { 39 | enumerable: true, 40 | configurable: true, 41 | get(): Any { 42 | // preserve mutation 43 | return deepClone(cloneDefaults); 44 | } 45 | }, 46 | context: createContext(stateClass) 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /lib/internals/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-internal", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@angular-ru/common/object": "angular-ru-common-object", 10 | "@ngxs-labs/data": "ngxs-labs-data", 11 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 12 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 13 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 14 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 15 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 16 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 17 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/internals/src/decorators/validate-action.ts: -------------------------------------------------------------------------------- 1 | import { Any, Fn } from '@angular-ru/common/typings'; 2 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 3 | 4 | export function validateAction(target: Fn, descriptor: TypedPropertyDescriptor): void { 5 | const isStaticMethod: boolean = target.hasOwnProperty('prototype'); 6 | 7 | if (isStaticMethod) { 8 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_STATIC_ACTION); 9 | } 10 | 11 | if (descriptor === undefined) { 12 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_ACTION); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/internals/src/decorators/validate-computed-method.ts: -------------------------------------------------------------------------------- 1 | import { isGetter } from '@angular-ru/common/object'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 4 | 5 | export function validateComputedMethod(target: Any, name: string | symbol): void { 6 | const notGetter: boolean = !isGetter(target, name?.toString()); 7 | if (notGetter) { 8 | throw new Error( 9 | NGXS_DATA_EXCEPTIONS.NGXS_COMPUTED_DECORATOR + 10 | `\nExample: \n@Computed() get ${name.toString()}() { \n\t .. \n}` 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/internals/src/exceptions/invalid-args-names.exception.ts: -------------------------------------------------------------------------------- 1 | export class InvalidArgsNamesException extends Error { 2 | constructor(name: string, method: string) { 3 | super(`An argument with the name '${name}' already exists in the method '${method}'`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/internals/src/exceptions/invalid-children.exception.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isNotNil } from '@angular-ru/common/utils'; 3 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 4 | 5 | export class InvalidChildrenException extends Error { 6 | constructor(currentDefaults: Any) { 7 | super( 8 | `${NGXS_DATA_EXCEPTIONS.NGXS_DATA_CHILDREN_CONVERT}. Cannot convert ${ 9 | isNotNil(currentDefaults?.constructor) ? currentDefaults.constructor.name : currentDefaults 10 | } to PlainObject` 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/internals/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { validateAction } from './decorators/validate-action'; 2 | export { validateComputedMethod } from './decorators/validate-computed-method'; 3 | export { NgxsDataSequence } from './services/ngxs-data-computed-stream.service'; 4 | export { NgxsDataFactory } from './services/ngxs-data-factory.service'; 5 | export { NgxsDataInjector } from './services/ngxs-data-injector.service'; 6 | export { STORAGE_INIT_EVENT } from './storage/init-storage'; 7 | export { actionNameCreator } from './utils/action/action-name-creator'; 8 | export { dynamicActionByType } from './utils/action/dynamic-action'; 9 | export { buildDefaultsGraph } from './utils/common/build-defaults-graph'; 10 | export { checkExistNgZone } from './utils/common/check-exist-ng-zone'; 11 | export { combineStream } from './utils/common/combine-stream'; 12 | export { ensureComputedCache } from './utils/computed/ensure-computed-cache'; 13 | export { getComputedCache } from './utils/computed/get-computed-cache'; 14 | export { globalSequenceId } from './utils/computed/global-sequence-id'; 15 | export { itObservable } from './utils/computed/it-observable'; 16 | export { ensureMethodArgsRegistry } from './utils/method-args-registry/ensure-method-args-registry'; 17 | export { getMethodArgsRegistry } from './utils/method-args-registry/get-method-args-registry'; 18 | export { MethodArgsRegistry } from './utils/method-args-registry/method-args-registry'; 19 | export { createRepositoryMetadata } from './utils/repository/create-repository-metadata'; 20 | export { defineDefaultRepositoryMeta } from './utils/repository/define-default-repository-meta'; 21 | export { ensureRepository } from './utils/repository/ensure-repository'; 22 | export { ensureSnapshot } from './utils/repository/ensure-snapshot'; 23 | export { getRepository } from './utils/repository/get-repository'; 24 | export { createContext } from './utils/state-context/create-context'; 25 | export { createStateSelector } from './utils/state-context/create-state-selector'; 26 | export { ensureDataStateContext } from './utils/state-context/ensure-data-state-context'; 27 | export { ensureStateMetadata } from './utils/state-context/ensure-state-metadata'; 28 | export { getStateMetadata } from './utils/state-context/get-state-metadata'; 29 | export { getStoreOptions } from './utils/state-context/get-store-options'; 30 | -------------------------------------------------------------------------------- /lib/internals/src/services/ngxs-data-computed-stream.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnDestroy, Optional } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { BehaviorSubject, Subscription } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class NgxsDataSequence implements OnDestroy { 7 | public readonly sequence$: BehaviorSubject = new BehaviorSubject(0); 8 | private subscription: Subscription | null = null; 9 | 10 | constructor(@Optional() store?: Store) { 11 | if (store) { 12 | this.subscription = store.subscribe((): void => this.updateSequence()); 13 | } 14 | } 15 | 16 | public get sequenceValue(): number { 17 | return this.sequence$.getValue(); 18 | } 19 | 20 | public ngOnDestroy(): void { 21 | this.sequence$.next(0); 22 | this.subscription?.unsubscribe(); 23 | } 24 | 25 | public updateSequence(): void { 26 | this.sequence$.next(this.sequenceValue + 1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/internals/src/services/ngxs-data-factory.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@angular/core'; 2 | import { Any, PlainObjectOf } from '@angular-ru/common/typings'; 3 | import { isNil, isNotNil } from '@angular-ru/common/utils'; 4 | import { StateContext } from '@ngxs/store'; 5 | import { MappedStore, MetaDataModel } from '@ngxs/store/src/internal/internals'; 6 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 7 | import { 8 | ActionEvent, 9 | DataStateClass, 10 | MappedState, 11 | NgxsDataOperation, 12 | NgxsRepositoryMeta, 13 | PayloadName 14 | } from '@ngxs-labs/data/typings'; 15 | 16 | import { dynamicActionByType } from '../utils/action/dynamic-action'; 17 | import { MethodArgsRegistry } from '../utils/method-args-registry/method-args-registry'; 18 | import { getRepository } from '../utils/repository/get-repository'; 19 | import { NgxsDataInjector } from './ngxs-data-injector.service'; 20 | 21 | @Injectable() 22 | export class NgxsDataFactory { 23 | private static readonly statesCachedMeta: Map = new Map(); 24 | 25 | constructor() { 26 | NgxsDataFactory.statesCachedMeta.clear(); 27 | } 28 | 29 | public static createStateContext(metadata: MappedStore): StateContext { 30 | return NgxsDataInjector.context.createStateContext(metadata); 31 | } 32 | 33 | public static ensureMappedState(stateMeta: MetaDataModel | undefined): MappedState | never { 34 | if (isNil(NgxsDataInjector.factory) || isNil(stateMeta)) { 35 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_MODULE_EXCEPTION); 36 | } 37 | 38 | const cachedMeta: MappedStore | null = 39 | (isNotNil(stateMeta.name) ? NgxsDataFactory.statesCachedMeta.get(stateMeta.name) : null) || null; 40 | 41 | if (!cachedMeta) { 42 | return NgxsDataFactory.ensureMeta(stateMeta); 43 | } 44 | 45 | return cachedMeta; 46 | } 47 | 48 | public static getRepositoryByInstance(target: DataStateClass | Any): NgxsRepositoryMeta | never { 49 | const stateClass: DataStateClass = NgxsDataFactory.getStateClassByInstance(target); 50 | const repository: NgxsRepositoryMeta | null = getRepository(stateClass) ?? null; 51 | 52 | if (isNil(repository)) { 53 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_STATE_DECORATOR); 54 | } 55 | 56 | return repository; 57 | } 58 | 59 | public static getStateClassByInstance(target: DataStateClass | Any): DataStateClass { 60 | return (target ?? {})['constructor']; 61 | } 62 | 63 | public static clearMetaByInstance(target: DataStateClass | Any): void { 64 | const repository: NgxsRepositoryMeta = NgxsDataFactory.getRepositoryByInstance(target); 65 | repository.stateMeta!.actions = {}; 66 | repository.operations = {}; 67 | } 68 | 69 | public static createPayload(args: Any[], registry?: MethodArgsRegistry): PlainObjectOf | null { 70 | const payload: PlainObjectOf = {}; 71 | const arrayArgs: Any[] = Array.from(args); 72 | 73 | for (let index: number = 0; index < arrayArgs.length; index++) { 74 | const payloadName: PayloadName | null | undefined = registry?.getPayloadTypeByIndex(index); 75 | if (isNotNil(payloadName)) { 76 | payload[payloadName] = arrayArgs[index]; 77 | } 78 | } 79 | 80 | return Object.keys(payload).length > 0 ? payload : null; 81 | } 82 | 83 | public static createAction(operation: NgxsDataOperation, args: Any[], registry?: MethodArgsRegistry): ActionEvent { 84 | const payload: PlainObjectOf | null = NgxsDataFactory.createPayload(args, registry); 85 | const dynamicActionByTypeFactory: Type = dynamicActionByType(operation.type); 86 | return new dynamicActionByTypeFactory(payload) as ActionEvent; 87 | } 88 | 89 | private static ensureMeta(stateMeta: MetaDataModel): MappedStore | null | undefined { 90 | const meta: MappedState = isNotNil(stateMeta.name) 91 | ? (NgxsDataInjector.factory.states as MappedStore[])?.find( 92 | (state: MappedStore): boolean => state.name === stateMeta.name 93 | ) 94 | : null; 95 | 96 | if (isNotNil(meta) && isNotNil(stateMeta.name)) { 97 | NgxsDataFactory.statesCachedMeta.set(stateMeta.name, meta); 98 | } 99 | 100 | return meta; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/internals/src/services/ngxs-data-injector.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Injector, NgZone } from '@angular/core'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { Store } from '@ngxs/store'; 4 | import { NGXS_STATE_CONTEXT_FACTORY, NGXS_STATE_FACTORY } from '@ngxs/store/internals'; 5 | 6 | import { NgxsDataSequence } from './ngxs-data-computed-stream.service'; 7 | 8 | @Injectable() 9 | export class NgxsDataInjector { 10 | public static store: Store | null = null; 11 | public static computed: NgxsDataSequence | null = null; 12 | public static context: Any | null = null; 13 | public static factory: Any | null = null; 14 | public static ngZone: NgZone | null = null; 15 | 16 | constructor( 17 | injector: Injector, 18 | @Inject(NGXS_STATE_FACTORY) stateFactory: Any, 19 | @Inject(NGXS_STATE_CONTEXT_FACTORY) stateContextFactory: Any 20 | ) { 21 | NgxsDataInjector.store = injector.get(Store); 22 | NgxsDataInjector.ngZone = injector.get(NgZone); 23 | NgxsDataInjector.factory = stateFactory; 24 | NgxsDataInjector.context = stateContextFactory; 25 | NgxsDataInjector.computed = injector.get(NgxsDataSequence); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/internals/src/storage/init-storage.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject } from 'rxjs'; 2 | 3 | export const STORAGE_INIT_EVENT: { firstInitialized: boolean; events$: ReplaySubject } = { 4 | firstInitialized: false, 5 | events$: new ReplaySubject(1) 6 | }; 7 | -------------------------------------------------------------------------------- /lib/internals/src/utils/action/action-name-creator.ts: -------------------------------------------------------------------------------- 1 | import { isNotNil } from '@angular-ru/common/utils'; 2 | 3 | import { MethodArgsRegistry } from '../method-args-registry/method-args-registry'; 4 | 5 | interface ActionNameCreatorOptions { 6 | statePath: string; 7 | methodName: string; 8 | argumentsNames: string[]; 9 | argumentRegistry?: MethodArgsRegistry; 10 | } 11 | 12 | export function actionNameCreator(options: ActionNameCreatorOptions): string { 13 | const { statePath, argumentsNames, methodName, argumentRegistry }: ActionNameCreatorOptions = options; 14 | 15 | let argsList: string = ''; 16 | for (let index: number = 0; index < argumentsNames.length; index++) { 17 | if (isNotNil(argumentRegistry?.getArgumentNameByIndex(index))) { 18 | argsList += argumentRegistry?.getArgumentNameByIndex(index); 19 | } else if (isNotNil(argumentRegistry?.getPayloadTypeByIndex(index))) { 20 | argsList += argumentRegistry?.getPayloadTypeByIndex(index); 21 | } else { 22 | argsList += `$arg${index}`; 23 | } 24 | 25 | if (index !== argumentsNames.length - 1) { 26 | argsList += ', '; 27 | } 28 | } 29 | 30 | return `@${statePath.replace(/\./g, '/')}.${methodName}(${argsList})`; 31 | } 32 | -------------------------------------------------------------------------------- /lib/internals/src/utils/action/dynamic-action.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { Any, PlainObjectOf } from '@angular-ru/common/typings'; 3 | 4 | export function dynamicActionByType(type: string): Type { 5 | return class NgxsDataAction { 6 | constructor(payload: PlainObjectOf | null) { 7 | if (payload) { 8 | Object.keys(payload).forEach((key: string): void => { 9 | (this as Any)[key] = payload[key]; 10 | }); 11 | } 12 | } 13 | 14 | public static get type(): string { 15 | return type; 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/internals/src/utils/common/build-defaults-graph.ts: -------------------------------------------------------------------------------- 1 | import { deepClone, isSimpleObject } from '@angular-ru/common/object'; 2 | import { Any, PlainObject } from '@angular-ru/common/typings'; 3 | import { checkValueIsEmpty } from '@angular-ru/common/utils'; 4 | import { StoreOptions } from '@ngxs/store/src/symbols'; 5 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 6 | import { DataStateClass } from '@ngxs-labs/data/typings'; 7 | 8 | import { InvalidChildrenException } from '../../exceptions/invalid-children.exception'; 9 | import { getStoreOptions } from '../state-context/get-store-options'; 10 | 11 | export function buildDefaultsGraph(stateClasses: DataStateClass): Any { 12 | const options: StoreOptions = getStoreOptions(stateClasses); 13 | const children: DataStateClass[] = options.children ?? []; 14 | const prepared: Any = options.defaults === undefined ? {} : options.defaults; 15 | const currentDefaults: Any = deepClone(prepared); 16 | 17 | if (children.length) { 18 | if (isSimpleObject(currentDefaults)) { 19 | return buildChildrenGraph(currentDefaults, children); 20 | } else { 21 | throw new InvalidChildrenException(currentDefaults); 22 | } 23 | } else { 24 | return currentDefaults; 25 | } 26 | } 27 | 28 | function buildChildrenGraph(currentDefaults: Any, children: DataStateClass[]): Any { 29 | return children.reduce((defaults: PlainObject, item: DataStateClass): PlainObject => { 30 | const childrenOptions: StoreOptions = getStoreOptions(item); 31 | if (checkValueIsEmpty(childrenOptions.name)) { 32 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_STATE_NAME_NOT_FOUND); 33 | } 34 | 35 | const name: string = childrenOptions.name.toString(); 36 | defaults[name] = buildDefaultsGraph(item); 37 | 38 | return defaults; 39 | }, currentDefaults ?? {}); 40 | } 41 | -------------------------------------------------------------------------------- /lib/internals/src/utils/common/check-exist-ng-zone.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from '@angular-ru/common/utils'; 2 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 3 | 4 | import { NgxsDataInjector } from '../../services/ngxs-data-injector.service'; 5 | 6 | export function checkExistNgZone(): void | never { 7 | if (isNil(NgxsDataInjector.ngZone)) { 8 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_MODULE_EXCEPTION); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/internals/src/utils/common/combine-stream.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { forkJoin, Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | export function combineStream(dispatched: Observable, result: Observable): Observable { 6 | return forkJoin([dispatched, result]).pipe(map((combines: [Any, Any]): Any => combines.pop())); 7 | } 8 | -------------------------------------------------------------------------------- /lib/internals/src/utils/common/computed-key.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_COMPUTED_OPTION } from '@ngxs-labs/data/tokens'; 2 | 3 | export function computedKey(): string { 4 | return `__${NGXS_COMPUTED_OPTION}__`; 5 | } 6 | -------------------------------------------------------------------------------- /lib/internals/src/utils/computed/ensure-computed-cache.ts: -------------------------------------------------------------------------------- 1 | import { Any, Fn } from '@angular-ru/common/typings'; 2 | import { isNil } from '@angular-ru/common/utils'; 3 | import { ComputedCacheMap, ComputedOptions } from '@ngxs-labs/data/typings'; 4 | 5 | import { computedKey } from '../common/computed-key'; 6 | import { getComputedCache } from './get-computed-cache'; 7 | 8 | export function ensureComputedCache(target: Any): ComputedCacheMap { 9 | const cache: ComputedCacheMap | null = getComputedCache(target); 10 | 11 | if (isNil(cache)) { 12 | Object.defineProperties(target, { 13 | [computedKey()]: { 14 | enumerable: true, 15 | configurable: true, 16 | value: new WeakMap() 17 | } 18 | }); 19 | } 20 | 21 | return getComputedCache(target)!; 22 | } 23 | -------------------------------------------------------------------------------- /lib/internals/src/utils/computed/get-computed-cache.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { ComputedCacheMap } from '@ngxs-labs/data/typings'; 3 | 4 | import { computedKey } from '../common/computed-key'; 5 | 6 | export function getComputedCache(target: Any): ComputedCacheMap | null { 7 | return target[computedKey()] ?? null; 8 | } 9 | -------------------------------------------------------------------------------- /lib/internals/src/utils/computed/global-sequence-id.ts: -------------------------------------------------------------------------------- 1 | import { NgxsDataInjector } from '../../services/ngxs-data-injector.service'; 2 | 3 | export function globalSequenceId(): number { 4 | return NgxsDataInjector?.computed?.sequenceValue ?? 0; 5 | } 6 | -------------------------------------------------------------------------------- /lib/internals/src/utils/computed/it-observable.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isObservable } from 'rxjs'; 3 | 4 | export function itObservable(value: Any): boolean { 5 | let observable: boolean = false; 6 | 7 | if (isObservable(value)) { 8 | observable = true; 9 | } 10 | 11 | return observable; 12 | } 13 | -------------------------------------------------------------------------------- /lib/internals/src/utils/method-args-registry/ensure-method-args-registry.ts: -------------------------------------------------------------------------------- 1 | import { Any, Fn } from '@angular-ru/common/typings'; 2 | import { isNil } from '@angular-ru/common/utils'; 3 | import { NGXS_ARGUMENT_REGISTRY_META } from '@ngxs-labs/data/tokens'; 4 | 5 | import { getMethodArgsRegistry } from './get-method-args-registry'; 6 | import { MethodArgsRegistry } from './method-args-registry'; 7 | 8 | export function ensureMethodArgsRegistry(target: Any, propertyKey: Any): MethodArgsRegistry { 9 | const originMethod: Fn = target[propertyKey]; 10 | const registry: MethodArgsRegistry | undefined = getMethodArgsRegistry(originMethod); 11 | 12 | if (isNil(registry)) { 13 | Object.defineProperties(originMethod, { 14 | [NGXS_ARGUMENT_REGISTRY_META]: { 15 | enumerable: true, 16 | configurable: true, 17 | value: new MethodArgsRegistry() 18 | } 19 | }); 20 | } 21 | 22 | return getMethodArgsRegistry(originMethod)!; 23 | } 24 | -------------------------------------------------------------------------------- /lib/internals/src/utils/method-args-registry/get-method-args-registry.ts: -------------------------------------------------------------------------------- 1 | import { Any, Fn } from '@angular-ru/common/typings'; 2 | import { NGXS_ARGUMENT_REGISTRY_META } from '@ngxs-labs/data/tokens'; 3 | 4 | import { MethodArgsRegistry } from './method-args-registry'; 5 | 6 | export function getMethodArgsRegistry(method: Fn): MethodArgsRegistry | undefined { 7 | return (method as Any)[NGXS_ARGUMENT_REGISTRY_META]; 8 | } 9 | -------------------------------------------------------------------------------- /lib/internals/src/utils/method-args-registry/method-args-registry.ts: -------------------------------------------------------------------------------- 1 | import { isTruthy } from '@angular-ru/common/utils'; 2 | import { ArgName, ArgNameMap, PayloadMap, PayloadName } from '@ngxs-labs/data/typings'; 3 | 4 | import { InvalidArgsNamesException } from '../../exceptions/invalid-args-names.exception'; 5 | 6 | export class MethodArgsRegistry { 7 | private payloadMap: PayloadMap = new Map(); 8 | private argumentMap: ArgNameMap = new Map(); 9 | 10 | public getPayloadTypeByIndex(index: number): PayloadName | null { 11 | return this.payloadMap.get(index) ?? null; 12 | } 13 | 14 | public getArgumentNameByIndex(index: number): ArgName | null { 15 | return this.argumentMap.get(index) ?? null; 16 | } 17 | 18 | public createPayloadType(name: PayloadName, method: string, paramIndex: number): void { 19 | this.checkDuplicateName(name, method); 20 | this.payloadMap.set(paramIndex, name); 21 | this.payloadMap.set(name, name); 22 | } 23 | 24 | public createArgumentName(name: ArgName, method: string, paramIndex: number): void | never { 25 | this.checkDuplicateName(name, method); 26 | this.argumentMap.set(paramIndex, name); 27 | this.argumentMap.set(name, name); 28 | } 29 | 30 | private checkDuplicateName(name: ArgName, method: string): void | never { 31 | if (isTruthy(this.argumentMap.has(name)) || isTruthy(this.payloadMap.has(name))) { 32 | throw new InvalidArgsNamesException(name, method); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/internals/src/utils/repository/create-repository-metadata.ts: -------------------------------------------------------------------------------- 1 | import { MetaDataModel } from '@ngxs/store/src/internal/internals'; 2 | import { DataStateClass, NgxsRepositoryMeta } from '@ngxs-labs/data/typings'; 3 | 4 | import { ensureRepository } from './ensure-repository'; 5 | 6 | /** 7 | * @description need mutate metadata for correct reference 8 | */ 9 | export function createRepositoryMetadata(target: DataStateClass, stateMeta: MetaDataModel): void { 10 | const repositoryMeta: NgxsRepositoryMeta = ensureRepository(target); 11 | repositoryMeta.stateMeta = stateMeta; 12 | } 13 | -------------------------------------------------------------------------------- /lib/internals/src/utils/repository/define-default-repository-meta.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_META } from '@ngxs-labs/data/tokens'; 2 | import { DataStateClass } from '@ngxs-labs/data/typings'; 3 | 4 | export function defineDefaultRepositoryMeta(target: DataStateClass): void { 5 | Object.defineProperty(target, NGXS_DATA_META, { 6 | writable: true, 7 | configurable: true, 8 | enumerable: true, 9 | value: { stateMeta: null, operations: {}, stateClass: target } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/internals/src/utils/repository/ensure-repository.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from '@angular-ru/common/utils'; 2 | import { DataStateClass, NgxsRepositoryMeta } from '@ngxs-labs/data/typings'; 3 | 4 | import { defineDefaultRepositoryMeta } from './define-default-repository-meta'; 5 | import { getRepository } from './get-repository'; 6 | 7 | /** 8 | * @description 9 | * don't use !target.hasOwnProperty(NGXS_DATA_META), 10 | * because you need support access from parent inheritance class 11 | */ 12 | export function ensureRepository(target: DataStateClass): NgxsRepositoryMeta { 13 | const repository: NgxsRepositoryMeta | null = getRepository(target) ?? null; 14 | const metaNotFound: boolean = isNil(repository) || repository?.stateClass !== target; 15 | 16 | if (metaNotFound) { 17 | defineDefaultRepositoryMeta(target); 18 | } 19 | 20 | return getRepository(target); 21 | } 22 | -------------------------------------------------------------------------------- /lib/internals/src/utils/repository/ensure-snapshot.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode } from '@angular/core'; 2 | import { deepFreeze } from '@angular-ru/common/object'; 3 | 4 | export function ensureSnapshot(state: T): T { 5 | return isDevMode() ? deepFreeze(state) : state; 6 | } 7 | -------------------------------------------------------------------------------- /lib/internals/src/utils/repository/get-repository.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_META } from '@ngxs-labs/data/tokens'; 2 | import { DataStateClass, NgxsRepositoryMeta } from '@ngxs-labs/data/typings'; 3 | 4 | export function getRepository(target: DataStateClass): NgxsRepositoryMeta { 5 | return target[NGXS_DATA_META]!; 6 | } 7 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/create-context.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { StateContext } from '@ngxs/store'; 3 | import { MappedStore } from '@ngxs/store/src/internal/internals'; 4 | import { DataStateClass, NgxsRepositoryMeta } from '@ngxs-labs/data/typings'; 5 | 6 | import { NgxsDataFactory } from '../../services/ngxs-data-factory.service'; 7 | import { getRepository } from '../repository/get-repository'; 8 | 9 | export function createContext(stateClass: DataStateClass): PropertyDescriptor { 10 | return { 11 | enumerable: true, 12 | configurable: true, 13 | get(): StateContext { 14 | const meta: NgxsRepositoryMeta = getRepository(stateClass); 15 | const mappedMeta: MappedStore = NgxsDataFactory.ensureMappedState(meta.stateMeta)!; 16 | return NgxsDataFactory.createStateContext(mappedMeta); 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/create-state-selector.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode } from '@angular/core'; 2 | import { deepFreeze } from '@angular-ru/common/object'; 3 | import { Any } from '@angular-ru/common/typings'; 4 | import { isNil, isNotNil } from '@angular-ru/common/utils'; 5 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 6 | import { DataStateClass, NgxsRepositoryMeta } from '@ngxs-labs/data/typings'; 7 | import { Observable } from 'rxjs'; 8 | import { map, shareReplay } from 'rxjs/operators'; 9 | 10 | import { NgxsDataInjector } from '../../services/ngxs-data-injector.service'; 11 | import { getRepository } from '../repository/get-repository'; 12 | 13 | // eslint-disable-next-line max-lines-per-function,sonarjs/cognitive-complexity 14 | export function createStateSelector(stateClass: DataStateClass): void { 15 | const repository: NgxsRepositoryMeta = getRepository(stateClass); 16 | const name: string | undefined | null = repository?.stateMeta?.name ?? null; 17 | 18 | if (isNotNil(name)) { 19 | const selectorId: string = `__${name}__selector`; 20 | 21 | Object.defineProperties(stateClass.prototype, { 22 | [selectorId]: { writable: true, enumerable: false, configurable: true }, 23 | state$: { 24 | enumerable: true, 25 | configurable: true, 26 | get(): Observable { 27 | if (isNotNil(this[selectorId])) { 28 | return this[selectorId]; 29 | } else { 30 | if (isNil(NgxsDataInjector.store)) { 31 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_MODULE_EXCEPTION); 32 | } 33 | 34 | this[selectorId] = NgxsDataInjector.store.select(stateClass as Any).pipe( 35 | map((state: Any): Any => (isDevMode() ? deepFreeze(state) : state)), 36 | shareReplay({ refCount: true, bufferSize: 1 }) 37 | ); 38 | } 39 | 40 | return this[selectorId]; 41 | } 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/ensure-data-state-context.ts: -------------------------------------------------------------------------------- 1 | import { isDevMode } from '@angular/core'; 2 | import { deepFreeze } from '@angular-ru/common/object'; 3 | import { Any } from '@angular-ru/common/typings'; 4 | import { StateContext } from '@ngxs/store'; 5 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 6 | 7 | export function ensureDataStateContext>(context: T | null): T { 8 | if (!context) { 9 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_DATA_STATE_DECORATOR); 10 | } 11 | 12 | return { 13 | ...context, 14 | getState(): U { 15 | return isDevMode() ? deepFreeze(context.getState()) : context.getState(); 16 | }, 17 | setState(val: Any): void { 18 | context.setState(val); 19 | }, 20 | patchState(val: Any): void { 21 | context.patchState(val); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/ensure-state-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isFalsy } from '@angular-ru/common/utils'; 3 | import { MetaDataModel, RuntimeSelectorContext } from '@ngxs/store/src/internal/internals'; 4 | import { NGXS_META_KEY } from '@ngxs-labs/data/tokens'; 5 | import { DataStateClass } from '@ngxs-labs/data/typings'; 6 | 7 | import { getStateMetadata } from './get-state-metadata'; 8 | 9 | export function ensureStateMetadata(target: DataStateClass): MetaDataModel { 10 | if (isFalsy(target.hasOwnProperty(NGXS_META_KEY))) { 11 | const defaultMetadata: MetaDataModel = { 12 | name: null, 13 | actions: {}, 14 | defaults: {}, 15 | path: null, 16 | makeRootSelector(context: RuntimeSelectorContext): Any { 17 | return context.getStateGetter(defaultMetadata.name); 18 | }, 19 | children: [] 20 | }; 21 | 22 | Object.defineProperty(target, NGXS_META_KEY, { value: defaultMetadata }); 23 | } 24 | 25 | return getStateMetadata(target); 26 | } 27 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/get-state-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { MetaDataModel } from '@ngxs/store/src/internal/internals'; 3 | import { NGXS_META_KEY } from '@ngxs-labs/data/tokens'; 4 | import { DataStateClass } from '@ngxs-labs/data/typings'; 5 | 6 | export function getStateMetadata(target: DataStateClass): MetaDataModel { 7 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 8 | return (target as Any)?.[NGXS_META_KEY]!; 9 | } 10 | -------------------------------------------------------------------------------- /lib/internals/src/utils/state-context/get-store-options.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { StoreOptions } from '@ngxs/store/src/symbols'; 3 | import { DataStateClass } from '@ngxs-labs/data/typings'; 4 | 5 | export function getStoreOptions(stateClass: DataStateClass): StoreOptions { 6 | return stateClass['NGXS_OPTIONS_META']! ?? { name: '' }; 7 | } 8 | -------------------------------------------------------------------------------- /lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts", 5 | "flatModuleFile": "ngxs-labs-data", 6 | "umdModuleIds": { 7 | "@angular-ru/common": "angular-ru-common", 8 | "@angular-ru/common/object": "angular-ru-common-object", 9 | "@ngxs/store": "ngxs-store", 10 | "@ngxs/store/internals": "ngxs-store-internals", 11 | "@ngxs-labs/data": "ngxs-labs-data", 12 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 13 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 14 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 15 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 16 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 17 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 18 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens" 19 | } 20 | }, 21 | "dest": "../dist/ngxs-data", 22 | "assets": ["ngcc.config.js"] 23 | } 24 | -------------------------------------------------------------------------------- /lib/ngcc.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignorableDeepImportMatchers: [/ngxs\/store\/src\/symbols/, /ngxs\/store\/src\/internal\/internals/] 3 | }; 4 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngxs-labs/data", 3 | "version": "6.2.0", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/ngxs-labs/data.git" 8 | }, 9 | "keywords": [ 10 | "ngxs", 11 | "state", 12 | "rxjs", 13 | "angular", 14 | "cqrs", 15 | "store", 16 | "state-management", 17 | "event-stream", 18 | "data-plugin" 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/ngxs-labs/data/issues" 22 | }, 23 | "homepage": "https://github.com/ngxs-labs/data#readme", 24 | "peerDependencies": { 25 | "@angular-ru/common": ">=15.262.0", 26 | "@ngxs/store": ">=3.7.2", 27 | "typescript": ">=4.3.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export { NgxsDataPluginModule } from './src/ngxs-data.module'; 2 | -------------------------------------------------------------------------------- /lib/repositories/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-repositories", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens", 17 | "@angular-ru/common/utils": "angular-ru-common-utils", 18 | "@angular-ru/common/typings": "angular-ru-common-typings", 19 | "@angular-ru/common/object": "angular-ru-common-object" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/repositories/src/common/abstract-repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActionType, NgxsAfterBootstrap, NgxsOnChanges, NgxsOnInit, NgxsSimpleChange } from '@ngxs/store'; 3 | import { NgxsDataAfterReset, NgxsDataDoCheck, NgxsDataStorageEvent } from '@ngxs-labs/data/typings'; 4 | import { Observable, Subject } from 'rxjs'; 5 | 6 | @Injectable() 7 | export abstract class AbstractRepository implements NgxsOnChanges, NgxsOnInit, NgxsAfterBootstrap { 8 | public browserStorageEvents$: Subject> = new Subject(); 9 | public readonly name!: string; 10 | public readonly initialState!: T; 11 | public readonly state$!: Observable; 12 | public isInitialised: boolean = false; 13 | public isBootstrapped: boolean = false; 14 | 15 | public abstract get snapshot(): T; 16 | 17 | private _dirty: boolean = true; 18 | 19 | protected get dirty(): boolean { 20 | return this._dirty; 21 | } 22 | 23 | protected set dirty(value: boolean) { 24 | this._dirty = value; 25 | } 26 | 27 | public ngxsOnChanges(_?: NgxsSimpleChange): void { 28 | if (this.dirty && this.isBootstrapped) { 29 | this.dirty = false; 30 | (this as NgxsDataDoCheck).ngxsDataDoCheck?.(); 31 | } 32 | } 33 | 34 | public ngxsOnInit(): void { 35 | this.isInitialised = true; 36 | } 37 | 38 | public ngxsAfterBootstrap(): void { 39 | this.isBootstrapped = true; 40 | if (this.dirty) { 41 | this.dirty = false; 42 | (this as NgxsDataDoCheck).ngxsDataDoCheck?.(); 43 | } 44 | } 45 | 46 | public abstract getState(): T; 47 | 48 | public abstract dispatch(actions: ActionType | ActionType[]): Observable; 49 | 50 | public abstract reset(): void; 51 | 52 | protected markAsDirtyAfterReset(): void { 53 | this.dirty = true; 54 | (this as NgxsDataAfterReset).ngxsDataAfterReset?.(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/repositories/src/ngxs-data/ngxs-data.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActionType, StateContext } from '@ngxs/store'; 3 | import { Computed, DataAction, Payload } from '@ngxs-labs/data/decorators'; 4 | import { ensureDataStateContext, ensureSnapshot } from '@ngxs-labs/data/internals'; 5 | import { DataRepository, DataStateContext, PatchValue, StateValue } from '@ngxs-labs/data/typings'; 6 | import { Observable } from 'rxjs'; 7 | 8 | import { AbstractRepository } from '../common/abstract-repository'; 9 | 10 | @Injectable() 11 | export abstract class AbstractNgxsDataRepository 12 | extends AbstractRepository 13 | implements DataStateContext, DataRepository 14 | { 15 | private readonly context!: DataStateContext; 16 | 17 | @Computed() 18 | public get snapshot(): T { 19 | return ensureSnapshot(this.getState()); 20 | } 21 | 22 | protected get ctx(): DataStateContext { 23 | return ensureDataStateContext>(this.context as StateContext); 24 | } 25 | 26 | public getState(): T { 27 | return this.ctx.getState(); 28 | } 29 | 30 | public dispatch(actions: ActionType | ActionType[]): Observable { 31 | return this.ctx.dispatch(actions); 32 | } 33 | 34 | @DataAction() 35 | public patchState(@Payload('patchValue') val: PatchValue): void { 36 | this.ctx.patchState(val); 37 | } 38 | 39 | @DataAction() 40 | public setState(@Payload('stateValue') stateValue: StateValue): void { 41 | this.ctx.setState(stateValue); 42 | } 43 | 44 | @DataAction() 45 | public reset(): void { 46 | this.ctx.setState(this.initialState); 47 | this.markAsDirtyAfterReset(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/repositories/src/ngxs-immutable-data/ngxs-immutable-data.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Any, Immutable } from '@angular-ru/common/typings'; 3 | import { ActionType } from '@ngxs/store'; 4 | import { Computed, DataAction, Payload } from '@ngxs-labs/data/decorators'; 5 | import { ensureDataStateContext, ensureSnapshot } from '@ngxs-labs/data/internals'; 6 | import { 7 | ImmutableDataRepository, 8 | ImmutablePatchValue, 9 | ImmutableStateContext, 10 | ImmutableStateValue 11 | } from '@ngxs-labs/data/typings'; 12 | import { Observable } from 'rxjs'; 13 | 14 | import { AbstractRepository } from '../common/abstract-repository'; 15 | 16 | @Injectable() 17 | export abstract class AbstractNgxsImmutableDataRepository 18 | extends AbstractRepository> 19 | implements ImmutableStateContext, ImmutableDataRepository 20 | { 21 | private readonly context!: ImmutableStateContext; 22 | 23 | @Computed() 24 | public get snapshot(): Immutable { 25 | return ensureSnapshot(this.getState()); 26 | } 27 | 28 | protected get ctx(): ImmutableStateContext { 29 | return ensureDataStateContext(this.context); 30 | } 31 | 32 | public getState(): Immutable { 33 | return this.ctx.getState(); 34 | } 35 | 36 | public dispatch(actions: ActionType | ActionType[]): Observable { 37 | return this.ctx.dispatch(actions); 38 | } 39 | 40 | @DataAction() 41 | public patchState(@Payload('patchValue') val: ImmutablePatchValue): void { 42 | this.ctx.patchState(val); 43 | } 44 | 45 | @DataAction() 46 | public setState(@Payload('stateValue') stateValue: ImmutableStateValue): void { 47 | this.ctx.setState(stateValue); 48 | } 49 | 50 | @DataAction() 51 | public reset(): void { 52 | this.ctx.setState(this.initialState); 53 | this.markAsDirtyAfterReset(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/repositories/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { AbstractRepository as NgxsAbstractDataRepository } from './common/abstract-repository'; 2 | export { AbstractNgxsDataRepository as NgxsDataRepository } from './ngxs-data/ngxs-data.repository'; 3 | export { AbstractNgxsDataEntityCollectionsRepository as NgxsDataEntityCollectionsRepository } from './ngxs-data-entity-collections/ngxs-data-entity-collections.repository'; 4 | export { AbstractNgxsImmutableDataRepository as NgxsImmutableDataRepository } from './ngxs-immutable-data/ngxs-immutable-data.repository'; 5 | -------------------------------------------------------------------------------- /lib/src/ngxs-data.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule, Self } from '@angular/core'; 2 | import { NgxsDataFactory, NgxsDataInjector, NgxsDataSequence } from '@ngxs-labs/data/internals'; 3 | import { NgxsDataExtension } from '@ngxs-labs/data/typings'; 4 | 5 | @NgModule() 6 | export class NgxsDataPluginModule { 7 | constructor(@Self() public accessor: NgxsDataFactory, @Self() public injector: NgxsDataInjector) {} 8 | 9 | public static forRoot(extensions: NgxsDataExtension[] = []): ModuleWithProviders { 10 | return { 11 | ngModule: NgxsDataPluginModule, 12 | providers: [NgxsDataFactory, NgxsDataInjector, NgxsDataSequence, ...extensions] 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-storage", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens", 17 | "@angular-ru/common/utils": "angular-ru-common-utils", 18 | "@angular-ru/common/object": "angular-ru-common-object" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/invalid-data-value.exception.ts: -------------------------------------------------------------------------------- 1 | export class InvalidDataValueException extends Error { 2 | constructor() { 3 | super(`missing key 'data' or it's value not serializable.`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/invalid-last-changed.exception.ts: -------------------------------------------------------------------------------- 1 | export class InvalidLastChangedException extends Error { 2 | constructor(value: string | null) { 3 | super(`lastChanged key not found in object ${value}.`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/invalid-structure-data.exception.ts: -------------------------------------------------------------------------------- 1 | const SPACE: number = 4; 2 | 3 | export class InvalidStructureDataException extends Error { 4 | constructor(message: string) { 5 | super( 6 | `${message}. \nIncorrect structure for deserialization!!! Your structure should be like this: \n${JSON.stringify( 7 | { lastChanged: '2020-01-01T12:00:00.000Z', data: '{}', version: 1 }, 8 | null, 9 | SPACE 10 | )}` 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/invalid-version.exception.ts: -------------------------------------------------------------------------------- 1 | export class InvalidVersionException extends Error { 2 | constructor(value: number | undefined) { 3 | super( 4 | `It's not possible to determine version (${value}), since it must be a integer type and must equal or more than 1.` 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/not-declare-engine.exception.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 2 | 3 | export class NotDeclareEngineException extends Error { 4 | constructor(key: string) { 5 | super(`${NGXS_DATA_EXCEPTIONS.NGXS_PERSISTENCE_ENGINE} \nMetadata { key: '${key}' }`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/exceptions/not-implemented-storage.exception.ts: -------------------------------------------------------------------------------- 1 | export class NotImplementedStorageException extends Error { 2 | constructor() { 3 | super(`StorageEngine instance should be implemented by DataStorageEngine interface`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/ngxs-data-storage-container.ts: -------------------------------------------------------------------------------- 1 | import { PersistenceProvider, StorageContainer } from '@ngxs-labs/data/typings'; 2 | 3 | export class NgxsDataStorageContainer implements StorageContainer { 4 | public providers: Set = new Set(); 5 | public keys: Map = new Map(); 6 | 7 | public getProvidedKeys(): string[] { 8 | return Array.from(this.keys.keys()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/storage/src/public_api.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | 3 | import { NGXS_DATA_STORAGE_CONTAINER } from './tokens/storage-container-provider'; 4 | import { NGXS_DATA_STORAGE_DECODE_TYPE } from './tokens/storage-decode-type'; 5 | import { NGXS_DATA_STORAGE_EXTENSION } from './tokens/storage-extension-provider'; 6 | import { NGXS_DATA_STORAGE_PREFIX } from './tokens/storage-prefix'; 7 | 8 | export { NgxsDataStorageContainer } from './ngxs-data-storage-container'; 9 | export { NgxsDataStoragePlugin } from './ngxs-data-storage-plugin.service'; 10 | export { NGXS_DATA_STORAGE_CONTAINER } from './tokens/storage-container-provider'; 11 | export { NGXS_DATA_STORAGE_CONTAINER_TOKEN } from './tokens/storage-container-token'; 12 | export { NGXS_DATA_STORAGE_DECODE_TYPE } from './tokens/storage-decode-type'; 13 | export { NGXS_DATA_STORAGE_EXTENSION } from './tokens/storage-extension-provider'; 14 | export { DEFAULT_KEY_PREFIX, NGXS_DATA_STORAGE_PREFIX } from './tokens/storage-prefix'; 15 | export { NGXS_DATA_STORAGE_PREFIX_TOKEN } from './tokens/storage-prefix-token'; 16 | export { storageUseFactory } from './tokens/storage-use-factory'; 17 | export { ensurePath } from './utils/ensure-path'; 18 | export { ensureProviders } from './utils/ensure-providers'; 19 | export { isStorageEvent } from './utils/is-storage-event'; 20 | export { registerStorageProviders } from './utils/register-storage-providers'; 21 | 22 | export const NGXS_DATA_STORAGE_PLUGIN: Provider[] = [ 23 | NGXS_DATA_STORAGE_EXTENSION, 24 | NGXS_DATA_STORAGE_CONTAINER, 25 | NGXS_DATA_STORAGE_PREFIX, 26 | NGXS_DATA_STORAGE_DECODE_TYPE 27 | ]; 28 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-container-provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | 3 | import { NGXS_DATA_STORAGE_CONTAINER_TOKEN } from './storage-container-token'; 4 | import { storageUseFactory } from './storage-use-factory'; 5 | 6 | export const NGXS_DATA_STORAGE_CONTAINER: Provider = { 7 | provide: NGXS_DATA_STORAGE_CONTAINER_TOKEN, 8 | useFactory: storageUseFactory 9 | }; 10 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-container-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { StorageContainer } from '@ngxs-labs/data/typings'; 3 | 4 | export const NGXS_DATA_STORAGE_CONTAINER_TOKEN: InjectionToken = new InjectionToken( 5 | 'NGXS_DATA_STORAGE_CONTAINER_TOKEN' 6 | ); 7 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-decode-type-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { STORAGE_DECODE_TYPE } from '@ngxs-labs/data/typings'; 3 | 4 | export const NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN: InjectionToken = new InjectionToken( 5 | 'NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN' 6 | ); 7 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-decode-type.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | import { STORAGE_DECODE_TYPE } from '@ngxs-labs/data/typings'; 3 | 4 | import { NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN } from './storage-decode-type-token'; 5 | 6 | export const NGXS_DATA_STORAGE_DECODE_TYPE: Provider = { 7 | provide: NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN, 8 | useValue: STORAGE_DECODE_TYPE.NONE 9 | }; 10 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-extension-provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | import { NGXS_PLUGINS } from '@ngxs/store'; 3 | 4 | import { NgxsDataStoragePlugin } from '../ngxs-data-storage-plugin.service'; 5 | 6 | export const NGXS_DATA_STORAGE_EXTENSION: Provider = { 7 | provide: NGXS_PLUGINS, 8 | useClass: NgxsDataStoragePlugin, 9 | multi: true 10 | }; 11 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-prefix-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export const NGXS_DATA_STORAGE_PREFIX_TOKEN: InjectionToken = new InjectionToken( 4 | 'NGXS_DATA_STORAGE_PREFIX_TOKEN' 5 | ); 6 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-prefix.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | 3 | import { NGXS_DATA_STORAGE_PREFIX_TOKEN } from './storage-prefix-token'; 4 | 5 | export const DEFAULT_KEY_PREFIX: string = '@ngxs.store.'; 6 | 7 | export const NGXS_DATA_STORAGE_PREFIX: Provider = { 8 | provide: NGXS_DATA_STORAGE_PREFIX_TOKEN, 9 | useValue: DEFAULT_KEY_PREFIX 10 | }; 11 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-ttl-delay.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-magic-numbers 2 | export const STORAGE_TTL_DELAY: number = 1000 * 60; // 1min 3 | -------------------------------------------------------------------------------- /lib/storage/src/tokens/storage-use-factory.ts: -------------------------------------------------------------------------------- 1 | import { NgxsDataStorageContainer } from '../ngxs-data-storage-container'; 2 | 3 | export function storageUseFactory(): NgxsDataStorageContainer { 4 | return new NgxsDataStorageContainer(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/utils/can-be-pull-from-storage.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isFalsy, isNotNil, isTruthy } from '@angular-ru/common/utils'; 3 | import { NgxsDataMigrateStorage, PullFromStorageInfo, PullFromStorageOptions } from '@ngxs-labs/data/typings'; 4 | 5 | import { existTtl } from './exist-ttl'; 6 | import { isExpiredByTtl } from './is-expired'; 7 | 8 | export function canBePullFromStorage(options: PullFromStorageOptions): PullFromStorageInfo { 9 | const { data, provider }: PullFromStorageOptions = options; 10 | const canBeOverrideFromStorage: boolean = isNotNil(data) || provider.nullable!; 11 | 12 | let result: PullFromStorageInfo = { 13 | canBeOverrideFromStorage, 14 | versionMismatch: false, 15 | expired: false, 16 | expiry: null 17 | }; 18 | 19 | result = ensureInfoByTtl(canBeOverrideFromStorage, result, options); 20 | result = ensureInfoByVersionMismatch(canBeOverrideFromStorage, result, options); 21 | 22 | return result; 23 | } 24 | 25 | function ensureInfoByTtl( 26 | canBeOverrideFromStorage: boolean, 27 | result: PullFromStorageInfo, 28 | options: PullFromStorageOptions 29 | ): PullFromStorageInfo { 30 | let newResult: PullFromStorageInfo = result; 31 | const { meta, provider }: PullFromStorageOptions = options; 32 | 33 | if (canBeOverrideFromStorage && existTtl(provider)) { 34 | const expiry: Date = new Date(meta.expiry!); 35 | const expiryExist: boolean = !isNaN(expiry.getTime()); 36 | 37 | if (expiryExist) { 38 | if (isExpiredByTtl(expiry)) { 39 | newResult = { canBeOverrideFromStorage: false, expired: true, expiry, versionMismatch: false }; 40 | } else { 41 | newResult = { canBeOverrideFromStorage, expired: false, expiry, versionMismatch: false }; 42 | } 43 | } 44 | } 45 | 46 | return newResult; 47 | } 48 | 49 | function ensureInfoByVersionMismatch( 50 | canBeOverrideFromStorage: boolean, 51 | result: PullFromStorageInfo, 52 | options: PullFromStorageOptions 53 | ): PullFromStorageInfo { 54 | let newResult: PullFromStorageInfo = result; 55 | const { meta, provider }: PullFromStorageOptions = options; 56 | 57 | if (canBeOverrideFromStorage && meta.version !== provider.version) { 58 | const instance: NgxsDataMigrateStorage | undefined = provider.stateInstance as Any as NgxsDataMigrateStorage; 59 | const tryMigrate: boolean = 60 | isFalsy(provider.skipMigrate) && (isTruthy(instance?.ngxsDataStorageMigrate) || isTruthy(provider.migrate)); 61 | 62 | if (tryMigrate) { 63 | newResult = { ...result, versionMismatch: true }; 64 | } else { 65 | newResult = { ...result, canBeOverrideFromStorage: false, versionMismatch: true }; 66 | } 67 | } 68 | 69 | return newResult; 70 | } 71 | -------------------------------------------------------------------------------- /lib/storage/src/utils/create-default.ts: -------------------------------------------------------------------------------- 1 | import { CreateStorageDefaultOptions, PersistenceProvider, TTL_EXPIRED_STRATEGY } from '@ngxs-labs/data/typings'; 2 | 3 | import { STORAGE_TTL_DELAY } from '../tokens/storage-ttl-delay'; 4 | 5 | // eslint-disable-next-line max-lines-per-function 6 | export function createDefault(options: CreateStorageDefaultOptions): PersistenceProvider[] { 7 | const { meta, decodeType, prefix, stateClassRef }: CreateStorageDefaultOptions = options; 8 | return [ 9 | { 10 | get path(): string | null | undefined { 11 | return meta?.stateMeta?.path; 12 | }, 13 | existingEngine: localStorage, 14 | ttl: -1, 15 | version: 1, 16 | decode: decodeType, 17 | prefixKey: prefix, 18 | nullable: false, 19 | fireInit: true, 20 | rehydrate: true, 21 | ttlDelay: STORAGE_TTL_DELAY, 22 | ttlExpiredStrategy: TTL_EXPIRED_STRATEGY.REMOVE_KEY_AFTER_EXPIRED, 23 | stateClassRef, 24 | skipMigrate: false 25 | } 26 | ] as PersistenceProvider[]; 27 | } 28 | -------------------------------------------------------------------------------- /lib/storage/src/utils/create-ttl-interval.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from '@angular-ru/common/typings'; 2 | import { isNotNil } from '@angular-ru/common/utils'; 3 | import { NgxsDataInjector } from '@ngxs-labs/data/internals'; 4 | import { TtLCreatorOptions } from '@ngxs-labs/data/typings'; 5 | import { interval, Subscription } from 'rxjs'; 6 | 7 | import { ttlHandler } from './ttl-handler'; 8 | 9 | export function createTtlInterval(options: TtLCreatorOptions): void { 10 | const { provider, map }: TtLCreatorOptions = options; 11 | map.get(provider)?.subscription.unsubscribe(); 12 | 13 | const watcher: Fn = (): void => { 14 | const startListen: string = new Date(Date.now()).toISOString(); 15 | 16 | const subscription: Subscription = interval(provider.ttlDelay!).subscribe((): void => 17 | ttlHandler(startListen, options, subscription) 18 | ); 19 | 20 | map.set(provider, { subscription, startListen, endListen: null }); 21 | }; 22 | 23 | if (isNotNil(NgxsDataInjector.ngZone)) { 24 | NgxsDataInjector.ngZone?.runOutsideAngular((): void => watcher()); 25 | } else { 26 | watcher(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/storage/src/utils/deserialize-by-storage-meta.ts: -------------------------------------------------------------------------------- 1 | import { isSimpleObject } from '@angular-ru/common/object'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { checkValueIsEmpty } from '@angular-ru/common/utils'; 4 | import { PersistenceProvider, STORAGE_DECODE_TYPE, StorageData, StorageMeta } from '@ngxs-labs/data/typings'; 5 | 6 | import { InvalidDataValueException } from '../exceptions/invalid-data-value.exception'; 7 | import { InvalidLastChangedException } from '../exceptions/invalid-last-changed.exception'; 8 | import { InvalidStructureDataException } from '../exceptions/invalid-structure-data.exception'; 9 | import { InvalidVersionException } from '../exceptions/invalid-version.exception'; 10 | 11 | export function deserializeByStorageMeta( 12 | meta: StorageMeta, 13 | value: string | null, 14 | provider: PersistenceProvider 15 | ): StorageData | never { 16 | if (isSimpleObject(meta)) { 17 | if (missingLastChanged(meta)) { 18 | throw new InvalidLastChangedException(value); 19 | } else if (versionIsInvalid(meta)) { 20 | throw new InvalidVersionException(meta.version); 21 | } else if (missingDataKey(meta)) { 22 | throw new InvalidDataValueException(); 23 | } 24 | 25 | return provider.decode === STORAGE_DECODE_TYPE.BASE64 ? JSON.parse(atob(meta.data as string)) : meta.data; 26 | } else { 27 | throw new InvalidStructureDataException(`"${value}" not an object`); 28 | } 29 | } 30 | 31 | function versionIsInvalid(meta: StorageMeta): boolean { 32 | const version: number = parseFloat(meta.version as Any); 33 | return isNaN(version) || version < 1 || parseInt(meta.version as Any) !== version; 34 | } 35 | 36 | function missingDataKey(meta: StorageMeta): boolean { 37 | return !('data' in meta); 38 | } 39 | 40 | function missingLastChanged(meta: StorageMeta): boolean { 41 | return !('lastChanged' in meta) || checkValueIsEmpty(meta.lastChanged); 42 | } 43 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ensure-key.ts: -------------------------------------------------------------------------------- 1 | import { PersistenceProvider } from '@ngxs-labs/data/typings'; 2 | 3 | import { ensurePath } from './ensure-path'; 4 | 5 | export function ensureKey(provider: PersistenceProvider): string { 6 | return `${provider.prefixKey}${ensurePath(provider)}`; 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ensure-path.ts: -------------------------------------------------------------------------------- 1 | import { getStateMetadata } from '@ngxs-labs/data/internals'; 2 | import { PersistenceProvider } from '@ngxs-labs/data/typings'; 3 | 4 | export function ensurePath(provider: PersistenceProvider): string { 5 | return provider.path ?? getStateMetadata(provider.stateClassRef!).path!; 6 | } 7 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ensure-providers.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@angular/core'; 2 | import { isNotNil } from '@angular-ru/common/utils'; 3 | import { StateClass } from '@ngxs/store/internals'; 4 | import { NgxsRepositoryMeta, PersistenceProvider, ProviderOptions, STORAGE_DECODE_TYPE } from '@ngxs-labs/data/typings'; 5 | 6 | import { NgxsDataStoragePlugin } from '../ngxs-data-storage-plugin.service'; 7 | import { NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN } from '../tokens/storage-decode-type-token'; 8 | import { DEFAULT_KEY_PREFIX } from '../tokens/storage-prefix'; 9 | import { NGXS_DATA_STORAGE_PREFIX_TOKEN } from '../tokens/storage-prefix-token'; 10 | import { createDefault } from './create-default'; 11 | import { mergeOptions } from './merge-options'; 12 | 13 | // eslint-disable-next-line max-lines-per-function 14 | export function ensureProviders( 15 | meta: NgxsRepositoryMeta, 16 | stateClassRef: Type, 17 | options?: ProviderOptions 18 | ): PersistenceProvider[] { 19 | let providers: PersistenceProvider[]; 20 | const prefix: string = 21 | NgxsDataStoragePlugin.injector?.get(NGXS_DATA_STORAGE_PREFIX_TOKEN, DEFAULT_KEY_PREFIX) ?? DEFAULT_KEY_PREFIX; 22 | 23 | const decodeType: STORAGE_DECODE_TYPE = 24 | NgxsDataStoragePlugin.injector?.get(NGXS_DATA_STORAGE_DECODE_TYPE_TOKEN, STORAGE_DECODE_TYPE.NONE) ?? 25 | STORAGE_DECODE_TYPE.NONE; 26 | 27 | if (isNotNil(options)) { 28 | const prepared: PersistenceProvider[] = Array.isArray(options) ? options : [options]; 29 | providers = prepared.map( 30 | (option: PersistenceProvider): PersistenceProvider => 31 | mergeOptions({ option, prefix, decodeType, meta, stateClassRef }) 32 | ); 33 | } else { 34 | providers = createDefault({ meta, prefix, decodeType, stateClassRef }); 35 | } 36 | 37 | return providers; 38 | } 39 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ensure-serialize-data.ts: -------------------------------------------------------------------------------- 1 | import { isNotNil } from '@angular-ru/common/utils'; 2 | import { PersistenceProvider, STORAGE_DECODE_TYPE, StorageData } from '@ngxs-labs/data/typings'; 3 | 4 | export function ensureSerializeData(data: T | null, provider: PersistenceProvider): StorageData { 5 | const dataLocal: T | null = isNotNil(data) ? data : null; 6 | return provider.decode === STORAGE_DECODE_TYPE.BASE64 ? btoa(JSON.stringify(dataLocal)) : dataLocal; 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/utils/exist-ttl.ts: -------------------------------------------------------------------------------- 1 | import { PersistenceProvider } from '@ngxs-labs/data/typings'; 2 | 3 | export function existTtl(provider: PersistenceProvider): boolean { 4 | return provider.ttl !== -1 && !isNaN(provider.ttl!) && provider.ttl! > 0; 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/utils/expose-engine.ts: -------------------------------------------------------------------------------- 1 | import { Injector } from '@angular/core'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { isNil } from '@angular-ru/common/utils'; 4 | import { 5 | DataStorage, 6 | ExistingEngineProvider, 7 | ExistingStorageEngine, 8 | PersistenceProvider, 9 | UseClassEngineProvider 10 | } from '@ngxs-labs/data/typings'; 11 | 12 | import { NotDeclareEngineException } from '../exceptions/not-declare-engine.exception'; 13 | import { NotImplementedStorageException } from '../exceptions/not-implemented-storage.exception'; 14 | import { ensureKey } from './ensure-key'; 15 | 16 | export function exposeEngine(provider: PersistenceProvider, injector: Injector): ExistingStorageEngine { 17 | const engine: ExistingStorageEngine | null | undefined = 18 | (provider as ExistingEngineProvider).existingEngine ?? 19 | injector.get((provider as UseClassEngineProvider).useClass as Any, null!); 20 | 21 | if (isNil(engine)) { 22 | throw new NotDeclareEngineException(ensureKey(provider)); 23 | } else if (!('getItem' in engine)) { 24 | throw new NotImplementedStorageException(); 25 | } 26 | 27 | return engine; 28 | } 29 | -------------------------------------------------------------------------------- /lib/storage/src/utils/fire-state-when-expired.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isNotNil } from '@angular-ru/common/utils'; 3 | import { NgxsDataInjector } from '@ngxs-labs/data/internals'; 4 | import { NgxsDataAfterExpired, NgxsDataExpiredEvent, TtLCreatorOptions } from '@ngxs-labs/data/typings'; 5 | 6 | export function firedStateWhenExpired(key: string, options: TtLCreatorOptions): void { 7 | const { provider, expiry }: TtLCreatorOptions = options; 8 | 9 | const event: NgxsDataExpiredEvent = { 10 | key, 11 | expiry: expiry?.toISOString(), 12 | timestamp: new Date(Date.now()).toISOString() 13 | }; 14 | 15 | const instance: NgxsDataAfterExpired | undefined = provider.stateInstance as Any as NgxsDataAfterExpired; 16 | instance?.expired$?.next(event); 17 | 18 | if (isNotNil(instance?.ngxsDataAfterExpired)) { 19 | if (isNotNil(NgxsDataInjector.ngZone)) { 20 | NgxsDataInjector.ngZone?.run((): void => instance?.ngxsDataAfterExpired?.(event, provider)); 21 | } else { 22 | instance?.ngxsDataAfterExpired?.(event, provider); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/storage/src/utils/is-expired.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from '@angular-ru/common/utils'; 2 | 3 | export function isExpiredByTtl(expiry?: Date | null): boolean { 4 | return isNil(expiry) ? true : Date.now() >= expiry.getTime(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/storage/src/utils/is-init-action.ts: -------------------------------------------------------------------------------- 1 | import { actionMatcher, ActionType, InitState, UpdateState } from '@ngxs/store'; 2 | 3 | export function isInitAction(action: ActionType): boolean { 4 | const matches: (action: ActionType) => boolean = actionMatcher(action); 5 | return matches(InitState) || matches(UpdateState); 6 | } 7 | -------------------------------------------------------------------------------- /lib/storage/src/utils/is-storage-event.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from '@ngxs/store'; 2 | import { NGXS_DATA_STORAGE_EVENT_TYPE } from '@ngxs-labs/data/tokens'; 3 | 4 | export function isStorageEvent(action: ActionType): boolean { 5 | return action.type === NGXS_DATA_STORAGE_EVENT_TYPE; 6 | } 7 | -------------------------------------------------------------------------------- /lib/storage/src/utils/merge-options.ts: -------------------------------------------------------------------------------- 1 | import { isNotNil } from '@angular-ru/common/utils'; 2 | import { MergeOptions, PersistenceProvider, TTL_EXPIRED_STRATEGY } from '@ngxs-labs/data/typings'; 3 | 4 | import { STORAGE_TTL_DELAY } from '../tokens/storage-ttl-delay'; 5 | import { validatePathInProvider } from './validate-path-in-provider'; 6 | 7 | // eslint-disable-next-line complexity 8 | export function mergeOptions({ option, decodeType, prefix, meta, stateClassRef }: MergeOptions): PersistenceProvider { 9 | const provider: PersistenceProvider = Object.assign(option, { 10 | ttl: isNotNil(option.ttl) ? option.ttl : -1, 11 | version: isNotNil(option.version) ? option.version : 1, 12 | decode: isNotNil(option.decode) ? option.decode : decodeType, 13 | prefixKey: isNotNil(option.prefixKey) ? option.prefixKey : prefix, 14 | nullable: isNotNil(option.nullable) ? option.nullable : false, 15 | fireInit: isNotNil(option.fireInit) ? option.fireInit : true, 16 | rehydrate: isNotNil(option.rehydrate) ? option.rehydrate : true, 17 | ttlDelay: isNotNil(option.ttlDelay) ? option.ttlDelay : STORAGE_TTL_DELAY, 18 | ttlExpiredStrategy: isNotNil(option.ttlExpiredStrategy) 19 | ? option.ttlExpiredStrategy 20 | : TTL_EXPIRED_STRATEGY.REMOVE_KEY_AFTER_EXPIRED, 21 | stateClassRef: isNotNil(option.stateClassRef) ? option.stateClassRef : stateClassRef, 22 | skipMigrate: isNotNil(option.skipMigrate) ? option.skipMigrate : false 23 | }); 24 | 25 | return validatePathInProvider(meta, provider); 26 | } 27 | -------------------------------------------------------------------------------- /lib/storage/src/utils/parse-storage-meta.ts: -------------------------------------------------------------------------------- 1 | import { StorageMeta } from '@ngxs-labs/data/typings'; 2 | 3 | import { InvalidStructureDataException } from '../exceptions/invalid-structure-data.exception'; 4 | 5 | export function parseStorageMeta(value: string | null): StorageMeta | never { 6 | try { 7 | return JSON.parse(value!); 8 | } catch (e) { 9 | throw new InvalidStructureDataException(e.message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/storage/src/utils/register-storage-providers.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 2 | import { PersistenceProvider, StorageContainer } from '@ngxs-labs/data/typings'; 3 | 4 | import { NgxsDataStoragePlugin } from '../ngxs-data-storage-plugin.service'; 5 | import { NGXS_DATA_STORAGE_CONTAINER_TOKEN } from '../tokens/storage-container-token'; 6 | 7 | export function registerStorageProviders(options: PersistenceProvider[]): void { 8 | try { 9 | const container: StorageContainer | undefined = NgxsDataStoragePlugin.injector?.get( 10 | NGXS_DATA_STORAGE_CONTAINER_TOKEN 11 | ); 12 | 13 | options.forEach((option: PersistenceProvider): void => { 14 | container?.providers.add(option); 15 | }); 16 | } catch { 17 | throw new Error(NGXS_DATA_EXCEPTIONS.NGXS_PERSISTENCE_CONTAINER); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/storage/src/utils/rehydrate.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { isFalsy, isTruthy } from '@angular-ru/common/utils'; 3 | import { getValue, setValue } from '@ngxs/store'; 4 | import { PlainObject } from '@ngxs/store/internals'; 5 | import { MigrateFn, NgxsDataMigrateStorage, RehydrateInfo, RehydrateInfoOptions } from '@ngxs-labs/data/typings'; 6 | 7 | import { ensurePath } from './ensure-path'; 8 | 9 | // eslint-disable-next-line max-lines-per-function 10 | export function rehydrate(params: RehydrateInfoOptions): RehydrateInfo { 11 | let states: PlainObject = params.states; 12 | const { provider, data, info }: RehydrateInfoOptions = params; 13 | 14 | if (isFalsy(provider.rehydrate)) { 15 | return { states, rehydrateIn: false }; 16 | } 17 | 18 | const path: string = ensurePath(provider); 19 | const prevData: T = getValue(states, path); 20 | 21 | if (isTruthy(info.versionMismatch)) { 22 | const stateInstance: Any = provider.stateInstance as Any; 23 | const instance: NgxsDataMigrateStorage = stateInstance as NgxsDataMigrateStorage; 24 | const migrateFn: MigrateFn = provider.migrate ?? instance.ngxsDataStorageMigrate?.bind(provider.stateInstance); 25 | const newMigrationData: PlainObject = migrateFn?.(prevData, data); 26 | states = setValue(states, path, newMigrationData); 27 | return { states, rehydrateIn: true }; 28 | } else if (JSON.stringify(prevData) !== JSON.stringify(data)) { 29 | states = setValue(states, path, data); 30 | return { states, rehydrateIn: true }; 31 | } 32 | 33 | return { states, rehydrateIn: false }; 34 | } 35 | -------------------------------------------------------------------------------- /lib/storage/src/utils/silent-deserialize-warning.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 2 | 3 | export function silentDeserializeWarning(key: string, value: string | null, error: string): void { 4 | console.warn( 5 | `${NGXS_DATA_EXCEPTIONS.NGXS_PERSISTENCE_DESERIALIZE} from metadata { key: '${key}', value: '${value}' }. \nError deserialize: ${error}` 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/utils/silent-serialize-warning.ts: -------------------------------------------------------------------------------- 1 | import { NGXS_DATA_EXCEPTIONS } from '@ngxs-labs/data/tokens'; 2 | 3 | export function silentSerializeWarning(key: string, error: string): void { 4 | console.warn( 5 | `${NGXS_DATA_EXCEPTIONS.NGXS_PERSISTENCE_SERIALIZE} from metadata { key: '${key}' }. \nError serialize: ${error}` 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ttl-handler.ts: -------------------------------------------------------------------------------- 1 | import { isNotNil } from '@angular-ru/common/utils'; 2 | import { TtLCreatorOptions } from '@ngxs-labs/data/typings'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { ensureKey } from './ensure-key'; 6 | import { firedStateWhenExpired } from './fire-state-when-expired'; 7 | import { isExpiredByTtl } from './is-expired'; 8 | import { ttlStrategyHandler } from './ttl-strategy-handler'; 9 | 10 | export function ttlHandler(start: string, options: TtLCreatorOptions, subscription: Subscription): void { 11 | const { provider, expiry, map, engine }: TtLCreatorOptions = options; 12 | const key: string = ensureKey(provider); 13 | const value: string | null = engine.getItem(key); 14 | 15 | if (isNotNil(value)) { 16 | if (isExpiredByTtl(expiry)) { 17 | const endListen: string = new Date(Date.now()).toISOString(); 18 | 19 | ttlStrategyHandler(key, value, options); 20 | firedStateWhenExpired(key, options); 21 | 22 | subscription.unsubscribe(); 23 | map.set(provider, { subscription, startListen: start, endListen }); 24 | } 25 | } else { 26 | subscription.unsubscribe(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/storage/src/utils/ttl-strategy-handler.ts: -------------------------------------------------------------------------------- 1 | import { StorageMeta, TTL_EXPIRED_STRATEGY, TtLCreatorOptions } from '@ngxs-labs/data/typings'; 2 | 3 | import { parseStorageMeta } from './parse-storage-meta'; 4 | 5 | export function ttlStrategyHandler(key: string, value: string | null, options: TtLCreatorOptions): void { 6 | const { provider, engine }: TtLCreatorOptions = options; 7 | 8 | switch (provider.ttlExpiredStrategy) { 9 | case TTL_EXPIRED_STRATEGY.REMOVE_KEY_AFTER_EXPIRED: 10 | engine.removeItem(key); 11 | break; 12 | case TTL_EXPIRED_STRATEGY.SET_NULL_DATA_AFTER_EXPIRED: 13 | // eslint-disable-next-line no-case-declarations 14 | const meta: StorageMeta = parseStorageMeta(value); 15 | meta.data = null; 16 | engine.setItem(key, JSON.stringify(meta)); 17 | break; 18 | case TTL_EXPIRED_STRATEGY.DO_NOTHING_AFTER_EXPIRED: 19 | default: 20 | break; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/storage/src/utils/validate-path-in-provider.ts: -------------------------------------------------------------------------------- 1 | import { NgxsRepositoryMeta, PersistenceProvider } from '@ngxs-labs/data/typings'; 2 | 3 | export function validatePathInProvider(meta: NgxsRepositoryMeta, provider: PersistenceProvider): PersistenceProvider { 4 | let newProvider: PersistenceProvider = provider; 5 | 6 | if (!('path' in newProvider)) { 7 | newProvider = { 8 | ...newProvider, 9 | get path(): string | null | undefined { 10 | return meta?.stateMeta?.path; 11 | } 12 | }; 13 | } 14 | 15 | return newProvider; 16 | } 17 | -------------------------------------------------------------------------------- /lib/testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-testing", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/testing/src/platform/internal/create-internal-ngxs-root-element.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { ɵgetDOM as getDOM } from '@angular/platform-browser'; 4 | 5 | export function createInternalNgxsRootElement(): void { 6 | const document: Document = TestBed.inject(DOCUMENT); 7 | const root: HTMLElement = getDOM().createElement('app-root', document); 8 | document.body.appendChild(root); 9 | } 10 | -------------------------------------------------------------------------------- /lib/testing/src/platform/internal/remove-internal-ngxs-root-element.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | export function removeInternalNgxsRootElement(): void { 5 | const document: Document = TestBed.inject(DOCUMENT); 6 | const root: Element = document.getElementsByTagName('app-root').item(0)!; 7 | try { 8 | document.body.removeChild(root); 9 | } catch {} 10 | } 11 | -------------------------------------------------------------------------------- /lib/testing/src/platform/internal/types.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | 3 | export type TestSpec = (...args: Any[]) => Promise; 4 | -------------------------------------------------------------------------------- /lib/testing/src/platform/ngxs-app-mock.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: '', 6 | changeDetection: ChangeDetectionStrategy.OnPush 7 | }) 8 | export class NgxsAppMockComponent implements OnInit, AfterViewInit { 9 | // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method 10 | public ngOnInit(): void { 11 | // noop 12 | } 13 | 14 | // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method 15 | public ngAfterViewInit(): void { 16 | // noop 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/testing/src/platform/ngxs-app-mock.module.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { NgxsAppMockComponent } from './ngxs-app-mock.component'; 5 | 6 | @NgModule({ 7 | imports: [BrowserModule], 8 | declarations: [NgxsAppMockComponent], 9 | entryComponents: [NgxsAppMockComponent] 10 | }) 11 | export class NgxsAppMockModule { 12 | // eslint-disable-next-line @angular-eslint/use-lifecycle-interface 13 | public static ngDoBootstrap(app: ApplicationRef): void { 14 | app.bootstrap(NgxsAppMockComponent); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/testing/src/platform/ngxs-data-testing.module.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationRef, destroyPlatform, ModuleWithProviders, NgModule, Type } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { StateClass } from '@ngxs/store/internals'; 5 | import { NgxsDataPluginModule } from '@ngxs-labs/data'; 6 | import { NGXS_DATA_STORAGE_CONTAINER, NGXS_DATA_STORAGE_EXTENSION } from '@ngxs-labs/data/storage'; 7 | 8 | import { createInternalNgxsRootElement } from './internal/create-internal-ngxs-root-element'; 9 | import { NgxsAppMockModule } from './ngxs-app-mock.module'; 10 | 11 | type NgxsDataTestingModuleProviders = [ 12 | Type, 13 | ModuleWithProviders, 14 | ModuleWithProviders 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [NgxsModule], 19 | exports: [NgxsModule] 20 | }) 21 | export class NgxsDataTestingModule { 22 | public static forRoot(states: StateClass[] = []): NgxsDataTestingModuleProviders { 23 | return [ 24 | NgxsAppMockModule, 25 | NgxsModule.forRoot(states, { developmentMode: true, selectorOptions: { suppressErrors: false } }), 26 | NgxsDataPluginModule.forRoot([NGXS_DATA_STORAGE_EXTENSION, NGXS_DATA_STORAGE_CONTAINER]) 27 | ]; 28 | } 29 | 30 | public static ngxsInitPlatform(): void { 31 | destroyPlatform(); 32 | createInternalNgxsRootElement(); 33 | // eslint-disable-next-line @angular-eslint/no-lifecycle-call 34 | NgxsAppMockModule.ngDoBootstrap(TestBed.inject(ApplicationRef)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/testing/src/platform/reset-platform-after-bootrapping.ts: -------------------------------------------------------------------------------- 1 | import { createPlatform, destroyPlatform } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | 4 | import { removeInternalNgxsRootElement } from './internal/remove-internal-ngxs-root-element'; 5 | 6 | export function resetPlatformAfterBootstrapping(): void { 7 | removeInternalNgxsRootElement(); 8 | destroyPlatform(); 9 | createPlatform(TestBed); 10 | } 11 | -------------------------------------------------------------------------------- /lib/testing/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { createInternalNgxsRootElement } from './platform/internal/create-internal-ngxs-root-element'; 2 | export { removeInternalNgxsRootElement } from './platform/internal/remove-internal-ngxs-root-element'; 3 | export { NgxsDataTestingModule } from './platform/ngxs-data-testing.module'; 4 | export { ngxsTestingPlatform } from './platform/ngxs-platform'; 5 | export { resetPlatformAfterBootstrapping } from './platform/reset-platform-after-bootrapping'; 6 | -------------------------------------------------------------------------------- /lib/tokens/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-tokens", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/tokens/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { NGXS_DATA_STORAGE_EVENT_TYPE } from './symbols/need-sync-type-action'; 2 | export { NGXS_COMPUTED_OPTION } from './symbols/ngxs-computed-options'; 3 | export { NgxsDataExceptions as NGXS_DATA_EXCEPTIONS } from './symbols/ngxs-data-exceptions'; 4 | export { NGXS_DATA_META } from './symbols/ngxs-data-meta'; 5 | export { NGXS_META_KEY } from './symbols/ngxs-meta-key'; 6 | export { NGXS_ARGUMENT_REGISTRY_META } from './symbols/ngxs-meta-payload'; 7 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/need-sync-type-action.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @publicApi 3 | */ 4 | export const NGXS_DATA_STORAGE_EVENT_TYPE: string = 'NGXS_DATA_STORAGE_EVENT_TYPE'; 5 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/ngxs-computed-options.ts: -------------------------------------------------------------------------------- 1 | export const NGXS_COMPUTED_OPTION: 'NGXS_COMPUTED_OPTION' = 'NGXS_COMPUTED_OPTION' as const; 2 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/ngxs-data-exceptions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @privateApi 3 | */ 4 | export const enum NgxsDataExceptions { 5 | NGXS_PERSISTENCE_STATE = '@Persistence should be add before decorator @State and @StateRepository', 6 | NGXS_DATA_STATE = '@StateRepository should be add before decorator @State', 7 | NGXS_DATA_STATE_NAME_NOT_FOUND = 'State name not provided in class', 8 | NGXS_DATA_MODULE_EXCEPTION = 'Metadata not created \n Maybe you forgot to import the NgxsDataPluginModule' + 9 | '\n Also, you cannot use this.ctx.* until the application is fully rendered ' + 10 | '\n (use by default ngxsOnInit(ctx: StateContext), or ngxsAfterBootstrap(ctx: StateContext) !!!', 11 | NGXS_DATA_STATE_DECORATOR = 'You forgot add decorator @StateRepository or initialize state!' + 12 | '\nExample: NgxsModule.forRoot([ .. ]), or NgxsModule.forFeature([ .. ])', 13 | NGXS_DATA_STATIC_ACTION = 'Cannot support static methods with @DataAction()', 14 | NGXS_DATA_ACTION = '@DataAction() can only decorate a method implementation', 15 | NGXS_DATA_ASYNC_ACTION_RETURN_TYPE = 'WARNING: If you use asynchronous actions' + 16 | ' `@Debounce() @DataAction()` the return result type should only void instead:', 17 | NGXS_PERSISTENCE_CONTAINER = 'You forgot provide NGXS_DATA_STORAGE_CONTAINER or NGXS_DATA_STORAGE_EXTENSION!!! Example: \n' + 18 | '\n@NgModule({' + 19 | '\n imports: [ ' + 20 | '\n NgxsDataPluginModule.forRoot([NGXS_DATA_STORAGE_PLUGIN]) ' + 21 | '\n ]\n' + 22 | '})' + 23 | '\nexport class AppModule {} \n\n', 24 | NGXS_PERSISTENCE_ENGINE = 'Not found storage engine from `existingEngine` or not found instance after injecting by `useClass`.', 25 | NGXS_PERSISTENCE_SERIALIZE = 'Error occurred while serializing value', 26 | NGXS_PERSISTENCE_DESERIALIZE = 'Error occurred while deserializing value', 27 | NGXS_DATA_CHILDREN_CONVERT = 'Child states can only be added to an object', 28 | NGXS_INVALID_PAYLOAD_NAME = 'Payload name should be initialized', 29 | NGXS_INVALID_ARG_NAME = 'Argument name should be initialized', 30 | NGXS_COMPUTED_DECORATOR = 'The method must be a getter for the computed decorator to work properly.', 31 | NGXS_COMPARE = 'You must set the compare function before sorting.' 32 | } 33 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/ngxs-data-meta.ts: -------------------------------------------------------------------------------- 1 | export const NGXS_DATA_META: 'NGXS_DATA_META' = 'NGXS_DATA_META' as const; 2 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/ngxs-meta-key.ts: -------------------------------------------------------------------------------- 1 | export const NGXS_META_KEY: 'NGXS_META' = 'NGXS_META' as const; 2 | -------------------------------------------------------------------------------- /lib/tokens/src/symbols/ngxs-meta-payload.ts: -------------------------------------------------------------------------------- 1 | export const NGXS_ARGUMENT_REGISTRY_META: 'NGXS_ARGUMENT_REGISTRY_META' = 'NGXS_ARGUMENT_REGISTRY_META' as const; 2 | -------------------------------------------------------------------------------- /lib/typings/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/package.schema.json", 3 | "ngPackage": { 4 | "lib": { 5 | "flatModuleFile": "ngxs-labs-data-typings", 6 | "umdModuleIds": { 7 | "@ngxs/store": "ngxs-store", 8 | "@ngxs/store/internals": "ngxs-store-internals", 9 | "@ngxs-labs/data": "ngxs-labs-data", 10 | "@ngxs-labs/data/testing": "ngxs-labs-data-testing", 11 | "@ngxs-labs/data/typings": "ngxs-labs-data-typings", 12 | "@ngxs-labs/data/storage": "ngxs-labs-data-storage", 13 | "@ngxs-labs/data/internals": "ngxs-labs-data-internals", 14 | "@ngxs-labs/data/repositories": "ngxs-labs-data-repositories", 15 | "@ngxs-labs/data/decorators": "ngxs-labs-data-decorators", 16 | "@ngxs-labs/data/tokens": "ngxs-labs-data-tokens" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/typings/src/common/actions-properties.ts: -------------------------------------------------------------------------------- 1 | import { Any, PlainObjectOf } from '@angular-ru/common/typings'; 2 | import { ActionOptions, ActionType } from '@ngxs/store'; 3 | 4 | /** 5 | * @publicApi 6 | */ 7 | export interface RepositoryActionOptions extends ActionOptions { 8 | insideZone?: boolean; 9 | } 10 | 11 | /** 12 | * @publicApi 13 | */ 14 | export type ActionEvent = (ActionType & { payload: PlainObjectOf }) | ActionType; 15 | 16 | export type ActionName = string; 17 | export type PayloadName = string; 18 | export type ArgName = string; 19 | 20 | export type PayloadMap = Map; 21 | export type ArgNameMap = Map; 22 | -------------------------------------------------------------------------------- /lib/typings/src/common/computed-cache-map.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from '@angular-ru/common/typings'; 2 | 3 | import { ComputedOptions } from './computed-options'; 4 | 5 | export type ComputedCacheMap = WeakMap; 6 | -------------------------------------------------------------------------------- /lib/typings/src/common/computed-options.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export interface ComputedOptions { 5 | sequenceId: number; 6 | isObservable: boolean; 7 | value: Any | Observable; 8 | } 9 | -------------------------------------------------------------------------------- /lib/typings/src/common/data-state-class.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { MetaDataModel, StateClassInternal } from '@ngxs/store/src/internal/internals'; 4 | import { NGXS_DATA_META, NGXS_META_KEY } from '@ngxs-labs/data/tokens'; 5 | 6 | import { NgxsRepositoryMeta } from './repository'; 7 | 8 | export interface DataStateClass extends StateClassInternal { 9 | [NGXS_DATA_META]?: NgxsRepositoryMeta; 10 | [NGXS_META_KEY]?: MetaDataModel; 11 | } 12 | 13 | export type StateClassDecorator = (stateClass: DataStateClass) => void; 14 | 15 | export type StateArgumentDecorator = ( 16 | stateClass: DataStateClass, 17 | propertyKey: string | symbol, 18 | parameterIndex: number 19 | ) => void; 20 | -------------------------------------------------------------------------------- /lib/typings/src/common/dispatched-result.ts: -------------------------------------------------------------------------------- 1 | import { Any } from '@angular-ru/common/typings'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export type DispatchedResult = Any | Observable | null; 5 | -------------------------------------------------------------------------------- /lib/typings/src/common/extension.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | 3 | export type NgxsDataExtension = Provider | Provider[]; 4 | -------------------------------------------------------------------------------- /lib/typings/src/common/mapped-state.ts: -------------------------------------------------------------------------------- 1 | import { MappedStore } from '@ngxs/store/src/internal/internals'; 2 | 3 | export type MappedState = MappedStore | null | undefined; 4 | -------------------------------------------------------------------------------- /lib/typings/src/common/ngxs-data-lifecycle.ts: -------------------------------------------------------------------------------- 1 | export interface NgxsDataDoCheck { 2 | ngxsDataDoCheck?(): void; 3 | } 4 | 5 | export interface NgxsDataAfterReset { 6 | ngxsDataAfterReset?(): void; 7 | } 8 | -------------------------------------------------------------------------------- /lib/typings/src/common/repository.ts: -------------------------------------------------------------------------------- 1 | import { Immutable, PlainObjectOf } from '@angular-ru/common/typings'; 2 | import { ActionOptions, ActionType } from '@ngxs/store'; 3 | import { MetaDataModel, StateClassInternal } from '@ngxs/store/src/internal/internals'; 4 | import { Observable } from 'rxjs'; 5 | 6 | /** 7 | * @publicApi 8 | */ 9 | export interface NgxsDataOperation { 10 | type: string; 11 | options: ActionOptions; 12 | } 13 | 14 | /** 15 | * @publicApi 16 | */ 17 | export interface NgxsRepositoryMeta { 18 | stateMeta?: MetaDataModel; 19 | operations?: PlainObjectOf; 20 | stateClass?: StateClassInternal; 21 | } 22 | 23 | export interface ImmutableDataRepository { 24 | name: string; 25 | initialState: Immutable; 26 | state$: Observable>; 27 | readonly snapshot: Immutable; 28 | 29 | getState(): Immutable; 30 | 31 | dispatch(actions: ActionType | ActionType[]): Observable; 32 | 33 | patchState(val: ImmutablePatchValue): void; 34 | 35 | setState(stateValue: ImmutableStateValue): void; 36 | 37 | reset(): void; 38 | } 39 | 40 | export type ImmutablePatchValue = Partial>; 41 | export type ImmutableStateValue = T | Immutable | ((state: Immutable) => Immutable | T); 42 | 43 | export interface ImmutableStateContext { 44 | getState(): Immutable; 45 | 46 | setState(val: ImmutableStateValue): void; 47 | 48 | patchState(val: ImmutablePatchValue): void; 49 | 50 | dispatch(actions: ActionType | ActionType[]): Observable; 51 | } 52 | 53 | export interface DataRepository { 54 | name: string; 55 | initialState: T; 56 | state$: Observable; 57 | readonly snapshot: T; 58 | 59 | getState(): T; 60 | 61 | dispatch(actions: ActionType | ActionType[]): Observable; 62 | 63 | patchState(val: PatchValue): void; 64 | 65 | setState(stateValue: StateValue): void; 66 | 67 | reset(): void; 68 | } 69 | 70 | export type PatchValue = Partial>; 71 | export type StateValue = T | Immutable | ((state: T) => T | Immutable); 72 | 73 | export interface DataStateContext { 74 | getState(): T; 75 | 76 | setState(val: StateValue): void; 77 | 78 | patchState(val: PatchValue): void; 79 | 80 | dispatch(actions: ActionType | ActionType[]): Observable; 81 | } 82 | -------------------------------------------------------------------------------- /lib/typings/src/entity/entity-context.ts: -------------------------------------------------------------------------------- 1 | import { EntityCollections, EntityPatchValue, EntityStateValue } from '@angular-ru/common/entity'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { ActionType } from '@ngxs/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | export interface EntityContext> { 7 | getState(): EntityCollections; 8 | 9 | setState(val: EntityStateValue>): void; 10 | 11 | patchState(val: EntityPatchValue>): void; 12 | 13 | dispatch(actions: ActionType | ActionType[]): Observable; 14 | } 15 | -------------------------------------------------------------------------------- /lib/typings/src/entity/entity-repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityCollections, EntityUpdate } from '@angular-ru/common/entity'; 2 | import { Any } from '@angular-ru/common/typings'; 3 | import { ActionType } from '@ngxs/store'; 4 | import { Observable } from 'rxjs'; 5 | 6 | export interface EntityRepository> { 7 | name: string; 8 | initialState: EntityCollections; 9 | state$: Observable>; 10 | readonly snapshot: EntityCollections; 11 | primaryKey: string; 12 | 13 | getState(): EntityCollections; 14 | 15 | dispatch(actions: ActionType | ActionType[]): Observable; 16 | 17 | reset(): void; 18 | 19 | selectId(entity: V): K; 20 | 21 | addOne(entity: V): void; 22 | 23 | addMany(entities: V[]): void; 24 | 25 | setOne(entity: V): void; 26 | 27 | setMany(entities: V[]): void; 28 | 29 | setAll(entities: V[]): void; 30 | 31 | updateOne(update: EntityUpdate): void; 32 | 33 | updateMany(updates: EntityUpdate[]): void; 34 | 35 | upsertOne(entity: V): void; 36 | 37 | upsertMany(entities: V[]): void; 38 | 39 | removeOne(id: K): void; 40 | 41 | removeByEntity(entity: V): void; 42 | 43 | removeMany(ids: K[]): void; 44 | 45 | removeByEntities(entities: V[]): void; 46 | 47 | removeAll(): void; 48 | } 49 | -------------------------------------------------------------------------------- /lib/typings/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ActionEvent, 3 | ActionName, 4 | ArgName, 5 | ArgNameMap, 6 | PayloadMap, 7 | PayloadName, 8 | RepositoryActionOptions 9 | } from './common/actions-properties'; 10 | export { ComputedCacheMap } from './common/computed-cache-map'; 11 | export { ComputedOptions } from './common/computed-options'; 12 | export { DataStateClass, StateArgumentDecorator, StateClassDecorator } from './common/data-state-class'; 13 | export { DispatchedResult } from './common/dispatched-result'; 14 | export { NgxsDataExtension } from './common/extension'; 15 | export { MappedState } from './common/mapped-state'; 16 | export { NgxsDataAfterReset, NgxsDataDoCheck } from './common/ngxs-data-lifecycle'; 17 | export { 18 | DataRepository, 19 | DataStateContext, 20 | ImmutableDataRepository, 21 | ImmutablePatchValue, 22 | ImmutableStateContext, 23 | ImmutableStateValue, 24 | NgxsDataOperation, 25 | NgxsRepositoryMeta, 26 | PatchValue, 27 | StateValue 28 | } from './common/repository'; 29 | export { EntityContext } from './entity/entity-context'; 30 | export { EntityRepository } from './entity/entity-repository'; 31 | export { 32 | CheckExpiredInitOptions, 33 | CreateStorageDefaultOptions, 34 | DataStorage, 35 | DataStoragePlugin, 36 | ExistingEngineProvider, 37 | ExistingStorageEngine, 38 | GlobalStorageOptionsHandler, 39 | MergeOptions, 40 | MigrateFn, 41 | NgxsDataAfterExpired, 42 | NgxsDataAfterStorageEvent, 43 | NgxsDataExpiredEvent, 44 | NgxsDataMigrateStorage, 45 | NgxsDataStorageEvent, 46 | PersistenceProvider, 47 | ProviderOptions, 48 | PullFromStorageInfo, 49 | PullFromStorageOptions, 50 | PullStorageMeta, 51 | RehydrateInfo, 52 | RehydrateInfoOptions, 53 | StorageDecodeType as STORAGE_DECODE_TYPE, 54 | StorageContainer, 55 | StorageData, 56 | StorageMeta, 57 | TtlExpiredStrategy as TTL_EXPIRED_STRATEGY, 58 | TtLCreatorOptions, 59 | TtlListenerOptions, 60 | UseClassEngineProvider 61 | } from './storage/storage'; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngxs-data", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build:app": "ng build", 7 | "build:lib": "ng build library && cp README.md dist/ngxs-data", 8 | "coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 9 | "format": "npx sort-package-json && prettier --write \"**/*.{js,ts,html,css,scss,md,json,yml}\"", 10 | "postinstall": "ngcc --async", 11 | "lint": "eslint --cache \"**/*.ts\"", 12 | "start": "ng serve --open --port 4400", 13 | "test": "jest --config ./jest.config.js" 14 | }, 15 | "browserslist": [ 16 | "defaults", 17 | "not IE 11", 18 | "maintained node versions" 19 | ], 20 | "prettier": "@angular-ru/prettier-config", 21 | "devDependencies": { 22 | "@angular-devkit/build-angular": "12.2.4", 23 | "@angular-devkit/schematics-cli": "12.2.4", 24 | "@angular-ru/build-tools": "15.307.1", 25 | "@angular-ru/common": "15.313.0", 26 | "@angular-ru/eslint-config": "15.306.2", 27 | "@angular-ru/jest-utils": "15.304.1", 28 | "@angular-ru/prettier-config": "15.304.1", 29 | "@angular-ru/tsconfig": "15.303.0", 30 | "@angular/animations": "12.2.4", 31 | "@angular/cdk": "12.2.4", 32 | "@angular/cli": "12.2.4", 33 | "@angular/common": "12.2.4", 34 | "@angular/compiler": "12.2.4", 35 | "@angular/compiler-cli": "12.2.4", 36 | "@angular/core": "12.2.4", 37 | "@angular/forms": "12.2.4", 38 | "@angular/language-service": "12.2.4", 39 | "@angular/material": "12.2.4", 40 | "@angular/platform-browser": "12.2.4", 41 | "@angular/platform-browser-dynamic": "12.2.4", 42 | "@angular/router": "12.2.4", 43 | "@ngxs/logger-plugin": "3.7.2", 44 | "@ngxs/store": "3.7.2", 45 | "@schematics/angular": "12.2.3", 46 | "@types/node": "15.14.9", 47 | "angular-cli-ghpages": "1.0.0-rc.2", 48 | "core-js": "3.16.3", 49 | "coveralls": "3.1.1", 50 | "hammerjs": "2.0.8", 51 | "ng-packagr": "12.2.1", 52 | "npx": "10.2.2", 53 | "rxjs": "6.6.7", 54 | "sort-package-json": "1.50.0", 55 | "ts-node": "10.2.1", 56 | "tsickle": "0.43.0", 57 | "tslib": "2.3.1", 58 | "typescript": "4.3.5", 59 | "zone.js": "0.11.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>Angular-RU/angular-renovate-config"], 3 | "baseBranches": ["master"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [], 6 | "paths": { 7 | "@ngxs-labs/data": ["./lib/public_api.ts"], 8 | "@ngxs-labs/data/*": ["./lib/*/src/public_api.ts"] 9 | } 10 | }, 11 | "files": ["integration/app/main.ts", "integration/app/polyfills.ts"], 12 | "include": ["integration/**/*.d.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@angular-ru/tsconfig", 3 | "angularCompilerOptions": { 4 | "strictTemplates": true, 5 | "fullTemplateTypeCheck": true, 6 | "annotateForClosureCompiler": true, 7 | "strictInjectionParameters": true, 8 | "skipTemplateCodegen": false, 9 | "preserveWhitespaces": true, 10 | "skipMetadataEmit": false, 11 | "disableTypeScriptVersionCheck": true, 12 | "enableIvy": true, 13 | "compilationMode": "partial" 14 | }, 15 | "compilerOptions": { 16 | "baseUrl": "./", 17 | "outDir": "./dist/out-tsc", 18 | "target": "es2015", 19 | "lib": ["es2017", "dom"], 20 | "module": "esnext", 21 | "typeRoots": ["./node_modules/@types"], 22 | "paths": { 23 | "@ngxs-labs/data": ["./lib/public_api.ts"], 24 | "@ngxs-labs/data/testing": ["./lib/testing/src/public_api.ts"], 25 | "@ngxs-labs/data/typings": ["./lib/typings/src/public_api.ts"], 26 | "@ngxs-labs/data/storage": ["./lib/storage/src/public_api.ts"], 27 | "@ngxs-labs/data/internals": ["./lib/internals/src/public_api.ts"], 28 | "@ngxs-labs/data/repositories": ["./lib/repositories/src/public_api.ts"], 29 | "@ngxs-labs/data/decorators": ["./lib/decorators/src/public_api.ts"], 30 | "@ngxs-labs/data/tokens": ["./lib/tokens/src/public_api.ts"] 31 | } 32 | }, 33 | "exclude": ["integration/types"] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@angular-ru/tsconfig", 3 | "references": [ 4 | { 5 | "path": "./tsconfig.base.json" 6 | }, 7 | { 8 | "path": "./tsconfig.spec.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/lib", 5 | "target": "es2015", 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": ["dom", "es2018"] 10 | }, 11 | "angularCompilerOptions": { 12 | "annotateForClosureCompiler": true, 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "fullTemplateTypeCheck": true, 16 | "strictInjectionParameters": true, 17 | "enableResourceInlining": true, 18 | "enableIvy": true, 19 | "compilationMode": "partial" 20 | }, 21 | "exclude": ["**/*.spec.ts", "**/schematics/**"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"], 5 | "preserveConstEnums": true 6 | }, 7 | "include": ["**/*.spec.ts", "**/*.d.ts"] 8 | } 9 | --------------------------------------------------------------------------------