├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yaml │ ├── 2-feature-request.yaml │ └── 3-support-request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── deploy-sample-app.yml │ └── docs.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── BREAKING_CHANGES.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING-ARCHIVED.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── changelog.config.js ├── docs ├── .gitignore ├── README.md ├── docs │ ├── additional │ │ ├── array.mdx │ │ ├── cache.mdx │ │ ├── class.mdx │ │ ├── events.mdx │ │ ├── js.mdx │ │ ├── middleware.mdx │ │ ├── notifications.mdx │ │ ├── operators.mdx │ │ ├── optimstic-updates.mdx │ │ └── reset.mdx │ ├── angular │ │ ├── architecture.mdx │ │ ├── effects.mdx │ │ ├── entity-service.mdx │ │ ├── hmr.mdx │ │ ├── local-state.mdx │ │ ├── router.mdx │ │ └── tests.mdx │ ├── best-practices.mdx │ ├── config.mdx │ ├── enhancers │ │ ├── cli.mdx │ │ ├── devtools.mdx │ │ ├── persist-state.mdx │ │ └── snapshot.mdx │ ├── entities │ │ ├── active.mdx │ │ ├── entity-store.mdx │ │ ├── query-entity.mdx │ │ └── sorting.mdx │ ├── immer.mdx │ ├── installation.mdx │ ├── plugins │ │ ├── dirty-check.mdx │ │ ├── pagination.mdx │ │ └── state-history.mdx │ ├── query.mdx │ ├── store.mdx │ ├── transactions.mdx │ └── ui.mdx ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── components │ │ ├── architecture-svg.js │ │ └── highlight.js │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ ├── sample-app.js │ │ └── styles.module.css ├── static │ ├── .nojekyll │ └── img │ │ ├── akita-arc.jpg │ │ ├── akita.png │ │ ├── akita.svg │ │ ├── devtools.gif │ │ └── favicon.ico └── yarn.lock ├── jest.config.ts ├── jest.preset.js ├── migrations.json ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── .gitkeep ├── akita │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── __tests__ │ │ │ ├── addEntities.spec.ts │ │ │ ├── akitaConfig.spec.ts │ │ │ ├── arrayAdd.spec.ts │ │ │ ├── arrayFind.spec.ts │ │ │ ├── arrayRemove.spec.ts │ │ │ ├── arrayToggle.spec.ts │ │ │ ├── arrayUpdate.spec.ts │ │ │ ├── arrayUpsert.spec.ts │ │ │ ├── booksStore.ts │ │ │ ├── classBased.spec.ts │ │ │ ├── customId.spec.ts │ │ │ ├── deepFreeze.spec.ts │ │ │ ├── devtools.spec.ts │ │ │ ├── dirtyCheck.spec.ts │ │ │ ├── entityActions.spec.ts │ │ │ ├── entityCollectionPlugin.spec.ts │ │ │ ├── entityStore.spec.ts │ │ │ ├── entityUIStore.spec.ts │ │ │ ├── env.spec.ts │ │ │ ├── mocks.ts │ │ │ ├── moveEntity.spec.ts │ │ │ ├── multiActive.spec.ts │ │ │ ├── multiProps.spec.ts │ │ │ ├── pagination.spec.ts │ │ │ ├── persistState.spec.ts │ │ │ ├── persistStateAsync.spec.ts │ │ │ ├── persistStateInclude.spec.ts │ │ │ ├── persistStateKey.spec.ts │ │ │ ├── persistStateSelect.spec.ts │ │ │ ├── persistStateStore.spec.ts │ │ │ ├── query.spec.ts │ │ │ ├── queryEntity.spec.ts │ │ │ ├── queryLastFirst.spec.ts │ │ │ ├── removeEntities.spec.ts │ │ │ ├── resetStore.spec.ts │ │ │ ├── resetStores.spec.ts │ │ │ ├── runStoreAction.spec.ts │ │ │ ├── selectMany.spec.ts │ │ │ ├── setActiveEntities.spec.ts │ │ │ ├── setEntities.spec.ts │ │ │ ├── setLoading.spec.ts │ │ │ ├── setup.ts │ │ │ ├── snapshotManager.spec.ts │ │ │ ├── stateHistory.spec.ts │ │ │ ├── store.spec.ts │ │ │ ├── storeMiddleware.spec.ts │ │ │ ├── transaction.spec.ts │ │ │ ├── update.spec.ts │ │ │ ├── updateEntities.spec.ts │ │ │ ├── upsert.spec.ts │ │ │ └── utils.spec.ts │ │ ├── index.ts │ │ └── lib │ │ │ ├── actions.ts │ │ │ ├── activeState.ts │ │ │ ├── addEntities.ts │ │ │ ├── arrayAdd.ts │ │ │ ├── arrayFind.ts │ │ │ ├── arrayRemove.ts │ │ │ ├── arrayToggle.ts │ │ │ ├── arrayUpdate.ts │ │ │ ├── arrayUpsert.ts │ │ │ ├── cacheable.ts │ │ │ ├── capitalize.ts │ │ │ ├── coerceArray.ts │ │ │ ├── combineQueries.ts │ │ │ ├── compareKeys.ts │ │ │ ├── config.ts │ │ │ ├── deepFreeze.ts │ │ │ ├── defaultIDKey.ts │ │ │ ├── devtools.ts │ │ │ ├── dispatchers.ts │ │ │ ├── entitiesToArray.ts │ │ │ ├── entitiesToMap.ts │ │ │ ├── entityActions.ts │ │ │ ├── entityService.ts │ │ │ ├── entityStore.ts │ │ │ ├── env.ts │ │ │ ├── errors.ts │ │ │ ├── filterNil.ts │ │ │ ├── fp.ts │ │ │ ├── getActiveEntities.ts │ │ │ ├── getEntity.ts │ │ │ ├── getInitialEntitiesState.ts │ │ │ ├── getValueByString.ts │ │ │ ├── guid.ts │ │ │ ├── hasEntity.ts │ │ │ ├── index.ts │ │ │ ├── isArray.ts │ │ │ ├── isDefined.ts │ │ │ ├── isEmpty.ts │ │ │ ├── isFunction.ts │ │ │ ├── isNil.ts │ │ │ ├── isNumber.ts │ │ │ ├── isObject.ts │ │ │ ├── isPlainObject.ts │ │ │ ├── isString.ts │ │ │ ├── isUndefined.ts │ │ │ ├── mapSkipUndefined.ts │ │ │ ├── not.ts │ │ │ ├── persistState.ts │ │ │ ├── plugins │ │ │ ├── dirtyCheck │ │ │ │ ├── dirtyCheckPlugin.ts │ │ │ │ └── entityDirtyCheckPlugin.ts │ │ │ ├── entityCollectionPlugin.ts │ │ │ ├── paginator │ │ │ │ └── paginatorPlugin.ts │ │ │ ├── persistForm │ │ │ │ └── persistNgFormPlugin.ts │ │ │ ├── plugin.ts │ │ │ └── stateHistory │ │ │ │ ├── entityStateHistoryPlugin.ts │ │ │ │ └── stateHistoryPlugin.ts │ │ │ ├── query.ts │ │ │ ├── queryConfig.ts │ │ │ ├── queryEntity.ts │ │ │ ├── removeEntities.ts │ │ │ ├── resetStores.ts │ │ │ ├── root.ts │ │ │ ├── runStoreAction.ts │ │ │ ├── selectAllOverloads.ts │ │ │ ├── setEntities.ts │ │ │ ├── setLoading.ts │ │ │ ├── setLoadingAndError.ts │ │ │ ├── setValueByString.ts │ │ │ ├── snapshotManager.ts │ │ │ ├── sort.ts │ │ │ ├── sortByOptions.ts │ │ │ ├── store.ts │ │ │ ├── storeConfig.ts │ │ │ ├── stores.ts │ │ │ ├── toBoolean.ts │ │ │ ├── toEntitiesIds.ts │ │ │ ├── toEntitiesObject.ts │ │ │ ├── trackIdChanges.ts │ │ │ ├── transaction.ts │ │ │ ├── types.ts │ │ │ └── updateEntities.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── ng-entity-service │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── ng-package.json │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── action-factory.ts │ │ │ ├── helpers.ts │ │ │ ├── ng-entity-service-notifier.ts │ │ │ ├── ng-entity-service.config.ts │ │ │ ├── ng-entity-service.loader.ts │ │ │ ├── ng-entity-service.spec.ts │ │ │ ├── ng-entity.service.ts │ │ │ ├── setup.ts │ │ │ └── types.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── ng-playground │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── auth │ │ │ │ ├── auth.guard.ts │ │ │ │ ├── auth.module.ts │ │ │ │ ├── login │ │ │ │ │ ├── login.component.html │ │ │ │ │ ├── login.component.ts │ │ │ │ │ └── login.module.ts │ │ │ │ └── state │ │ │ │ │ ├── auth.query.ts │ │ │ │ │ ├── auth.service.ts │ │ │ │ │ └── auth.store.ts │ │ │ ├── cart │ │ │ │ ├── cart.component.html │ │ │ │ ├── cart.component.ts │ │ │ │ ├── cart.module.ts │ │ │ │ └── state │ │ │ │ │ ├── cart.model.ts │ │ │ │ │ ├── cart.query.ts │ │ │ │ │ ├── cart.service.ts │ │ │ │ │ └── cart.store.ts │ │ │ ├── contacts │ │ │ │ ├── contacts-page │ │ │ │ │ ├── contacts-page.component.css │ │ │ │ │ ├── contacts-page.component.html │ │ │ │ │ └── contacts-page.component.ts │ │ │ │ ├── contacts.data.ts │ │ │ │ ├── contacts.module.ts │ │ │ │ └── state │ │ │ │ │ ├── contact.model.ts │ │ │ │ │ ├── contacts.pagination.ts │ │ │ │ │ ├── contacts.query.ts │ │ │ │ │ ├── contacts.service.ts │ │ │ │ │ └── contacts.store.ts │ │ │ ├── movies │ │ │ │ ├── actors │ │ │ │ │ └── state │ │ │ │ │ │ ├── actor.model.ts │ │ │ │ │ │ ├── actors.query.ts │ │ │ │ │ │ └── actors.store.ts │ │ │ │ ├── genres │ │ │ │ │ └── state │ │ │ │ │ │ ├── genre.model.ts │ │ │ │ │ │ ├── genres.query.ts │ │ │ │ │ │ └── genres.store.ts │ │ │ │ ├── movies.module.ts │ │ │ │ ├── movies │ │ │ │ │ ├── movies.component.css │ │ │ │ │ ├── movies.component.html │ │ │ │ │ └── movies.component.ts │ │ │ │ ├── normalized.ts │ │ │ │ └── state │ │ │ │ │ ├── movie.model.ts │ │ │ │ │ ├── movies.query.ts │ │ │ │ │ ├── movies.service.ts │ │ │ │ │ └── movies.store.ts │ │ │ ├── nav │ │ │ │ └── nav.component.ts │ │ │ ├── posts │ │ │ │ ├── posts.component.html │ │ │ │ ├── posts.component.ts │ │ │ │ ├── posts.css │ │ │ │ ├── posts.module.ts │ │ │ │ └── state │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── post.model.ts │ │ │ │ │ ├── posts.query.ts │ │ │ │ │ ├── posts.service.ts │ │ │ │ │ └── posts.store.ts │ │ │ ├── product-page │ │ │ │ └── product-page.component.ts │ │ │ ├── products │ │ │ │ ├── product │ │ │ │ │ ├── product.component.html │ │ │ │ │ └── product.component.ts │ │ │ │ ├── products.component.html │ │ │ │ ├── products.component.ts │ │ │ │ ├── products.mocks.ts │ │ │ │ ├── products.module.ts │ │ │ │ └── state │ │ │ │ │ ├── products.model.ts │ │ │ │ │ ├── products.query.ts │ │ │ │ │ ├── products.service.ts │ │ │ │ │ └── products.store.ts │ │ │ ├── stories │ │ │ │ ├── state │ │ │ │ │ ├── stories.query.ts │ │ │ │ │ ├── stories.service.ts │ │ │ │ │ ├── stories.store.ts │ │ │ │ │ └── story.model.ts │ │ │ │ ├── stories.module.ts │ │ │ │ └── stories │ │ │ │ │ ├── stories.component.css │ │ │ │ │ ├── stories.component.html │ │ │ │ │ └── stories.component.ts │ │ │ ├── todos-app │ │ │ │ ├── filter │ │ │ │ │ ├── filter.model.ts │ │ │ │ │ └── todos-filters.component.ts │ │ │ │ ├── state │ │ │ │ │ ├── todo.model.ts │ │ │ │ │ ├── todos.query.ts │ │ │ │ │ ├── todos.service.ts │ │ │ │ │ └── todos.store.ts │ │ │ │ ├── todo │ │ │ │ │ ├── todo.component.html │ │ │ │ │ └── todo.component.ts │ │ │ │ ├── todos-page │ │ │ │ │ ├── todos-page.component.css │ │ │ │ │ ├── todos-page.component.html │ │ │ │ │ └── todos-page.component.ts │ │ │ │ ├── todos.module.ts │ │ │ │ └── todos │ │ │ │ │ └── todos.component.ts │ │ │ └── widgets │ │ │ │ ├── state │ │ │ │ ├── widget.model.ts │ │ │ │ ├── widgets.query.ts │ │ │ │ ├── widgets.service.ts │ │ │ │ └── widgets.store.ts │ │ │ │ ├── widgets.component.html │ │ │ │ ├── widgets.component.ts │ │ │ │ └── widgets.module.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── akita.svg │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── ng-router-store │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── ng-package.json │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── router.module.ts │ │ ├── router.query.ts │ │ ├── router.service.ts │ │ ├── router.spec.ts │ │ └── router.store.ts │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json └── workspace.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # prettier v2 dictates LF 2 | # https://prettier.io/docs/en/options.html#end-of-line 3 | * text=auto eol=lf 4 | 5 | .vscode/*.json linguist-language=jsonc 6 | tslint.json linguist-language=jsonc 7 | tsconfig.json linguist-language=jsonc 8 | tsconfig.*.json linguist-language=jsonc 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug in the Akita Library 3 | 4 | body: 5 | - type: dropdown 6 | id: is-regression 7 | attributes: 8 | label: Is this a regression? 9 | options: 10 | - 'Yes' 11 | - 'No' 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: Description 19 | validations: 20 | required: true 21 | 22 | - type: input 23 | id: reproduction 24 | attributes: 25 | label: Please provide a link to a minimal reproduction of the bug 26 | 27 | - type: textarea 28 | id: exception-or-error 29 | attributes: 30 | label: Please provide the exception or error you saw 31 | render: true 32 | 33 | - type: textarea 34 | id: environment 35 | attributes: 36 | label: Please provide the environment you discovered this bug in 37 | render: true 38 | 39 | - type: textarea 40 | id: other 41 | attributes: 42 | label: Anything else? 43 | 44 | - type: dropdown 45 | id: contribute 46 | attributes: 47 | label: Do you want to create a pull request? 48 | options: 49 | - 'Yes' 50 | - 'No' 51 | validations: 52 | required: true 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: 'Feature Request' 2 | description: Suggest a feature for Akita Library 3 | 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | id: proposed-solution 14 | attributes: 15 | label: Proposed solution 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: alternatives-considered 21 | attributes: 22 | label: Alternatives considered 23 | validations: 24 | required: true 25 | 26 | - type: dropdown 27 | id: contribute 28 | attributes: 29 | label: Do you want to create a pull request? 30 | options: 31 | - 'Yes' 32 | - 'No' 33 | validations: 34 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-support-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Support Request' 3 | about: Questions and requests for support 4 | --- 5 | 6 | Please do not file questions or support requests on the GitHub issues tracker. 7 | 8 | You can get your questions answered using other communication channels. Please see: 9 | 10 | https://github.com/datorama/akita/discussions 11 | 12 | Thank you! 13 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our guidelines: https://github.com/datorama/akita/blob/master/CONTRIBUTING.md#commit 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | ``` 16 | [ ] Bugfix 17 | [ ] Feature 18 | [ ] Code style update (formatting, local variables) 19 | [ ] Refactoring (no functional changes, no api changes) 20 | [ ] Build related changes 21 | [ ] CI related changes 22 | [ ] Documentation content changes 23 | [ ] Other... Please describe: 24 | ``` 25 | 26 | ## What is the current behavior? 27 | 28 | 29 | 30 | Issue Number: N/A 31 | 32 | ## What is the new behavior? 33 | 34 | ## Does this PR introduce a breaking change? 35 | 36 | ``` 37 | [ ] Yes 38 | [ ] No 39 | ``` 40 | 41 | 42 | 43 | ## Other information 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: '@datorama/akita' 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: true 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Cache node modules 15 | uses: actions/cache@v2 16 | env: 17 | cache-name: cache-node-modules 18 | with: 19 | path: ~/.npm 20 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 21 | restore-keys: | 22 | ${{ runner.os }}-build-${{ env.cache-name }}- 23 | ${{ runner.os }}-build- 24 | ${{ runner.os }}- 25 | - uses: actions/setup-node@v2 26 | with: 27 | node-version: '16' 28 | cache: 'npm' 29 | 30 | - name: Install dependencies 31 | run: npm i 32 | 33 | - name: Run Build 34 | run: npm run build:all 35 | 36 | - name: Run unit tests 37 | run: npm run test:all 38 | -------------------------------------------------------------------------------- /.github/workflows/deploy-sample-app.yml: -------------------------------------------------------------------------------- 1 | name: 'deploy-sample-app' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | cache: 'npm' 20 | 21 | - run: npm i 22 | - run: npx nx build ng-playground 23 | 24 | - name: Deploy to akita.surge.sh 25 | uses: dswistowski/surge-sh-action@v1 26 | with: 27 | domain: 'akita.surge.sh' 28 | project: 'dist/packages/ng-playground' 29 | login: ${{ secrets.surge_login }} 30 | token: ${{ secrets.surge_token }} 31 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: 'docs' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'docs/**' 9 | 10 | defaults: 11 | run: 12 | working-directory: ./docs 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: webfactory/ssh-agent@v0.5.3 21 | with: 22 | ssh-private-key: ${{ secrets.GH_PAGES_DEPLOY }} 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: '16.x' 26 | - name: Get yarn cache directory path 27 | id: yarn-cache-dir-path 28 | run: echo "::set-output name=dir::$(yarn cache dir)" 29 | 30 | - uses: actions/cache@v2 31 | id: yarn-cache 32 | with: 33 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 34 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-yarn- 37 | 38 | - name: Install dependencies 39 | run: yarn 40 | 41 | - name: Identity 42 | run: | 43 | git config --global user.email "netanel7799@gmail.com" 44 | git config --global user.name "NetanelBasal" 45 | 46 | - name: Release to GitHub Pages 47 | env: 48 | USE_SSH: true 49 | GIT_USER: NetanelBasal 50 | run: yarn deploy 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | /tools/akita-cli/node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | report*.json 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | .angular -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | **/src/**/files 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 200 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": ["source.organizeImports", "source.fixAll"], 3 | "editor.formatOnSave": true, 4 | "editor.rulers": [200], // matches prettier printWidth 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "typescript.tsdk": "node_modules\\typescript\\lib" 7 | } 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CONTRIBUTING-ARCHIVED.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This project is `Archived` and is no longer actively maintained; 4 | We are not accepting contributions or Pull Requests. 5 | 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /changelog.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | disableEmoji: false, 3 | list: [ 4 | 'test', 5 | 'feat', 6 | 'fix', 7 | 'chore', 8 | 'docs', 9 | 'refactor', 10 | 'style', 11 | 'ci', 12 | 'perf', 13 | ], 14 | maxMessageLength: 64, 15 | minMessageLength: 3, 16 | questions: ['type', 'scope', 'subject', 'body', 'breaking', 'issues'], 17 | scopes: [ 18 | 'akita', 19 | 'ng-devtools', 20 | 'ng-router-store', 21 | 'ng-entity-service', 22 | 'general' 23 | ], 24 | types: { 25 | chore: { 26 | description: 'Build process or auxiliary tool changes', 27 | emoji: '🤖', 28 | value: 'chore', 29 | }, 30 | ci: { 31 | description: 'CI related changes', 32 | emoji: '😺', 33 | value: 'ci', 34 | }, 35 | docs: { 36 | description: 'Documentation only changes', 37 | emoji: '📘', 38 | value: 'docs', 39 | }, 40 | feat: { 41 | description: 'A new feature', 42 | emoji: '🔥', 43 | value: 'feat', 44 | }, 45 | fix: { 46 | description: 'A bug fix', 47 | emoji: '🐞', 48 | value: 'fix', 49 | }, 50 | perf: { 51 | description: 'A code change that improves performance', 52 | emoji: '🏎', 53 | value: 'perf', 54 | }, 55 | refactor: { 56 | description: 'A code change that neither fixes a bug or adds a feature', 57 | emoji: '💡', 58 | value: 'refactor', 59 | }, 60 | release: { 61 | description: 'Create a release commit', 62 | emoji: '🤩', 63 | value: 'release', 64 | }, 65 | style: { 66 | description: 'Markup, white-space, formatting, missing semi-colons...', 67 | emoji: '💄', 68 | value: 'style', 69 | }, 70 | test: { 71 | description: 'Adding missing tests', 72 | emoji: '🥳', 73 | value: 'test', 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /docs/docs/additional/class.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Class Support 3 | --- 4 | 5 | Akita also supports using a `class` as the underlying value instead of a plain object. In most cases, we don't recommend doing so for the following reasons: 6 | 7 | 1. We can’t store classes in the database. A typical example of this is when you need to save the store snapshot. 8 | 2. Classes are harder to use with web workers, due to serialization concerns. 9 | 3. There are various third-party tools (for example `immer`) that only work with plain objects. 10 | 11 | However, sometimes they can be useful. Here's an example that uses a class: 12 | 13 | ```ts title="user.model.ts" 14 | export class User { 15 | constructor({ firstName, lastName, token }: Partial) { 16 | this.firstName = firstName; 17 | this.lastName = lastName; 18 | this.token = token; 19 | } 20 | 21 | get name() { 22 | return `${this.firstName} ${this.lastName}`; 23 | } 24 | } 25 | ``` 26 | 27 | ```ts title="user.store.ts" 28 | export interface UserState extends EntityState {} 29 | 30 | @StoreConfig({ name: 'user' }) 31 | export class UserStore extends EntityStore { 32 | constructor() { 33 | super(); 34 | } 35 | } 36 | ``` 37 | 38 | :::warning 39 | The class constructor should accept **only** one parameter which is a plain object 40 | ::: 41 | 42 | In this case, when you call `update()`, Akita will take care to instantiate a new `User` by merging the entity's current state with the new parameters. 43 | 44 | You can use the `akitaPreAddEntity` to create a new instance of the entity: 45 | 46 | ```ts 47 | @StoreConfig({ name: 'posts' }) 48 | export class PostsStore extends EntityStore { 49 | constructor() { 50 | super(); 51 | } 52 | 53 | akitaPreAddEntity(nextState: Post): Post { 54 | return new Post(nextState); 55 | } 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/docs/additional/events.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Event-based APIs 3 | --- 4 | 5 | One of the recurring requests we got, was to simplify and improve the experience of working with event-based APIs such as web-sockets. 6 | 7 | To make it easier for you, we’ve added a new API method — `runStoreAction` and `runEntityStoreAction`: 8 | 9 | ```ts 10 | import { runStoreAction, StoreAction, runEntityStoreAction, EntityStoreAction } from '@datorama/akita'; 11 | 12 | runStoreAction(BooksStore, StoreAction.Update, update => update({ filter: 'COMPLETE' })); 13 | 14 | // Or use a string 15 | runStoreAction('books', StoreAction.Update, update => update({ filter: 'COMPLETE' })); 16 | 17 | runEntityStoreAction(BooksStore, EntityStoreAction.SetEntities, set => set([ 18 | { id: 1 }, 19 | { id: 2 } 20 | ])); 21 | 22 | runEntityStoreAction(BooksStore, EntityStoreAction.AddEntities, add => add({ id: 1 })); 23 | 24 | runEntityStoreAction(BooksStore, EntityStoreAction.UpdateEntities, update => update(2, { title: 'New title' })); 25 | 26 | runEntityStoreAction(BooksStore, EntityStoreAction.RemoveEntities, remove => remove(2)); 27 | 28 | runEntityStoreAction(BooksStore, EntityStoreAction.UpsertEntities, upsert => upsert([2, 3], 29 | { title: 'New Title' }, (id, newState) => ({ id, ...newState, price: 0 }))); 30 | 31 | runEntityStoreAction(BooksStore, EntityStoreAction.UpsertManyEntities, upsertMany => upsertMany([ 32 | { id: 2, title: 'New title', price: 0 }, 33 | { id: 4, title: 'Another title', price: 0 }, 34 | )); 35 | ``` 36 | 37 | The `runStoreAction()` and `runEntityStoreAction()` takes the store’s class or name, the store action to perform and an operation 38 | callback. The first argument of the operation callback is the store operator specified by the action. You can determine 39 | these parameters from your socket connection and update any store you want. 40 | 41 | By using the store name and not the store class as the first argument, type checking of the respective action arguments is disabled. 42 | This can be useful when validation of untyped data is unnecessary and can be passed directly to the activity. 43 | -------------------------------------------------------------------------------- /docs/docs/additional/js.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plain JS Usage 3 | --- 4 | 5 | Akita can work with any framework and can be used with plain JS. Here's an [example](https://github.com/datorama/akita/tree/master/apps/svelte-todo) that uses [Svelte](https://github.com/sveltejs/svelte) and Akita functional creation methods: 6 | 7 | ```js 8 | import { createStore, createQuery } from '@datorama/akita'; 9 | 10 | export const store = createStore({ count: 0 }, { name: 'counter' }); 11 | export const query = createQuery(store); 12 | 13 | export const selectCount = query.select('count'); 14 | ``` 15 | 16 | ```js 17 | 24 | 25 | 26 | ``` 27 | 28 | For `Entity` feature use `createEntityStore` and `createQueryEntity` functions. 29 | 30 | You can read more about it in [this](https://netbasal.com/supercharge-your-svelte-state-management-with-akita-f1f9de5ef43d) article. -------------------------------------------------------------------------------- /docs/docs/additional/operators.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Operators 3 | --- 4 | 5 | ### `filterNilValue` 6 | Filters `undefined` or `null` values: 7 | 8 | ```ts 9 | import { filterNilValue } from '@datorama/akita'; 10 | 11 | query.selectEntity(1).pipe(filterNilValue()).subscribe(); 12 | ``` 13 | 14 | ### `setLoading` 15 | set the `loading` property to `true` and change it to `false` when the request completed or there was an error: 16 | 17 | ```ts title="products.service.ts" 18 | import { setLoading } from '@datorama/akita'; 19 | 20 | export class ProductsService { 21 | constructor(private productsStore: ProductsStore) {} 22 | 23 | getProducts() { 24 | return this.http.get(url).pipe( 25 | setLoading(store), 26 | tap(response => this.productsStore.set(response)) 27 | ); 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/docs/additional/optimstic-updates.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optimistic Updates 3 | --- 4 | 5 | When performing optimistic updates, the UI adds a new entity to the store before the server request responded with the actual data. 6 | One approach to this is to create a temporary entity id on the client-side and later update it with the real id when the server request is finished. 7 | To track id changes of entities in a store Akita provides the RxJS operator `trackIdChanges(query: QueryEntity)`: 8 | 9 | ```ts 10 | import { trackIdChanges } from '@datorama/akita'; 11 | 12 | query.selectEntity(1).pipe(trackIdChanges(query)).subscribe(entity => { 13 | /* ... */ 14 | }); 15 | 16 | ``` 17 | 18 | :::info 19 | Operators preceding `trackIdChanges` in the same RxJS pipeline will only run once and are then discarded. 20 | ::: 21 | 22 | By applying the `trackIdChanges` operator on a query, the query gets rebuild each time the id changes. 23 | This also means that all successive operators in the same pipeline get re-evaluated, but preceding operators will be discarded. 24 | The project function argument of `selectEntity(id, project)` is also discarded on id changes. 25 | 26 | In the following example, the `filter()` operator, and the project function of `selectEntity()` will only run once and be discarded: 27 | ```ts 28 | 29 | query.selectEntity(1).pipe(filter(entity => entity.done), trackIdChanges(query)).subscribe(entity => { 30 | /* ... */ 31 | }); 32 | 33 | query.selectEntity(1, entity => entity.done).pipe(trackIdChanges(query)).subscribe(entity => { 34 | /* ... */ 35 | }); 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/docs/additional/reset.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reset Stores 3 | --- 4 | 5 | Akita's provides `resetStores()` method that reset all the stores back to their initial state. It can be useful when you want to clean the store's data upon user logout. 6 | 7 | In order to enable it globally, you should set the `resettable` option to `true`: 8 | 9 | ```ts 10 | import { akitaConfig } from '@datorama/akita'; 11 | 12 | akitaConfig({ resettable: true }); 13 | ``` 14 | 15 | Now you can call the global `resetStores()` method: 16 | 17 | ```ts 18 | import { resetStores } from "@datorama/akita"; 19 | 20 | class AuthService { 21 | logout() { 22 | resetStores(); 23 | 24 | // Optionally exclude stores 25 | resetStores({ exclude: ['storeName'] }) 26 | } 27 | } 28 | ``` 29 | 30 | In addition to that, you can set a specific store to be `resettable`: 31 | ```ts title="todos.store.ts" 32 | @StoreConfig({ name: 'todos', resettable: true }) 33 | export class TodosStore extends EntityStore { 34 | constructor() { 35 | super(); 36 | } 37 | } 38 | ``` 39 | 40 | Now, you can call the store's `reset()` method. -------------------------------------------------------------------------------- /docs/docs/angular/hmr.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: HMR 3 | --- 4 | 5 | When using the `hmr` feature, Angular destroys and creates the store on each change. We can work around this by using `persistStorage`: 6 | 7 | ```ts title="main.ts" 8 | if (!environment.production) { 9 | persistState(); 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/docs/config.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Store Config 3 | --- 4 | 5 | ## Production Mode 6 | In dev mode, Akita will deep freeze the store's value to avoid store mutations. Moreover, it will expose a reference to the stores through `window.$$stores` property, and to the queries through `window.$$queries`. 7 | 8 | In production, you should **disable** this feature by calling the `enableAkitaProdMode()` function in order to get the store to operate optimally: 9 | 10 | ```ts 11 | import { enableAkitaProdMode } from '@datorama/akita'; 12 | 13 | if (environment.production) { 14 | enableAkitaProdMode(); 15 | } 16 | ``` 17 | 18 | ## StoreConfig Decorator 19 | 20 | ### `name` 21 | 22 | The name of the store. The name is a required parameter and must be unique for the entire application: 23 | 24 | ```ts title="session.store.ts" 25 | @StoreConfig({ name: 'session' }) 26 | export class SessionStore extends Store { 27 | constructor() { 28 | super(initialState); 29 | } 30 | } 31 | ``` 32 | 33 | ### `resettable` 34 | Whether to allow a `reset()` functionality. This means that you'll be able to call `store.reset()` any time to go back to the store's initial state value. (`false` by default) 35 | ```ts title="session.store.ts" 36 | @StoreConfig({ name: 'session', resettable: true }) 37 | export class SessionStore extends Store { 38 | constructor() { 39 | super(initialState); 40 | } 41 | } 42 | ``` 43 | 44 | 45 | ### `deepFreezeFn` 46 | function customDeepFreeze(state) { 47 | return freezedState; 48 | } 49 | 50 | ```ts title="session.store.ts" 51 | @StoreConfig({ name: 'session', deepFreezeFn: customDeepFreeze }) 52 | export class SessionStore extends Store { 53 | constructor() { 54 | super(initialState); 55 | } 56 | } 57 | ``` 58 | 59 | ### `cache` 60 | See [caching support](additional/cache.mdx) section. 61 | 62 | ### `idKey` 63 | A custom `idKey` for `EntityStore` - see [EntityId](entities/entity-store.mdx#entity-id) section. 64 | 65 | :::info 66 | You can also provide the `options` in the constructor: 67 | 68 | ```ts 69 | new Store(initialState, options) 70 | ``` 71 | ::: 72 | -------------------------------------------------------------------------------- /docs/docs/enhancers/cli.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Akita CLI 3 | --- 4 | 5 | Akita offers a CLI tool, enabling you to generate stores based on the specifications quickly. 6 | 7 | Install it via npm: 8 | 9 | ```bash 10 | npm install @datorama/akita-cli -g 11 | ``` 12 | 13 | Now you can run the `akita` command. 14 | 15 | ## Options 16 | 17 | ## `basePath` 18 | 19 | By default, the prompt is set to the current directory. To change it, set the `basePath`: 20 | 21 | ```json title="package.json" 22 | "akitaCli": { 23 | "basePath": "./playground/src/app/" 24 | } 25 | ``` 26 | The path should be relative to the `package.json`. 27 | 28 | ## `template` 29 | The default template is for plain JS applications. To change it set the `template` property: 30 | ```json title="package.json" 31 | "akitaCli": { 32 | "template": "js|angular|ts" 33 | } 34 | ``` 35 | 36 | ## `idKey` 37 | The idKey for `EntityStore`: 38 | 39 | ```json title="package.json" 40 | "akitaCli": { 41 | "idKey": "_id" 42 | } 43 | ``` 44 | 45 | ## `customFolderName` 46 | Whether to provide custom folder name. Default is `state`: 47 | ```json title="package.json" 48 | "akitaCli": { 49 | "customFolderName": "true" 50 | } 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/docs/enhancers/snapshot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Snapshot Manager 3 | --- 4 | 5 | There are times when saving the local state in the server becomes useful. For example, you may want to give the user a PDF representing their local state. 6 | For such cases, Akita provides the `snapshotManager` API. 7 | 8 | To get a snapshot of the whole application state, you can call the `getStoresSnapshot()` method: 9 | 10 | ```ts title="todos.service.ts" 11 | import { snapshotManager } from '@datorama/akita'; 12 | 13 | export class TodosService { 14 | saveState() { 15 | this.http.post('/url', snapshotManager.getStoresSnapshot()); 16 | } 17 | } 18 | ``` 19 | 20 | The `getStoresSnapshot()` returns an object containing the whole application state. For example: 21 | 22 | ```json 23 | { 24 | "todos": { 25 | "entities": { ... } 26 | }, 27 | "cart": { 28 | "entities": { ... } 29 | }, 30 | "session": { 31 | "firstName": "", 32 | ... 33 | } 34 | } 35 | ``` 36 | 37 | If you don't need the whole application state, you can pass the specific stores that you need: 38 | ```ts title="todos.service.ts" 39 | import { snapshotManager } from '@datorama/akita'; 40 | 41 | export class TodosService { 42 | saveState() { 43 | const stores = ['todos', 'widgets']; 44 | this.http.post('/url', snapshotManager.getStoresSnapshot(stores)); 45 | } 46 | } 47 | ``` 48 | 49 | It also works the other way around, when you get the snapshot from the server you can save it by calling the `setStoresSnapshot() `method and passing the snapshot: 50 | ```ts title="todos.service.ts" 51 | import { snapshotManager } from '@datorama/akita'; 52 | 53 | export class TodosService { 54 | setSnapshotFromServer(snapshotFromServer) { 55 | snapshotManager.setStoresSnapshot(snapshotFromServer); 56 | 57 | // Support lazy stores 58 | snapshotManager.setStoresSnapshot(snapshotFromServer, { lazy: true } ); 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/docs/entities/sorting.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sorting 3 | --- 4 | 5 | By default, the store returns entities in the order in which they arrived from the server. The entities you add are pushed to the end of the collection. 6 | You may prefer getting the entities from the store in some other order. You can provide a `sortBy` option which could be based on an entity `key` or a `comparer` function. 7 | 8 | Akita will keep the collection in the order prescribed by your `key` or `comparer`. You can set it once for the entire Query: 9 | 10 | ```ts title="products.query.ts" 11 | import { QueryConfig, Order } from '@datorama/akita'; 12 | 13 | @QueryConfig({ 14 | sortBy: 'price', 15 | sortByOrder: Order.ASC 16 | }) 17 | export class ProductsQuery extends QueryEntity { 18 | constructor(protected store: ProductsStore) { 19 | super(store); 20 | } 21 | } 22 | ``` 23 | 24 | Or you can set it per `selectAll()`: 25 | 26 | ```ts 27 | const products$ = sortControl.valueChanges.pipe( 28 | startWith('title'), 29 | switchMap((sortBy) => productsQuery.selectAll({ 30 | sortBy 31 | })) 32 | ); 33 | ``` 34 | 35 | The query `sortBy` function also passes the whole `state` as the third argument, give you the ability to return a different `sortBy` function based on your current `state`. For example: 36 | 37 | ```ts 38 | const sortBy = (a, b, state) => ( 39 | state.sortyByPrice ? 40 | sortByPrice(a, b) : 41 | sortById(a, b) 42 | ); 43 | 44 | // With QueryConfig 45 | @QueryConfig({ sortBy }) 46 | 47 | // With selectAll 48 | queryTodos.selectAll({ sortBy }); 49 | ``` -------------------------------------------------------------------------------- /docs/docs/immer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Immer 3 | --- 4 | 5 | As you know, when working with immutable objects you often get to what’s called a “spread hell” situations. If you prefer working with immutable objects in an mutable fashion, you can use [immer](https://github.com/immerjs/immer) with Akita. 6 | 7 | The only thing you need to do is pass the `produce` function from `immer` to your store: 8 | 9 | ```ts title="todos.store.ts" 10 | import { produce } from 'immer'; 11 | 12 | export interface TodosState extends EntityState { 13 | ui: { 14 | filter: VISIBILITY_FILTER 15 | }; 16 | } 17 | 18 | const initialState = { 19 | ui: { filter: VISIBILITY_FILTER.SHOW_ALL } 20 | }; 21 | 22 | @StoreConfig({ name: 'todos', producerFn: produce }) 23 | export class TodosStore extends EntityStore { 24 | constructor() { 25 | super(initialState); 26 | } 27 | } 28 | ``` 29 | 30 | Now when you use the store's `update` function, you'll get the `draft` version, which you can mutate: 31 | 32 | ```ts title="todos.service.ts" 33 | export class TodosService { 34 | 35 | constructor(private todosStore: TodosStore) { } 36 | 37 | updateFilter(filter: VISIBILITY_FILTER) { 38 | this.todosStore.update(state => { 39 | state.ui.filter = filter; 40 | }) 41 | } 42 | 43 | complete({ id, completed }: Todo) { 44 | this.todosStore.update(id, entity => { 45 | entity.completed = completed; 46 | }); 47 | } 48 | } 49 | ``` 50 | 51 | :::warning 52 | When you choose to work with `immer`, you can't return a new value from the callback function: 53 | ::: 54 | 55 | ```ts 56 | todosStore.update(id, entity => entity.completed = completed); 57 | ``` 58 | 59 | This will cause `immer` to throw. Here's a [live](https://stackblitz.com/edit/akita-todos-immer) example you can play with. 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | Install from the NPM repository using yarn or npm: 6 | 7 | ```bash 8 | yarn add @datorama/akita 9 | ``` 10 | 11 | ```bash 12 | npm install @datorama/akita 13 | ``` 14 | 15 | Angular Applications: 16 | ```bash 17 | ng add @datorama/akita 18 | ``` -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akita-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "docusaurus start", 7 | "build": "docusaurus build", 8 | "swizzle": "docusaurus swizzle", 9 | "deploy": "GIT_USER=NetanelBasal USE_SSH=true docusaurus deploy", 10 | "serve": "docusaurus serve", 11 | "clear": "docusaurus clear" 12 | }, 13 | "dependencies": { 14 | "@docusaurus/core": "2.3.0", 15 | "@docusaurus/preset-classic": "2.3.0", 16 | "classnames": "^2.3.1", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-helmet": "^6.1.0" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/src/components/highlight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Highlight = ({ children, bgColor, color }) => ( 4 | 12 | {' '} 13 | {children}{' '} 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /docs/src/pages/sample-app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import Layout from "@theme/Layout"; 4 | import Link from "@docusaurus/Link"; 5 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 6 | import useBaseUrl from "@docusaurus/useBaseUrl"; 7 | import styles from "./styles.module.css"; 8 | 9 | function SampleApp() { 10 | const context = useDocusaurusContext(); 11 | const { siteConfig = {} } = context; 12 | const iframeStyle = { 13 | width: "100%", 14 | height: "100vh", 15 | marginLeft: 0, 16 | }; 17 | return ( 18 | 22 |
23 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default SampleApp; 35 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * CSS files with the .module.css suffix will be treated as CSS modules 4 | * and scoped locally. 5 | */ 6 | 7 | .heroBanner { 8 | padding: 4rem 0; 9 | text-align: center; 10 | position: relative; 11 | overflow: hidden; 12 | } 13 | 14 | @media screen and (max-width: 966px) { 15 | .heroBanner { 16 | padding: 2rem; 17 | } 18 | } 19 | 20 | .buttons { 21 | margin-top: 10px; 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .features { 27 | display: flex; 28 | align-items: center; 29 | padding: 2rem 0; 30 | width: 100%; 31 | } 32 | 33 | .featureImage { 34 | height: 200px; 35 | width: 200px; 36 | } 37 | 38 | .getStarted { 39 | background-color: rgba(211, 175, 211, 0.8); 40 | border-color: #8c7db6; 41 | color: white !important; 42 | } 43 | 44 | .getStarted:hover { 45 | background-color: #d3afd3; 46 | } 47 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/akita-arc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/docs/static/img/akita-arc.jpg -------------------------------------------------------------------------------- /docs/static/img/akita.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/docs/static/img/akita.png -------------------------------------------------------------------------------- /docs/static/img/devtools.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/docs/static/img/devtools.gif -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "tasksRunnerOptions": { 4 | "default": { 5 | "runner": "nx/tasks-runners/default", 6 | "options": { 7 | "cacheableOperations": ["build", "lint", "test", "e2e"] 8 | } 9 | } 10 | }, 11 | "npmScope": "@datorama", 12 | "affected": { 13 | "defaultBase": "master" 14 | }, 15 | "cli": { 16 | "defaultCollection": "@nrwl/angular" 17 | }, 18 | "generators": { 19 | "@nrwl/angular:application": { 20 | "style": "scss", 21 | "linter": "eslint", 22 | "unitTestRunner": "jest", 23 | "e2eTestRunner": "none", 24 | "strict": false 25 | }, 26 | "@nrwl/angular:library": { 27 | "style": "scss", 28 | "linter": "eslint", 29 | "unitTestRunner": "jest" 30 | }, 31 | "@nrwl/angular:component": { 32 | "style": "scss" 33 | } 34 | }, 35 | "targetDefaults": { 36 | "build": { 37 | "dependsOn": ["^build"] 38 | }, 39 | "test": { 40 | "inputs": ["default", "^default", "{workspaceRoot}/jest.preset.js"] 41 | }, 42 | "lint": { 43 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/.gitkeep -------------------------------------------------------------------------------- /packages/akita/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /packages/akita/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/akita/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [8.0.1](https://github-personal/salesforce/akita/compare/akita-8.0.0...akita-8.0.1) (2023-02-13) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **akita:** support TS strict mode ([#1050](https://github-personal/salesforce/akita/issues/1050)) ([1d9eaac](https://github-personal/salesforce/akita/commit/1d9eaacc19d43882e5e926cabe8b823047968695)), closes [salesforce/akita#870](https://github-personal/salesforce/akita/issues/870) 11 | 12 | 13 | 14 | # [8.0.0](https://github.com/salesforce/akita/compare/akita-7.1.1...akita-8.0.0) (2023-01-09) 15 | 16 | 17 | ### Features 18 | 19 | * upgrade to Angular v15 ([c9af3ea](https://github.com/salesforce/akita/commit/c9af3eae3a1cec9fba48760736124d26fc14486b)) 20 | 21 | 22 | ### BREAKING CHANGES 23 | 24 | * Upgrade to Angular v15 25 | 26 | - Remove the schematics package 27 | - Remove the devtools package and provide a new simpler Angular alternative in the docs 28 | 29 | 30 | 31 | # [7.1.0](https://github.com/datorama/akita/compare/akita-7.0.1...akita-7.1.0) (2022-01-06) 32 | 33 | 34 | ### Features 35 | 36 | * **akita:** 🔥 add native middleware hooks binding support ([#783](https://github.com/datorama/akita/issues/783)) ([7698049](https://github.com/datorama/akita/commit/76980498e1d285df8a05826be8f6bc0da0e82dba)) 37 | 38 | 39 | 40 | ## [7.0.1](https://github.com/datorama/akita/compare/akita-7.0.0...akita-7.0.1) (2021-11-29) 41 | -------------------------------------------------------------------------------- /packages/akita/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'akita', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | isolatedModules: true, 9 | }, 10 | }, 11 | testEnvironment: 'jsdom', 12 | transform: { 13 | '^.+\\.[tj]s?$': 'ts-jest', 14 | }, 15 | 16 | moduleFileExtensions: ['ts', 'js'], 17 | coverageDirectory: '../../coverage/packages/akita', 18 | }; 19 | -------------------------------------------------------------------------------- /packages/akita/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datorama/akita", 3 | "version": "8.0.1", 4 | "description": "A Reactive State Management Tailored-Made for JS Applications", 5 | "keywords": [ 6 | "angular", 7 | "state management", 8 | "react", 9 | "vue", 10 | "typescript", 11 | "javascript", 12 | "rxjs", 13 | "angular store", 14 | "store", 15 | "observable data stores", 16 | "redux", 17 | "reactive" 18 | ], 19 | "homepage": "https://github.com/datorama/akita/tree/master/libs/akita#readme", 20 | "bugs": { 21 | "url": "https://github.com/datorama/akita/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/datorama/akita", 26 | "directory": "libs/akita" 27 | }, 28 | "license": "Apache-2.0", 29 | "author": "Netanel Basal", 30 | "peerDependencies": { 31 | "rxjs": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/akita/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akita", 3 | "sourceRoot": "packages/akita/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/packages/akita", 11 | "main": "packages/akita/src/index.ts", 12 | "tsConfig": "packages/akita/tsconfig.lib.json", 13 | "assets": ["packages/akita/*.md"] 14 | } 15 | }, 16 | "lint": { 17 | "executor": "@nrwl/linter:eslint", 18 | "outputs": ["{options.outputFile}"], 19 | "options": { 20 | "lintFilePatterns": ["packages/akita/**/*.ts"] 21 | } 22 | }, 23 | "test": { 24 | "executor": "@nrwl/jest:jest", 25 | "outputs": ["{workspaceRoot}/coverage/packages/akita"], 26 | "options": { 27 | "jestConfig": "packages/akita/jest.config.ts", 28 | "passWithNoTests": true 29 | } 30 | }, 31 | "version": { 32 | "executor": "@jscutlery/semver:version" 33 | } 34 | }, 35 | "tags": [] 36 | } 37 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/akitaConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import { akitaConfig, EntityStore, QueryEntity } from '..'; 2 | import { initialState, Todo, TodosStore } from './setup'; 3 | 4 | akitaConfig({ 5 | resettable: true, 6 | ttl: 300 7 | }); 8 | 9 | class TodosQuery extends QueryEntity { 10 | constructor(store: EntityStore) { 11 | super(store); 12 | } 13 | } 14 | 15 | describe('Akita global config', () => { 16 | let todosStore: TodosStore; 17 | let todosQuery: TodosQuery; 18 | 19 | beforeEach(() => { 20 | todosStore = new TodosStore({}); 21 | todosQuery = new TodosQuery(todosStore); 22 | }); 23 | 24 | it('should set cache timeout with 300ms', () => { 25 | jest.useFakeTimers({ legacyFakeTimers: true }); 26 | todosStore.setHasCache(true, { restartTTL: true }); 27 | expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 300); 28 | }); 29 | 30 | it('should not have cache after 300ms', () => { 31 | jest.useFakeTimers({ legacyFakeTimers: true }); 32 | todosStore.add({ id: 1 }); 33 | todosStore.setHasCache(true, { restartTTL: true }); 34 | jest.runAllTimers(); 35 | expect(todosQuery.getHasCache()).toBe(false); 36 | }); 37 | 38 | it('should reset the store', () => { 39 | const expected = { 40 | entities: {}, 41 | ids: [], 42 | loading: true, 43 | error: null, 44 | ...initialState 45 | }; 46 | todosStore.add({ id: 1 }); 47 | jest.spyOn(todosStore, 'setHasCache'); 48 | todosStore.reset(); 49 | expect(todosStore._value()).toEqual(expected); 50 | expect(todosStore.setHasCache).toHaveBeenCalledWith(false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/arrayToggle.spec.ts: -------------------------------------------------------------------------------- 1 | import { arrayToggle, byId, byKey } from '../lib/arrayToggle'; 2 | import { EntityStore } from '../lib/entityStore'; 3 | import { StoreConfig } from '../lib/storeConfig'; 4 | import { EntityState, ID } from '../lib/types'; 5 | 6 | interface Comment { 7 | id: ID; 8 | text: string; 9 | } 10 | 11 | interface Article { 12 | id: ID; 13 | comments: Comment[]; 14 | title: string; 15 | } 16 | 17 | interface ArticlesState extends EntityState
{ 18 | names: string[]; 19 | } 20 | 21 | @StoreConfig({ name: 'articles' }) 22 | class ArticlesStore extends EntityStore { 23 | constructor() { 24 | super({ names: [] }); 25 | } 26 | } 27 | 28 | describe('arrayToggle', () => { 29 | let store: ArticlesStore; 30 | 31 | beforeEach(() => { 32 | store = new ArticlesStore(); 33 | }); 34 | 35 | afterEach(() => { 36 | store.destroy(); 37 | }); 38 | 39 | it('should add new item if none exist', () => { 40 | store.add({ 41 | id: 1, 42 | title: '', 43 | comments: [], 44 | }); 45 | 46 | store.update(1, (state) => { 47 | return { 48 | comments: arrayToggle(state.comments, { id: 1, text: 'comment' }, byId()), 49 | }; 50 | }); 51 | 52 | expect(store._value().entities[1].comments.length).toBe(1); 53 | expect(store._value().entities[1].comments[0].id).toBe(1); 54 | expect(store._value().entities[1].comments[0].text).toBe('comment'); 55 | }); 56 | 57 | it('should remove item if one exist', () => { 58 | store.add({ 59 | id: 1, 60 | title: '', 61 | comments: [{ id: 1, text: 'comment' }], 62 | }); 63 | 64 | store.update(1, (state) => { 65 | return { 66 | comments: arrayToggle(state.comments, { id: 1, text: 'comment' }, byKey('id')), 67 | }; 68 | }); 69 | 70 | expect(store._value().entities[1].comments.length).toBe(0); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/booksStore.ts: -------------------------------------------------------------------------------- 1 | import { ActiveState, EntityState, EntityStore, ID, StoreConfig } from '..'; 2 | 3 | export type TestBook = { 4 | id: ID; 5 | title: string; 6 | price: number; 7 | }; 8 | 9 | export interface TestBooksState extends EntityState, ActiveState { 10 | filter: string; 11 | } 12 | 13 | @StoreConfig({ name: 'books' }) 14 | export class BooksStore extends EntityStore { 15 | constructor() { 16 | super({ filter: 'ALL', active: null }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/classBased.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityStore, ID, StoreConfig } from '..'; 2 | 3 | class Todo { 4 | id; 5 | title; 6 | completed; 7 | 8 | constructor(params: { id: ID; title: string; completed: boolean }) { 9 | this.completed = params.completed || false; 10 | this.title = params.title; 11 | this.id = params.id; 12 | } 13 | } 14 | 15 | @StoreConfig({ name: 'todos' }) 16 | class TodosStore extends EntityStore {} 17 | 18 | const store = new TodosStore(); 19 | 20 | describe('Class Based', () => { 21 | it('should instantiate new Todo if not exists', function () { 22 | store.upsert(1, { title: 'new title' }, (id, newState) => ({ id, ...newState, completed: false }), { baseClass: Todo }); 23 | expect(store._value().entities[1]).toBeInstanceOf(Todo); 24 | expect(store._value().entities[1].title).toBe('new title'); 25 | expect(store._value().entities[1].completed).toBe(false); 26 | expect(store._value().entities[1].id).toBe(1); 27 | store.upsert(1, { title: 'new title2' }, (id, newState) => ({ id, ...newState, completed: false }), { baseClass: Todo }); 28 | expect(store._value().entities[1]).toBeInstanceOf(Todo); 29 | expect(store._value().entities[1].title).toBe('new title2'); 30 | expect(store._value().entities[1].completed).toBe(false); 31 | expect(store._value().entities[1].id).toBe(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/customId.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityStore, StoreConfig } from '..'; 2 | 3 | interface Todo { 4 | id?: string; 5 | title: string; 6 | group: string; 7 | completed: boolean; 8 | } 9 | 10 | @StoreConfig({ 11 | name: 'todos' 12 | }) 13 | class TodosStore extends EntityStore { 14 | akitaPreAddEntity(todo: Readonly): Todo { 15 | return { 16 | id: `${todo.group}${todo.title}`, 17 | ...todo 18 | }; 19 | } 20 | } 21 | 22 | const store = new TodosStore(); 23 | 24 | describe('Custom id', () => { 25 | it('should set the custom id', () => { 26 | store.set([{ title: 'titleOne', completed: false, group: 'GroupA' }]); 27 | expect(store._value().entities[`GroupAtitleOne`].title).toBe('titleOne'); 28 | expect(store._value().ids).toEqual([`GroupAtitleOne`]); 29 | }); 30 | 31 | it('should add the custom id', () => { 32 | store.add([{ title: 'titleTwo', completed: false, group: 'GroupA' }]); 33 | expect(store._value().entities[`GroupAtitleTwo`].title).toBe('titleTwo'); 34 | expect(store._value().ids).toEqual([`GroupAtitleOne`, `GroupAtitleTwo`]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/env.spec.ts: -------------------------------------------------------------------------------- 1 | import { enableAkitaProdMode } from '../lib/env'; 2 | 3 | // isBrowser expression has to be mocked because 4 | // in context of enableAkitaProdMode func it evaluates 5 | // to true and that defeats the purpose of this test 6 | jest.mock('../lib/root', () => ({ 7 | get isBrowser() { 8 | return false; // set some default value 9 | }, 10 | })); 11 | 12 | describe('env', () => { 13 | let windowSpy; 14 | 15 | beforeEach(() => { 16 | // @ts-ignore 17 | windowSpy = jest.spyOn(global, 'window', 'get'); 18 | }); 19 | 20 | afterEach(() => { 21 | windowSpy.mockRestore(); 22 | }); 23 | 24 | it('should not throw an error despite window being undefined.', () => { 25 | windowSpy.mockImplementation(() => undefined); 26 | expect(window).toBeUndefined(); 27 | expect(() => enableAkitaProdMode()).not.toThrow(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/mocks.ts: -------------------------------------------------------------------------------- 1 | import { TestBook } from './booksStore'; 2 | 3 | export const entitiesMapMock = { 4 | 1: { 5 | id: 1, 6 | title: `Item 1`, 7 | price: 1 8 | }, 9 | 2: { 10 | id: 2, 11 | title: `Item 2`, 12 | price: 2 13 | } 14 | }; 15 | 16 | export function createMockEntities(start = 0, end = 2): TestBook[] { 17 | return Array.from({ length: end - start }, (v, k) => { 18 | const i = k + start; 19 | return { 20 | id: i + 1, 21 | title: `Item ${i + 1}`, 22 | price: i + 1 23 | }; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/moveEntity.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from '../lib/types'; 2 | import { StoreConfig, EntityStore, QueryEntity } from '..'; 3 | 4 | interface Article { 5 | id: number; 6 | title: string; 7 | } 8 | 9 | interface ArticlesState extends EntityState
{} 10 | 11 | @StoreConfig({ name: 'articles' }) 12 | class ArticlesStore extends EntityStore {} 13 | 14 | class ArticlesQuery extends QueryEntity {} 15 | 16 | const store = new ArticlesStore(); 17 | const query = new ArticlesQuery(store); 18 | 19 | describe('Move', () => { 20 | it('should move entity in the collection', () => { 21 | const data = Array.from({ length: 5 }, (_, i) => ({ 22 | id: i + 1, 23 | title: i.toString() 24 | })); 25 | 26 | store.set(data); 27 | const spy = jest.fn(); 28 | query.selectAll().subscribe(spy); 29 | expect(spy).toHaveBeenCalledTimes(1); 30 | store.move(3, 2); 31 | expect(spy).toHaveBeenCalledTimes(2); 32 | 33 | expect(query.getValue().ids).toEqual([1, 2, 4, 3, 5]); 34 | // now it's [1, 2, 4, 3, 5] 35 | store.move(4, 3); 36 | expect(spy).toHaveBeenCalledTimes(3); 37 | expect(query.getValue().ids).toEqual([1, 2, 4, 5, 3]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/persistStateAsync.spec.ts: -------------------------------------------------------------------------------- 1 | import { mapTo, tap, timer } from 'rxjs'; 2 | import { Store, StoreConfig } from '..'; 3 | import { persistState, PersistStateStorage } from '../lib/persistState'; 4 | import { tick } from './setup'; 5 | 6 | function random(min: number, max: number) { 7 | return Math.floor(Math.random() * (max - min + 1) + min); 8 | } 9 | 10 | jest.useFakeTimers({ legacyFakeTimers: true }); 11 | 12 | let cache = {}; 13 | 14 | const asyncStorage: PersistStateStorage = { 15 | getItem(key) { 16 | return timer(random(1000, 3000)).pipe(mapTo(cache[key])); 17 | }, 18 | setItem(key, value) { 19 | return timer(random(1000, 10000)).pipe(tap(() => (cache[key] = value))); 20 | }, 21 | clear() { 22 | cache = {}; 23 | }, 24 | }; 25 | 26 | persistState({ storage: asyncStorage }); 27 | 28 | @StoreConfig({ 29 | name: 'auth', 30 | }) 31 | class AuthStore extends Store { 32 | constructor() { 33 | super({}); 34 | } 35 | } 36 | 37 | const store = new AuthStore(); 38 | 39 | describe('Persist state async', () => { 40 | it('should work with async', async () => { 41 | await tick(); 42 | 43 | store.update({ async: true }); 44 | await tick(); 45 | jest.runAllTimers(); 46 | 47 | store.update({ async: false }); 48 | await tick(); 49 | jest.runAllTimers(); 50 | 51 | store.update({ async: true }); 52 | await tick(); 53 | jest.runAllTimers(); 54 | 55 | store.update({ async: false }); 56 | await tick(); 57 | jest.runAllTimers(); 58 | 59 | store.update({ async: true }); 60 | await tick(); 61 | jest.runAllTimers(); 62 | 63 | store.update({ async: false }); 64 | await tick(); 65 | jest.runAllTimers(); 66 | expect(cache['AkitaStores'].auth.async).toBeFalsy(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/persistStateStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreConfig } from '../lib/storeConfig'; 2 | import { persistState } from '../lib/persistState'; 3 | import { Store } from '../lib/store'; 4 | import { tick } from './setup'; 5 | 6 | @StoreConfig({ 7 | name: 'todos' 8 | }) 9 | class TodosStore extends Store { 10 | constructor() { 11 | super({ todos: [] }); 12 | } 13 | } 14 | 15 | @StoreConfig({ 16 | name: 'todosUi' 17 | }) 18 | class TodosUiStore extends Store { 19 | constructor() { 20 | super({ filter: 'SHOW_ALL' }); 21 | } 22 | } 23 | 24 | describe('Persist state - similar store names', () => { 25 | localStorage.clear(); 26 | const storageState = { 27 | todos: { todos: ['Akita'] }, 28 | todosUi: { filter: 'SHOW_COMPLETED' } 29 | }; 30 | localStorage.setItem('AkitaStores', JSON.stringify(storageState)); 31 | 32 | persistState({ include: ['todosUi', 'todos'] }); 33 | 34 | const todosStore = new TodosStore(); 35 | const todosUiStore = new TodosUiStore(); 36 | 37 | it('should persist only the exact name', async () => { 38 | await tick(); 39 | expect(todosStore._value().todos).toEqual(['Akita']); 40 | expect(todosUiStore._value().filter).toEqual('SHOW_COMPLETED'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/query.spec.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '../lib/store'; 2 | import { Query } from '../lib/query'; 3 | import { StoreConfig } from '../lib/storeConfig'; 4 | 5 | class User { 6 | firstName: string = ''; 7 | lastName: string = ''; 8 | 9 | constructor(params: Partial) { 10 | Object.assign(this, params); 11 | } 12 | 13 | get name() { 14 | return `${this.firstName} ${this.lastName}`; 15 | } 16 | } 17 | 18 | @StoreConfig({ 19 | name: 'user' 20 | }) 21 | class UserStore extends Store { 22 | constructor() { 23 | super(new User({ firstName: 'Netanel', lastName: 'Basal' })); 24 | } 25 | } 26 | 27 | const userStore = new UserStore(); 28 | 29 | class UserQuery extends Query { 30 | constructor() { 31 | super(userStore); 32 | } 33 | } 34 | 35 | const query = new UserQuery(); 36 | 37 | describe('With Class', () => { 38 | it('should select a slice from the state', () => { 39 | const spy = jest.fn(); 40 | query.select(state => state.firstName).subscribe(spy); 41 | expect(spy).toHaveBeenCalledWith('Netanel'); 42 | }); 43 | 44 | it('should select all', () => { 45 | const spy = jest.fn(); 46 | query.select().subscribe(spy); 47 | expect(spy).toHaveBeenCalledWith({ firstName: 'Netanel', lastName: 'Basal' }); 48 | }); 49 | 50 | it('should get the value', () => { 51 | expect(query.getValue()).toEqual(userStore._value()); 52 | }); 53 | 54 | it('should work with string', () => { 55 | let result; 56 | query.select('firstName').subscribe(name => { 57 | result = name; 58 | }); 59 | expect(result).toBe('Netanel'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/resetStore.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreConfig } from '../lib/storeConfig'; 2 | import { EntityStore } from '../lib/entityStore'; 3 | import { Store } from '../lib/store'; 4 | 5 | @StoreConfig({ 6 | name: 'todos', 7 | resettable: true 8 | }) 9 | class TodosStore extends EntityStore { 10 | constructor() { 11 | super(); 12 | } 13 | } 14 | 15 | const todosstore = new TodosStore(); 16 | 17 | @StoreConfig({ 18 | name: 'auth', 19 | resettable: true 20 | }) 21 | class AuthStore extends Store { 22 | constructor() { 23 | super({ 24 | id: null, 25 | firstName: '', 26 | lastName: '', 27 | token: '' 28 | }); 29 | } 30 | } 31 | 32 | const authStore = new AuthStore(); 33 | 34 | describe('Reset store', () => { 35 | it('should reset store state to its initial state - Store', () => { 36 | authStore._setState(() => { 37 | return { 38 | id: 1, 39 | firstName: 'Netanel', 40 | lastName: 'Basal', 41 | token: 'token' 42 | }; 43 | }); 44 | jest.spyOn(authStore, 'setHasCache'); 45 | authStore.reset(); 46 | expect(authStore._value()).toEqual({ id: null, firstName: '', lastName: '', token: '' }); 47 | expect(authStore.setHasCache).toHaveBeenCalledWith(false); 48 | }); 49 | 50 | it('should reset store state to its initial state - EntityStore', () => { 51 | todosstore.add({ id: 1 }); 52 | const expected = { 53 | entities: {}, 54 | ids: [], 55 | loading: true, 56 | error: null 57 | }; 58 | jest.spyOn(todosstore, 'setHasCache'); 59 | todosstore.reset(); 60 | expect(todosstore._value()).toEqual(expected); 61 | expect(todosstore.setHasCache).toHaveBeenCalledWith(false); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/selectMany.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, StoreConfig, EntityStore, QueryEntity, ID } from '..'; 2 | 3 | interface Article { 4 | id: ID; 5 | } 6 | 7 | interface ArticlesState extends EntityState
{ } 8 | 9 | @StoreConfig({ name: 'articles' }) 10 | class ArticlesStore extends EntityStore { } 11 | 12 | class ArticlesQuery extends QueryEntity { } 13 | 14 | const store = new ArticlesStore(); 15 | const query = new ArticlesQuery(store); 16 | jest.useFakeTimers({ legacyFakeTimers: true }); 17 | 18 | describe('selectMany', () => { 19 | it('should filter nil', () => { 20 | const spy = jest.fn(); 21 | store.set([]); 22 | query.selectMany([1, 2]).subscribe(spy); 23 | jest.runAllTimers(); 24 | expect(spy).toHaveBeenCalledTimes(1); 25 | expect(spy).toHaveBeenCalledWith([]); 26 | store.add({ id: 1 }); 27 | jest.runAllTimers(); 28 | expect(spy).toHaveBeenCalledTimes(2); 29 | expect(spy).toHaveBeenCalledWith([{ id: 1 }]); 30 | expect(true).toBeTruthy(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/setLoading.spec.ts: -------------------------------------------------------------------------------- 1 | import { tap, timer } from 'rxjs'; 2 | import { EntityStore, QueryEntity } from '..'; 3 | import { cacheable } from '../lib/index'; 4 | import { setLoading } from '../lib/setLoading'; 5 | 6 | const store = new EntityStore({}, { name: 'test' }); 7 | const query = new QueryEntity(store); 8 | 9 | jest.useFakeTimers({ legacyFakeTimers: true }); 10 | 11 | describe('setLoading', () => { 12 | it('should work', () => { 13 | const log = []; 14 | const request = timer(1000).pipe( 15 | setLoading(store), 16 | tap(() => log.push(1)), 17 | tap(() => store.set([{ id: 1 }])) 18 | ); 19 | 20 | jest.spyOn(store, 'setLoading'); 21 | 22 | const spy = jest.fn(); 23 | cacheable(store, request).subscribe(spy); 24 | jest.runAllTimers(); 25 | expect(spy).toHaveBeenCalledTimes(1); 26 | expect(log.length).toEqual(1); 27 | expect(query.getValue().loading).toBe(false); 28 | cacheable(store, request).subscribe(spy); 29 | // from inside withLoading 30 | expect(store.setLoading).toBeCalledTimes(2); 31 | expect(spy).toHaveBeenCalledTimes(1); 32 | expect(log.length).toEqual(1); 33 | 34 | cacheable(store, request).subscribe(spy); 35 | // from inside withLoading 36 | expect(store.setLoading).toBeCalledTimes(2); 37 | expect(spy).toHaveBeenCalledTimes(1); 38 | expect(log.length).toEqual(1); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/akita/src/__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from '../lib/isPlainObject'; 2 | 3 | describe('Utils', () => { 4 | describe('isPlainObject', () => { 5 | it('should return true', () => { 6 | expect(isPlainObject({})).toBeTruthy(); 7 | }); 8 | 9 | it('should return false', () => { 10 | expect(isPlainObject(class User {})).toBeFalsy(); 11 | }); 12 | 13 | it('should return false when passing function', () => { 14 | expect(isPlainObject(function() {})).toBeFalsy(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/akita/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/index'; 2 | -------------------------------------------------------------------------------- /packages/akita/src/lib/actions.ts: -------------------------------------------------------------------------------- 1 | import { IDS } from './types'; 2 | 3 | export interface StoreSnapshotAction { 4 | type: string | null; 5 | entityIds: IDS[] | null; 6 | skip: boolean; 7 | payload: any 8 | } 9 | 10 | export const currentAction: StoreSnapshotAction = { 11 | type: null, 12 | entityIds: null, 13 | skip: false, 14 | payload: null 15 | }; 16 | 17 | let customActionActive = false; 18 | 19 | export function resetCustomAction() { 20 | customActionActive = false; 21 | } 22 | 23 | // public API for custom actions. Custom action always wins 24 | export function logAction(type: string, entityIds?, payload?: any) { 25 | setAction(type, entityIds, payload); 26 | customActionActive = true; 27 | } 28 | 29 | export function setAction(type: string, entityIds?, payload?: any) { 30 | if (customActionActive === false) { 31 | currentAction.type = type; 32 | currentAction.entityIds = entityIds; 33 | currentAction.payload = payload 34 | } 35 | } 36 | 37 | export function setSkipAction(skip = true) { 38 | currentAction.skip = skip; 39 | } 40 | 41 | export function action(action: string, entityIds?) { 42 | return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { 43 | const originalMethod = descriptor.value; 44 | descriptor.value = function (...args) { 45 | logAction(action, entityIds); 46 | return originalMethod.apply(this, args); 47 | }; 48 | 49 | return descriptor; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /packages/akita/src/lib/activeState.ts: -------------------------------------------------------------------------------- 1 | import { ActiveState, EntityState, ID, IDS, MultiActiveState } from './types'; 2 | import { hasEntity } from './hasEntity'; 3 | import { isArray } from './isArray'; 4 | 5 | // @internal 6 | export function hasActiveState(state: EntityState): state is EntityState & (ActiveState | MultiActiveState) { 7 | return state.hasOwnProperty('active'); 8 | } 9 | 10 | // @internal 11 | export function isMultiActiveState(active: IDS): active is ID[] { 12 | return isArray(active); 13 | } 14 | 15 | // @internal 16 | export function resolveActiveEntity({ active, ids, entities }: EntityState & (ActiveState | MultiActiveState)) { 17 | if (isMultiActiveState(active)) { 18 | return getExitingActives(active, ids); 19 | } 20 | 21 | if (hasEntity(entities, active) === false) { 22 | return null; 23 | } 24 | 25 | return active; 26 | } 27 | 28 | // @internal 29 | export function getExitingActives(currentActivesIds: ID[], newIds: ID[]) { 30 | const filtered = currentActivesIds.filter(id => newIds.indexOf(id) > -1); 31 | /** Return the same reference if nothing has changed */ 32 | if (filtered.length === currentActivesIds.length) { 33 | return currentActivesIds; 34 | } 35 | 36 | return filtered; 37 | } 38 | -------------------------------------------------------------------------------- /packages/akita/src/lib/addEntities.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, PreAddEntity } from './types'; 2 | import { hasEntity } from './hasEntity'; 3 | 4 | export type AddEntitiesParams = { 5 | state: State; 6 | entities: Entity[]; 7 | idKey: string; 8 | options: AddEntitiesOptions; 9 | preAddEntity: PreAddEntity; 10 | }; 11 | 12 | export type AddEntitiesOptions = { prepend?: boolean; loading?: boolean }; 13 | 14 | // @internal 15 | export function addEntities, E>({ state, entities, idKey, options = {}, preAddEntity }: AddEntitiesParams) { 16 | let newEntities = {}; 17 | let newIds = []; 18 | let hasNewEntities = false; 19 | 20 | for (const entity of entities) { 21 | if (hasEntity(state.entities, entity[idKey]) === false) { 22 | // evaluate the middleware first to support dynamic ids 23 | const current = preAddEntity(entity); 24 | const entityId = current[idKey]; 25 | newEntities[entityId] = current; 26 | if (options.prepend) newIds.unshift(entityId); 27 | else newIds.push(entityId); 28 | 29 | hasNewEntities = true; 30 | } 31 | } 32 | 33 | return hasNewEntities 34 | ? { 35 | newState: { 36 | ...state, 37 | entities: { 38 | ...state.entities, 39 | ...newEntities 40 | }, 41 | ids: options.prepend ? [...newIds, ...state.ids] : [...state.ids, ...newIds] 42 | }, 43 | newIds 44 | } 45 | : null; 46 | } 47 | -------------------------------------------------------------------------------- /packages/akita/src/lib/arrayAdd.ts: -------------------------------------------------------------------------------- 1 | import { OrArray } from './types'; 2 | import { coerceArray } from './coerceArray'; 3 | import { AddEntitiesOptions } from './addEntities'; 4 | 5 | /** 6 | * Add item to a collection 7 | * 8 | * @example 9 | * 10 | * 11 | * store.update(state => ({ 12 | * comments: arrayAdd(state.comments, { id: 2 }) 13 | * })) 14 | * 15 | */ 16 | 17 | export function arrayAdd(arr: T, newEntity: OrArray, options: AddEntitiesOptions = {}): T { 18 | const newEntities = coerceArray(newEntity); 19 | const toArr = arr || []; 20 | 21 | return options.prepend ? [...newEntities, ...toArr] : ([...toArr, ...newEntities] as any); 22 | } 23 | -------------------------------------------------------------------------------- /packages/akita/src/lib/arrayRemove.ts: -------------------------------------------------------------------------------- 1 | import { IDS, ItemPredicate } from './types'; 2 | import { DEFAULT_ID_KEY } from './defaultIDKey'; 3 | import { coerceArray } from './coerceArray'; 4 | import { isObject } from './isObject'; 5 | import { isFunction } from './isFunction'; 6 | import { not } from './not'; 7 | 8 | /** 9 | * Remove item from collection 10 | * 11 | * @example 12 | * 13 | * 14 | * store.update(state => ({ 15 | * names: arrayRemove(state.names, ['one', 'second']) 16 | * })) 17 | */ 18 | export function arrayRemove(arr: T, identifier: IDS | ItemPredicate, idKey = DEFAULT_ID_KEY): T { 19 | let identifiers; 20 | let filterFn; 21 | 22 | if (isFunction(identifier)) { 23 | filterFn = not(identifier); 24 | } else { 25 | identifiers = coerceArray(identifier as IDS); 26 | filterFn = (current) => { 27 | return identifiers.includes(isObject(current) ? current[idKey] : current) === false; 28 | }; 29 | } 30 | 31 | if (Array.isArray(arr)) { 32 | return arr.filter(filterFn) as any; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/akita/src/lib/arrayToggle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create an array value comparator for a specific key of the value. 3 | * @param prop The property of the value to be compared. 4 | */ 5 | export function byKey(prop: keyof T) { 6 | return (a: T, b: T) => a[prop] === b[prop]; 7 | } 8 | 9 | /** 10 | * Create an array value comparator for the id field of an array value. 11 | */ 12 | export function byId>() { 13 | return byKey('id'); 14 | } 15 | 16 | /** 17 | * Adds or removes a value from an array by comparing its values. If a matching value exists it is removed, otherwise 18 | * it is added to the array. 19 | * 20 | * @param array The array to modify. 21 | * @param newValue The new value to toggle. 22 | * @param compare A compare function to determine equality of array values. Default is an equality test by object identity. 23 | */ 24 | export function arrayToggle(array: T[], newValue: T, compare: (a: T, b: T) => boolean = (a, b) => a === b) { 25 | const index = array.findIndex((oldValue) => compare(newValue, oldValue)); 26 | return !!~index ? [...array.slice(0, index), ...array.slice(index + 1)] : [...array, newValue]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/akita/src/lib/arrayUpdate.ts: -------------------------------------------------------------------------------- 1 | import { coerceArray } from './coerceArray'; 2 | import { DEFAULT_ID_KEY } from './defaultIDKey'; 3 | import { isFunction } from './isFunction'; 4 | import { isObject } from './isObject'; 5 | import { IDS, ItemPredicate } from './types'; 6 | 7 | /** 8 | * Update item in a collection 9 | * 10 | * @example 11 | * 12 | * 13 | * store.update(1, entity => ({ 14 | * comments: arrayUpdate(entity.comments, 1, { name: 'newComment' }) 15 | * })) 16 | */ 17 | export function arrayUpdate(arr: T, predicateOrIds: IDS | ItemPredicate, obj: Partial, idKey = DEFAULT_ID_KEY): T { 18 | let condition: ItemPredicate; 19 | 20 | if (isFunction(predicateOrIds)) { 21 | condition = predicateOrIds; 22 | } else { 23 | const ids = coerceArray(predicateOrIds); 24 | condition = (item) => ids.includes(isObject(item) ? item[idKey] : item) === true; 25 | } 26 | 27 | const updateFn = (state) => 28 | state.map((entity, index) => { 29 | if (condition(entity, index) === true) { 30 | return isObject(entity) 31 | ? { 32 | ...entity, 33 | ...obj, 34 | } 35 | : obj; 36 | } 37 | 38 | return entity; 39 | }); 40 | 41 | return updateFn(arr); 42 | } 43 | -------------------------------------------------------------------------------- /packages/akita/src/lib/arrayUpsert.ts: -------------------------------------------------------------------------------- 1 | import { ID } from './types'; 2 | import { DEFAULT_ID_KEY } from './defaultIDKey'; 3 | import { arrayAdd } from './arrayAdd'; 4 | import { arrayUpdate } from './arrayUpdate'; 5 | import { isObject } from './isObject'; 6 | 7 | /** 8 | * Upsert item in a collection 9 | * 10 | * @example 11 | * 12 | * 13 | * store.update(1, entity => ({ 14 | * comments: arrayUpsert(entity.comments, 1, { name: 'newComment' }) 15 | * })) 16 | */ 17 | export function arrayUpsert(arr: Root, id: ID, obj: Partial, idKey = DEFAULT_ID_KEY): Root[0][] { 18 | const entityIsObject = isObject(obj); 19 | const entityExists = arr.some(entity => (entityIsObject ? entity[idKey] === id : entity === id)); 20 | if (entityExists) { 21 | return arrayUpdate(arr, id, obj, idKey); 22 | } else { 23 | return arrayAdd(arr, entityIsObject ? { ...obj, [idKey]: id } : obj); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/akita/src/lib/cacheable.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY, Observable, of } from 'rxjs'; 2 | import { Store } from './store'; 3 | 4 | /** 5 | * 6 | * Helper function for checking if we have data in cache 7 | * 8 | * export class ProductsService { 9 | * constructor(private productsStore: ProductsStore) {} 10 | 11 | * get(): Observable { 12 | * const request = this.http.get().pipe( 13 | * tap(this.productsStore.set(response)) 14 | * ); 15 | * 16 | * return cacheable(this.productsStore, request); 17 | * } 18 | * } 19 | */ 20 | export function cacheable(store: Store, request$: Observable, options: { emitNext: boolean } = { emitNext: false }): Observable { 21 | if (store._cache().value) { 22 | return options.emitNext ? of(undefined) : EMPTY; 23 | } 24 | return request$; 25 | } 26 | -------------------------------------------------------------------------------- /packages/akita/src/lib/capitalize.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function capitalize(value: string) { 3 | return value && value.charAt(0).toUpperCase() + value.slice(1); 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/coerceArray.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from './isNil'; 2 | 3 | // @internal 4 | export function coerceArray(value: T | T[]): T[] { 5 | if (isNil(value)) { 6 | return []; 7 | } 8 | return Array.isArray(value) ? value : [value]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/akita/src/lib/combineQueries.ts: -------------------------------------------------------------------------------- 1 | import { auditTime, combineLatest, Observable } from 'rxjs'; 2 | 3 | type ReturnTypes[]> = { [P in keyof T]: T[P] extends Observable ? R : never }; 4 | type Observables = [Observable] | Observable[]; 5 | 6 | export function combineQueries(observables: R): Observable> { 7 | return combineLatest(observables).pipe(auditTime(0)) as any; 8 | } 9 | -------------------------------------------------------------------------------- /packages/akita/src/lib/compareKeys.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from './isFunction'; 2 | 3 | export function compareKeys(keysOrFuncs: any[]) { 4 | return function (prevState, currState) { 5 | const isFns = isFunction(keysOrFuncs[0]); 6 | // Return when they are NOT changed 7 | return keysOrFuncs.some(keyOrFunc => { 8 | if(isFns) { 9 | return keyOrFunc(prevState) !== keyOrFunc(currState); 10 | } 11 | return prevState[keyOrFunc] !== currState[keyOrFunc]; 12 | }) === false; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/akita/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export interface AkitaConfig { 2 | /** 3 | * Whether to allowed the reset() stores functionality 4 | */ 5 | resettable?: boolean; 6 | ttl?: number; 7 | producerFn?: (state: any, fn: any) => any; 8 | } 9 | 10 | let CONFIG: AkitaConfig = { 11 | resettable: false, 12 | ttl: null, 13 | producerFn: undefined 14 | }; 15 | 16 | export function akitaConfig(config: AkitaConfig) { 17 | CONFIG = { ...CONFIG, ...config }; 18 | } 19 | 20 | // @internal 21 | export function getAkitaConfig() { 22 | return CONFIG; 23 | } 24 | 25 | export function getGlobalProducerFn() { 26 | return CONFIG.producerFn; 27 | } 28 | -------------------------------------------------------------------------------- /packages/akita/src/lib/deepFreeze.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function deepFreeze(o) { 3 | Object.freeze(o); 4 | 5 | const oIsFunction = typeof o === 'function'; 6 | const hasOwnProp = Object.prototype.hasOwnProperty; 7 | 8 | Object.getOwnPropertyNames(o).forEach(function(prop) { 9 | if ( 10 | hasOwnProp.call(o, prop) && 11 | (oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) && 12 | o[prop] !== null && 13 | (typeof o[prop] === 'object' || typeof o[prop] === 'function') && 14 | !Object.isFrozen(o[prop]) 15 | ) { 16 | deepFreeze(o[prop]); 17 | } 18 | }); 19 | 20 | return o; 21 | } 22 | -------------------------------------------------------------------------------- /packages/akita/src/lib/defaultIDKey.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ID_KEY = 'id'; 2 | -------------------------------------------------------------------------------- /packages/akita/src/lib/dispatchers.ts: -------------------------------------------------------------------------------- 1 | import { ReplaySubject, Subject } from 'rxjs'; 2 | import { StoreSnapshotAction } from './actions'; 3 | 4 | // @internal 5 | export const $$deleteStore = new Subject(); 6 | // @internal 7 | export const $$addStore = new ReplaySubject(50, 5000); 8 | // @internal 9 | export const $$updateStore = new Subject<{ storeName: string; action: StoreSnapshotAction }>(); 10 | 11 | // @internal 12 | export function dispatchDeleted(storeName: string) { 13 | $$deleteStore.next(storeName); 14 | } 15 | 16 | // @internal 17 | export function dispatchAdded(storeName: string) { 18 | $$addStore.next(storeName); 19 | } 20 | 21 | // @internal 22 | export function dispatchUpdate(storeName: string, action: StoreSnapshotAction) { 23 | $$updateStore.next({ storeName, action }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/akita/src/lib/entitiesToArray.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, SelectOptions } from './types'; 2 | import { isFunction } from './isFunction'; 3 | import { compareValues } from './sort'; 4 | import { coerceArray } from './coerceArray'; 5 | 6 | // @internal 7 | export function entitiesToArray(state: S, options: SelectOptions): E[] { 8 | let arr = []; 9 | const { ids, entities } = state; 10 | const { filterBy, limitTo, sortBy, sortByOrder } = options; 11 | 12 | for (let i = 0; i < ids.length; i++) { 13 | const entity = entities[ids[i]]; 14 | if (!filterBy) { 15 | arr.push(entity); 16 | continue; 17 | } 18 | 19 | const toArray = coerceArray(filterBy); 20 | const allPass = toArray.every(fn => fn(entity, i)); 21 | if (allPass) { 22 | arr.push(entity); 23 | } 24 | } 25 | 26 | if (sortBy) { 27 | let _sortBy: any = isFunction(sortBy) ? sortBy : compareValues(sortBy, sortByOrder); 28 | arr = arr.sort((a, b) => _sortBy(a, b, state)); 29 | } 30 | 31 | const length = Math.min(limitTo || arr.length, arr.length); 32 | 33 | return length === arr.length ? arr : arr.slice(0, length); 34 | } 35 | -------------------------------------------------------------------------------- /packages/akita/src/lib/entitiesToMap.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from './types'; 2 | import { isNil } from './isNil'; 3 | import { coerceArray } from './coerceArray'; 4 | 5 | // @internal 6 | export function entitiesToMap, E>(state: S, options) { 7 | const map = {}; 8 | const { filterBy, limitTo } = options; 9 | const { ids, entities } = state; 10 | 11 | if (!filterBy && !limitTo) { 12 | return entities; 13 | } 14 | const hasLimit = isNil(limitTo) === false; 15 | 16 | if (filterBy && hasLimit) { 17 | let count = 0; 18 | for (let i = 0, length = ids.length; i < length; i++) { 19 | if (count === limitTo) break; 20 | const id = ids[i]; 21 | const entity = entities[id]; 22 | const allPass = coerceArray(filterBy).every(fn => fn(entity, i)); 23 | if (allPass) { 24 | map[id] = entity; 25 | count++; 26 | } 27 | } 28 | } else { 29 | const finalLength = Math.min(limitTo || ids.length, ids.length); 30 | 31 | for (let i = 0; i < finalLength; i++) { 32 | const id = ids[i]; 33 | const entity = entities[id]; 34 | 35 | if (!filterBy) { 36 | map[id] = entity; 37 | continue; 38 | } 39 | 40 | const allPass = coerceArray(filterBy).every(fn => fn(entity, i)); 41 | if (allPass) { 42 | map[id] = entity; 43 | } 44 | } 45 | } 46 | 47 | return map; 48 | } 49 | -------------------------------------------------------------------------------- /packages/akita/src/lib/entityActions.ts: -------------------------------------------------------------------------------- 1 | export enum EntityActions { 2 | Set = 'Set', 3 | Add = 'Add', 4 | Update = 'Update', 5 | Remove = 'Remove', 6 | } 7 | 8 | export interface EntityAction { 9 | type: EntityActions; 10 | ids: IDType[]; 11 | } 12 | -------------------------------------------------------------------------------- /packages/akita/src/lib/entityService.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, getEntityType, getIDType } from './types'; 2 | import { Observable } from 'rxjs'; 3 | 4 | export abstract class EntityService { 5 | abstract get(id?: getIDType, config?: any): Observable; 6 | abstract add(entity: getEntityType, config?: any): Observable; 7 | abstract update(id: getIDType, entity: Partial>, config: any): Observable; 8 | abstract delete(id: getIDType, config: any): Observable; 9 | } 10 | -------------------------------------------------------------------------------- /packages/akita/src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from './root'; 2 | 3 | export let __DEV__ = true; 4 | 5 | export function enableAkitaProdMode() { 6 | __DEV__ = false; 7 | if (isBrowser) { 8 | delete (window as any).$$stores; 9 | delete (window as any).$$queries; 10 | } 11 | } 12 | 13 | // @internal 14 | export function isDev() { 15 | return __DEV__; 16 | } 17 | -------------------------------------------------------------------------------- /packages/akita/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export class AkitaError extends Error { 3 | constructor(message: string) { 4 | super(message); 5 | } 6 | } 7 | 8 | // @internal 9 | export function assertStoreHasName(name: string, className: string) { 10 | if (!name) { 11 | console.error(`@StoreConfig({ name }) is missing in ${className}`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/akita/src/lib/filterNil.ts: -------------------------------------------------------------------------------- 1 | import { filter, Observable, OperatorFunction } from 'rxjs'; 2 | 3 | /** 4 | * @example 5 | * 6 | * query.selectEntity(2).pipe(filterNil) 7 | * @deprecated Use the operator function filterNilValue() 8 | */ 9 | export const filterNil = (source: Observable): Observable> => 10 | source.pipe(filter((value): value is NonNullable => value !== null && typeof value !== 'undefined')); 11 | 12 | /** 13 | * @example 14 | * 15 | * query.selectEntity(2).pipe(filterNilValue()) 16 | */ 17 | export function filterNilValue(): OperatorFunction> { 18 | return filter((value: T): value is NonNullable => value !== null && value !== undefined); 19 | } 20 | -------------------------------------------------------------------------------- /packages/akita/src/lib/fp.ts: -------------------------------------------------------------------------------- 1 | import { Store } from './store'; 2 | import { Query } from './query'; 3 | import { StoreConfigOptions } from './storeConfig'; 4 | import { EntityStore } from './entityStore'; 5 | import { QueryEntity } from './queryEntity'; 6 | import { QueryConfigOptions } from './queryConfig'; 7 | import { EntityState } from './types'; 8 | 9 | export function createStore(initialState: Partial, options: Partial) { 10 | return new Store(initialState, options); 11 | } 12 | 13 | export function createQuery(store: Store) { 14 | return new Query(store); 15 | } 16 | 17 | export function createEntityStore(initialState: Partial, options: Partial) { 18 | return new EntityStore(initialState, options); 19 | } 20 | 21 | export function createEntityQuery(store: EntityStore, options: QueryConfigOptions = {}) { 22 | return new QueryEntity(store, options); 23 | } 24 | -------------------------------------------------------------------------------- /packages/akita/src/lib/getActiveEntities.ts: -------------------------------------------------------------------------------- 1 | import { ID, IDS } from './types'; 2 | import { isNil } from './isNil'; 3 | import { isObject } from './isObject'; 4 | import { isArray } from './isArray'; 5 | 6 | export type SetActiveOptions = { prev?: boolean; next?: boolean; wrap?: boolean }; 7 | 8 | // @internal 9 | export function getActiveEntities(idOrOptions: IDS | SetActiveOptions | null, ids: ID[], currentActive: IDS | null) { 10 | let result; 11 | 12 | if (isArray(idOrOptions)) { 13 | result = idOrOptions; 14 | } else { 15 | if (isObject(idOrOptions)) { 16 | if (isNil(currentActive)) return; 17 | (idOrOptions as SetActiveOptions) = Object.assign({ wrap: true }, idOrOptions); 18 | const currentIdIndex = ids.indexOf(currentActive as ID); 19 | if ((idOrOptions as SetActiveOptions).prev) { 20 | const isFirst = currentIdIndex === 0; 21 | if (isFirst && !(idOrOptions as SetActiveOptions).wrap) return; 22 | result = isFirst ? ids[ids.length - 1] : (ids[currentIdIndex - 1] as any); 23 | } else if ((idOrOptions as SetActiveOptions).next) { 24 | const isLast = ids.length === currentIdIndex + 1; 25 | if (isLast && !(idOrOptions as SetActiveOptions).wrap) return; 26 | result = isLast ? ids[0] : (ids[currentIdIndex + 1] as any); 27 | } 28 | } else { 29 | if (idOrOptions === currentActive) return; 30 | result = idOrOptions as ID; 31 | } 32 | } 33 | 34 | return result; 35 | } 36 | -------------------------------------------------------------------------------- /packages/akita/src/lib/getEntity.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from './isUndefined'; 2 | import { isString } from './isString'; 3 | import { ItemPredicate } from './types'; 4 | 5 | // @internal 6 | export function findEntityByPredicate(predicate: ItemPredicate, entities) { 7 | for(const entityId of Object.keys(entities)) { 8 | if(predicate(entities[entityId]) === true) { 9 | return entityId; 10 | } 11 | } 12 | 13 | return undefined; 14 | } 15 | 16 | // @internal 17 | export function getEntity( id, project ) { 18 | return function(entities) { 19 | const entity = entities[id]; 20 | 21 | if(isUndefined(entity)) { 22 | return undefined; 23 | } 24 | 25 | if(!project) { 26 | return entity; 27 | } 28 | 29 | if(isString(project)) { 30 | return entity[project]; 31 | } 32 | 33 | return (project as Function)(entity); 34 | }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /packages/akita/src/lib/getInitialEntitiesState.ts: -------------------------------------------------------------------------------- 1 | import { EntityState } from './index'; 2 | 3 | // @internal 4 | export const getInitialEntitiesState = () => 5 | ({ 6 | entities: {}, 7 | ids: [], 8 | loading: true, 9 | error: null 10 | } as EntityState); 11 | -------------------------------------------------------------------------------- /packages/akita/src/lib/getValueByString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | * 4 | * @example 5 | * 6 | * getValue(state, 'todos.ui') 7 | * 8 | */ 9 | export function getValue( obj: any, prop: string ) { 10 | /** return the whole state */ 11 | if( prop.split('.').length === 1 ) { 12 | return obj; 13 | } 14 | const removeStoreName = prop 15 | .split('.') 16 | .slice(1) 17 | .join('.'); 18 | return removeStoreName.split('.').reduce(( acc: any, part: string ) => acc && acc[part], obj); 19 | } 20 | -------------------------------------------------------------------------------- /packages/akita/src/lib/guid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate random guid 3 | * 4 | * @example 5 | * 6 | * { 7 | * id: guid() 8 | * } 9 | * 10 | * @remarks this isn't a GUID, but a 10 char random alpha-num 11 | */ 12 | export function guid() { 13 | return Math.random() 14 | .toString(36) 15 | .slice(2); 16 | } 17 | -------------------------------------------------------------------------------- /packages/akita/src/lib/hasEntity.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, ID } from './index'; 2 | 3 | // @internal 4 | export function hasEntity(entities: EntityState, id: ID) { 5 | return entities.hasOwnProperty(id); 6 | } 7 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isArray.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isArray(value: any): value is T[] { 3 | return Array.isArray(value); 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isDefined.ts: -------------------------------------------------------------------------------- 1 | import { isNil } from './isNil'; 2 | 3 | // @internal 4 | export function isDefined(val: any) { 5 | return isNil(val) === false; 6 | } 7 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isEmpty.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from './isArray'; 2 | 3 | // @internal 4 | export function isEmpty(arr: T) { 5 | if (isArray(arr)) { 6 | return arr.length === 0; 7 | } 8 | return false; 9 | } 10 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isFunction.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isFunction(value: any): value is Function { 3 | return typeof value === 'function'; 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isNil.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isNil(v) { 3 | return v === null || v === undefined; 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isNumber.ts: -------------------------------------------------------------------------------- 1 | import { isArray } from './isArray'; 2 | 3 | // @internal 4 | export function isNumber(value: any): value is number { 5 | return !isArray(value) && value - parseFloat(value) + 1 >= 0; 6 | } 7 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isObject.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isObject(value: any) { 3 | const type = typeof value; 4 | return value != null && (type == 'object' || type == 'function'); 5 | } 6 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | import { toBoolean } from './toBoolean'; 2 | 3 | // @internal 4 | export function isPlainObject(value) { 5 | return toBoolean(value) && value.constructor.name === 'Object'; 6 | } 7 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isString.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isString(value: any): value is string { 3 | return typeof value === 'string'; 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/isUndefined.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function isUndefined(value: any): value is undefined { 3 | return value === undefined; 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/mapSkipUndefined.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function mapSkipUndefined(arr: T[], callbackFn: (value: T, index: number, array: T[]) => V) { 3 | return arr.reduce((result, value, index, array) => { 4 | const val = callbackFn(value, index, array); 5 | if (val !== undefined) { 6 | result.push(val); 7 | } 8 | return result; 9 | }, []); 10 | } 11 | -------------------------------------------------------------------------------- /packages/akita/src/lib/not.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function not(pred: Function): Function { 3 | return function(...args) { 4 | return !pred(...args); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /packages/akita/src/lib/queryConfig.ts: -------------------------------------------------------------------------------- 1 | import { Order } from './sort'; 2 | 3 | export type SortBy = ((a: E, b: E, state?: S) => number) | keyof E; 4 | 5 | export interface SortByOptions { 6 | sortBy?: SortBy; 7 | sortByOrder?: Order; 8 | } 9 | 10 | export interface QueryConfigOptions extends SortByOptions {} 11 | 12 | export const queryConfigKey = 'akitaQueryConfig'; 13 | 14 | export function QueryConfig(metadata: QueryConfigOptions) { 15 | return function(constructor: Function) { 16 | constructor[queryConfigKey] = {}; 17 | for (let i = 0, keys = Object.keys(metadata); i < keys.length; i++) { 18 | const key = keys[i]; 19 | constructor[queryConfigKey][key] = metadata[key]; 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/akita/src/lib/removeEntities.ts: -------------------------------------------------------------------------------- 1 | import { EntityState, ID, StateWithActive } from './types'; 2 | import { isNil } from './isNil'; 3 | import { hasActiveState, isMultiActiveState, resolveActiveEntity } from './activeState'; 4 | 5 | export type RemoveEntitiesParams = { 6 | state: StateWithActive; 7 | ids: any[]; 8 | }; 9 | 10 | // @internal 11 | export function removeEntities, E>({ state, ids }: RemoveEntitiesParams): S { 12 | if (isNil(ids)) return removeAllEntities(state); 13 | const entities = state.entities; 14 | let newEntities = {}; 15 | 16 | for (const id of state.ids) { 17 | if (ids.includes(id) === false) { 18 | newEntities[id] = entities[id]; 19 | } 20 | } 21 | 22 | const newState = { 23 | ...state, 24 | entities: newEntities, 25 | ids: state.ids.filter(current => ids.includes(current) === false) 26 | }; 27 | 28 | if (hasActiveState(state)) { 29 | newState.active = resolveActiveEntity(newState); 30 | } 31 | 32 | return newState; 33 | } 34 | 35 | // @internal 36 | export function removeAllEntities(state: StateWithActive): S { 37 | return { 38 | ...state, 39 | entities: {}, 40 | ids: [], 41 | active: isMultiActiveState(state.active) ? [] : null 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/akita/src/lib/resetStores.ts: -------------------------------------------------------------------------------- 1 | import { __stores__ } from './stores'; 2 | import { applyTransaction } from './transaction'; 3 | 4 | export interface ResetStoresParams { 5 | exclude: string[]; 6 | } 7 | 8 | /** 9 | * Reset stores back to their initial state 10 | * 11 | * @example 12 | * 13 | * resetStores() 14 | * resetStores({ 15 | * exclude: ['auth'] 16 | * }) 17 | */ 18 | export function resetStores(options?: Partial) { 19 | const defaults: ResetStoresParams = { 20 | exclude: [] 21 | }; 22 | 23 | options = Object.assign({}, defaults, options); 24 | const stores = Object.keys(__stores__); 25 | 26 | applyTransaction(() => { 27 | for (const store of stores) { 28 | const s = __stores__[store]; 29 | if (!options.exclude) { 30 | s.reset(); 31 | } else { 32 | if (options.exclude.indexOf(s.storeName) === -1) { 33 | s.reset(); 34 | } 35 | } 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/akita/src/lib/root.ts: -------------------------------------------------------------------------------- 1 | export const isBrowser = typeof window !== 'undefined'; 2 | export const isNotBrowser = !isBrowser; 3 | // export const isNativeScript = typeof global !== 'undefined' && (global).__runtimeVersion !== 'undefined'; TODO is this used? 4 | export const hasLocalStorage = () => { 5 | try { 6 | return typeof localStorage !== 'undefined'; 7 | } catch { 8 | return false; 9 | } 10 | }; 11 | export const hasSessionStorage = () => { 12 | try { 13 | return typeof sessionStorage !== 'undefined'; 14 | } catch { 15 | return false; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /packages/akita/src/lib/selectAllOverloads.ts: -------------------------------------------------------------------------------- 1 | import { SelectOptions } from './types'; 2 | import { SortBy } from './queryConfig'; 3 | import { Order } from './sort'; 4 | 5 | export type SelectAllOptionsA = { asObject: true; filterBy?: SelectOptions['filterBy']; limitTo?: number; sortBy?: undefined; sortByOrder?: undefined }; 6 | export type SelectAllOptionsB = { filterBy: SelectOptions['filterBy']; limitTo?: number; sortBy?: SortBy; sortByOrder?: Order }; 7 | export type SelectAllOptionsC = { asObject: true; limitTo?: number; sortBy?: undefined; sortByOrder?: undefined }; 8 | export type SelectAllOptionsD = { limitTo?: number; sortBy?: SortBy; sortByOrder?: Order }; 9 | export type SelectAllOptionsE = { asObject: false; filterBy?: SelectOptions['filterBy']; limitTo?: number; sortBy?: SortBy; sortByOrder?: Order }; 10 | -------------------------------------------------------------------------------- /packages/akita/src/lib/setLoading.ts: -------------------------------------------------------------------------------- 1 | import { defer, finalize, MonoTypeOperatorFunction, Observable } from 'rxjs'; 2 | import { Store } from './store'; 3 | 4 | export function setLoading(store: Store): MonoTypeOperatorFunction { 5 | return function (source: Observable) { 6 | return defer(() => { 7 | store.setLoading(true); 8 | return source.pipe(finalize(() => store.setLoading(false))); 9 | }); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/akita/src/lib/setLoadingAndError.ts: -------------------------------------------------------------------------------- 1 | import { defer, MonoTypeOperatorFunction, Observable, tap } from 'rxjs'; 2 | import { Store } from './store'; 3 | 4 | export function setLoadingAndError(store: Store): MonoTypeOperatorFunction { 5 | return function (source: Observable) { 6 | return defer(() => { 7 | store.setLoading(true); 8 | store.setError(null); 9 | 10 | return source.pipe( 11 | tap({ 12 | error(err) { 13 | store.setLoading(false); 14 | store.setError(err); 15 | }, 16 | complete() { 17 | store.setLoading(false); 18 | }, 19 | }) 20 | ); 21 | }); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/akita/src/lib/setValueByString.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from './isObject'; 2 | 3 | /** 4 | * @internal 5 | * 6 | * @example 7 | * setValue(state, 'todos.ui', { filter: {} }) 8 | */ 9 | export function setValue(obj: any, prop: string, val: any, replace = false) { 10 | const split = prop.split('.'); 11 | 12 | if (split.length === 1) { 13 | return { ...obj, ...val }; 14 | } 15 | 16 | obj = { ...obj }; 17 | 18 | const lastIndex = split.length - 2; 19 | const removeStoreName = prop.split('.').slice(1); 20 | 21 | removeStoreName.reduce((acc, part, index) => { 22 | if (index !== lastIndex) { 23 | acc[part] = { ...acc[part] }; 24 | return acc && acc[part]; 25 | } 26 | 27 | acc[part] = replace || Array.isArray(acc[part]) || !isObject(acc[part]) ? val : { ...acc[part], ...val }; 28 | 29 | return acc && acc[part]; 30 | }, obj); 31 | 32 | return obj; 33 | } 34 | -------------------------------------------------------------------------------- /packages/akita/src/lib/sort.ts: -------------------------------------------------------------------------------- 1 | export enum Order { 2 | ASC = 'asc', 3 | DESC = 'desc' 4 | } 5 | 6 | // @internal 7 | export function compareValues(key, order: Order = Order.ASC) { 8 | return function(a, b) { 9 | if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) { 10 | return 0; 11 | } 12 | 13 | const varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key]; 14 | const varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key]; 15 | 16 | let comparison = 0; 17 | if (varA > varB) { 18 | comparison = 1; 19 | } else if (varA < varB) { 20 | comparison = -1; 21 | } 22 | return order == Order.DESC ? comparison * -1 : comparison; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/akita/src/lib/sortByOptions.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function sortByOptions(options, config) { 3 | options.sortBy = options.sortBy || (config && config.sortBy); 4 | options.sortByOrder = options.sortByOrder || (config && config.sortByOrder); 5 | } 6 | -------------------------------------------------------------------------------- /packages/akita/src/lib/storeConfig.ts: -------------------------------------------------------------------------------- 1 | import { AkitaConfig } from './config'; 2 | 3 | export type StoreConfigOptions = { 4 | name: string; 5 | resettable?: AkitaConfig['resettable']; 6 | cache?: { ttl: number }; 7 | deepFreezeFn?: (o: any) => any; 8 | idKey?: string; 9 | producerFn?: AkitaConfig['producerFn']; 10 | }; 11 | 12 | export type UpdatableStoreConfigOptions = { 13 | cache?: { ttl: number }; 14 | }; 15 | 16 | export const configKey = 'akitaConfig'; 17 | 18 | export function StoreConfig(metadata: StoreConfigOptions) { 19 | return function(constructor: Function) { 20 | constructor[configKey] = { idKey: 'id' }; 21 | 22 | for (let i = 0, keys = Object.keys(metadata); i < keys.length; i++) { 23 | const key = keys[i]; 24 | /* name is preserved read only key */ 25 | if (key === 'name') { 26 | constructor[configKey]['storeName'] = metadata[key]; 27 | } else { 28 | constructor[configKey][key] = metadata[key]; 29 | } 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/akita/src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { Query } from './query'; 2 | import { isBrowser } from './root'; 3 | import { Store } from './store'; 4 | 5 | // @internal 6 | export const __stores__: { [storeName: string]: Store } = {}; 7 | 8 | // @internal 9 | export const __queries__: { [storeName: string]: Query } = {}; 10 | 11 | if (isBrowser) { 12 | (window as any).$$stores = __stores__; 13 | (window as any).$$queries = __queries__; 14 | } 15 | -------------------------------------------------------------------------------- /packages/akita/src/lib/toBoolean.ts: -------------------------------------------------------------------------------- 1 | // @internal 2 | export function toBoolean(value: any): boolean { 3 | return value != null && `${value}` !== 'false'; 4 | } 5 | -------------------------------------------------------------------------------- /packages/akita/src/lib/toEntitiesIds.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ID_KEY } from './defaultIDKey'; 2 | 3 | // @internal 4 | export function toEntitiesIds(entities: E[], idKey = DEFAULT_ID_KEY) { 5 | const ids = []; 6 | for (const entity of entities) { 7 | ids.push(entity[idKey]); 8 | } 9 | return ids; 10 | } 11 | -------------------------------------------------------------------------------- /packages/akita/src/lib/toEntitiesObject.ts: -------------------------------------------------------------------------------- 1 | import { PreAddEntity } from './types'; 2 | 3 | // @internal 4 | export function toEntitiesObject(entities: E[], idKey: string, preAddEntity: PreAddEntity) { 5 | const acc = { 6 | entities: {}, 7 | ids: [] 8 | }; 9 | 10 | for (const entity of entities) { 11 | // evaluate the middleware first to support dynamic ids 12 | const current = preAddEntity(entity); 13 | acc.entities[current[idKey]] = current; 14 | acc.ids.push(current[idKey]); 15 | } 16 | 17 | return acc; 18 | } 19 | -------------------------------------------------------------------------------- /packages/akita/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true 16 | } 17 | } -------------------------------------------------------------------------------- /packages/akita/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["src/**/*.ts"], 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/__tests__"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/akita/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/ng-entity-service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "akitaNx", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "akita-nx", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/ng-entity-service/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [8.0.0](https://github.com/salesforce/akita/compare/ng-entity-service-7.0.0...ng-entity-service-8.0.0) (2023-01-09) 6 | 7 | 8 | ### Features 9 | 10 | * upgrade to Angular v15 ([c9af3ea](https://github.com/salesforce/akita/commit/c9af3eae3a1cec9fba48760736124d26fc14486b)) 11 | 12 | 13 | ### BREAKING CHANGES 14 | 15 | * Upgrade to Angular v15 16 | 17 | - Remove the schematics package 18 | - Remove the devtools package and provide a new simpler Angular alternative in the docs 19 | -------------------------------------------------------------------------------- /packages/ng-entity-service/README.md: -------------------------------------------------------------------------------- 1 | # ng-entity-service 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ng-entity-service` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /packages/ng-entity-service/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ng-entity-service', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | }, 10 | }, 11 | coverageDirectory: '../../coverage/packages/ng-entity-service', 12 | transform: { 13 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', 14 | }, 15 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 16 | snapshotSerializers: [ 17 | 'jest-preset-angular/build/serializers/no-ng-attributes', 18 | 'jest-preset-angular/build/serializers/ng-snapshot', 19 | 'jest-preset-angular/build/serializers/html-comment', 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /packages/ng-entity-service/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/packages/ng-entity-service", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ng-entity-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datorama/akita-ng-entity-service", 3 | "version": "8.0.0", 4 | "description": "Akita entity service", 5 | "keywords": [ 6 | "angular", 7 | "akita" 8 | ], 9 | "homepage": "https://github.com/datorama/akita/tree/master/libs/akita-ng-entity-service#readme", 10 | "bugs": { 11 | "url": "https://github.com/datorama/akita/issues" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/datorama/akita", 16 | "directory": "libs/akita-ng-entity-service" 17 | }, 18 | "license": "Apache-2.0", 19 | "author": "Netanel Basal", 20 | "peerDependencies": { 21 | "@datorama/akita": ">= 8.0.0", 22 | "@angular/core": ">= 15.0.0", 23 | "rxjs": "*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ng-entity-service/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectType": "library", 3 | "root": "packages/ng-entity-service", 4 | "sourceRoot": "packages/ng-entity-service/src", 5 | "prefix": "akita-nx", 6 | "targets": { 7 | "build": { 8 | "executor": "@nrwl/angular:package", 9 | "outputs": ["dist/packages/ng-entity-service"], 10 | "options": { 11 | "project": "packages/ng-entity-service/ng-package.json" 12 | }, 13 | "configurations": { 14 | "production": { 15 | "tsConfig": "packages/ng-entity-service/tsconfig.lib.prod.json" 16 | }, 17 | "development": { 18 | "tsConfig": "packages/ng-entity-service/tsconfig.lib.json" 19 | } 20 | }, 21 | "defaultConfiguration": "production" 22 | }, 23 | "test": { 24 | "executor": "@nrwl/jest:jest", 25 | "outputs": ["coverage/packages/ng-entity-service"], 26 | "options": { 27 | "jestConfig": "packages/ng-entity-service/jest.config.js", 28 | "passWithNoTests": true 29 | } 30 | }, 31 | "lint": { 32 | "executor": "@nrwl/linter:eslint", 33 | "options": { 34 | "lintFilePatterns": ["packages/ng-entity-service/src/**/*.ts", "packages/ng-entity-service/src/**/*.html"] 35 | } 36 | }, 37 | "version": { 38 | "executor": "@jscutlery/semver:version" 39 | } 40 | }, 41 | "tags": [], 42 | "name": "packages-ng-entity-service" 43 | } 44 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/ng-entity.service'; 2 | export * from './lib/ng-entity-service.config'; 3 | export * from './lib/ng-entity-service-notifier'; 4 | export * from './lib/helpers'; 5 | export * from './lib/types'; 6 | export * from './lib/ng-entity-service.loader'; 7 | export * from './lib/action-factory'; 8 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/action-factory.ts: -------------------------------------------------------------------------------- 1 | import { EntityServiceAction, NgEntityServiceNotifier } from './ng-entity-service-notifier'; 2 | 3 | export function successAction( 4 | storeName: string, 5 | notifier: NgEntityServiceNotifier 6 | ): (params: Partial) => void { 7 | return function ({ payload, method, successMsg }) { 8 | notifier.dispatch({ 9 | type: 'success', 10 | storeName, 11 | payload, 12 | method: method!, 13 | successMsg 14 | }); 15 | }; 16 | } 17 | 18 | export function errorAction( 19 | storeName: string, 20 | notifier: NgEntityServiceNotifier 21 | ): (params: Partial) => void { 22 | return function ({ payload, method, errorMsg }) { 23 | notifier.dispatch({ 24 | type: 'error', 25 | storeName, 26 | payload, 27 | method: method!, 28 | errorMsg 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { isNumber, isString } from '@datorama/akita'; 2 | 3 | export function isID(idOrConfig: any) { 4 | return isNumber(idOrConfig) || isString(idOrConfig); 5 | } 6 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/ng-entity-service-notifier.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { filter, Subject } from 'rxjs'; 3 | import { Msg } from './types'; 4 | 5 | export enum HttpMethod { 6 | GET = 'GET', 7 | POST = 'POST', 8 | PUT = 'PUT', 9 | PATCH = 'PATCH', 10 | DELETE = 'DELETE', 11 | } 12 | 13 | export type ActionType = 'success' | 'error'; 14 | 15 | export type EntityServiceAction = { 16 | storeName: string; 17 | type: ActionType; 18 | payload: any; 19 | method: HttpMethod; 20 | } & Msg; 21 | 22 | export const ofType = (type: ActionType) => filter((action: EntityServiceAction) => action.type === type); 23 | 24 | export const filterMethod = (method: HttpMethod | keyof typeof HttpMethod) => filter((action: EntityServiceAction) => action.method === method); 25 | 26 | export const filterStore = (name: string) => filter((action: EntityServiceAction) => action.storeName === name); 27 | 28 | @Injectable({ providedIn: 'root' }) 29 | export class NgEntityServiceNotifier { 30 | private dispatcher = new Subject(); 31 | action$ = this.dispatcher.asObservable(); 32 | 33 | dispatch(event: EntityServiceAction) { 34 | this.dispatcher.next(event); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/ng-entity-service.config.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { isObject } from '@datorama/akita'; 3 | import { HttpMethod } from './ng-entity-service-notifier'; 4 | import { NgEntityServiceParams } from './types'; 5 | 6 | export interface NgEntityServiceGlobalConfig { 7 | baseUrl?: string; 8 | httpMethods?: Partial<{ 9 | GET: HttpMethod; 10 | POST: HttpMethod; 11 | PATCH: HttpMethod; 12 | PUT: HttpMethod; 13 | DELETE: HttpMethod; 14 | }>; 15 | } 16 | 17 | export const NG_ENTITY_SERVICE_CONFIG = new InjectionToken('NgEntityServiceGlobalConfig'); 18 | 19 | export const defaultConfig: NgEntityServiceGlobalConfig = { 20 | httpMethods: { 21 | GET: HttpMethod.GET, 22 | POST: HttpMethod.POST, 23 | PATCH: HttpMethod.PATCH, 24 | PUT: HttpMethod.PUT, 25 | DELETE: HttpMethod.DELETE, 26 | }, 27 | }; 28 | 29 | export function mergeDeep(target, ...sources) { 30 | if (!sources.length) return target; 31 | const source = sources.shift(); 32 | 33 | if (isObject(target) && isObject(source)) { 34 | for (const key in source) { 35 | if (isObject(source[key])) { 36 | if (!target[key]) Object.assign(target, { [key]: {} }); 37 | mergeDeep(target[key], source[key]); 38 | } else { 39 | Object.assign(target, { [key]: source[key] }); 40 | } 41 | } 42 | } 43 | 44 | return mergeDeep(target, ...sources); 45 | } 46 | 47 | export function NgEntityServiceConfig(config: NgEntityServiceParams = {}) { 48 | return function (constructor) { 49 | if (config.baseUrl) { 50 | constructor['baseUrl'] = config.baseUrl; 51 | } 52 | 53 | if (config.resourceName) { 54 | constructor['resourceName'] = config.resourceName; 55 | } 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/ng-entity-service.loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { isFunction } from '@datorama/akita'; 3 | import { filter, map, ReplaySubject } from 'rxjs'; 4 | import { HttpMethod } from './ng-entity-service-notifier'; 5 | 6 | export type Event = { method: HttpMethod; loading: boolean; storeName: string; entityId?: any }; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class NgEntityServiceLoader { 10 | private dispatcher = new ReplaySubject(1); 11 | loading$ = this.dispatcher.asObservable(); 12 | 13 | dispatch(event: Event) { 14 | this.dispatcher.next(event); 15 | } 16 | 17 | loadersFor(name?: string) { 18 | const filterStore = filter(({ storeName }: Event) => (name ? storeName === name : true)); 19 | const filterMethod = (mthd) => 20 | filter(({ method }: Event) => { 21 | return isFunction(mthd) ? mthd(method) : method === mthd; 22 | }); 23 | 24 | const actionBased = (current: ((method) => boolean) | HttpMethod) => 25 | this.loading$.pipe( 26 | filterStore, 27 | filterMethod(current), 28 | map((action) => action.loading) 29 | ); 30 | 31 | const idBased = (id: any, mthd: ((method) => boolean) | HttpMethod) => 32 | this.loading$.pipe( 33 | filterStore, 34 | filterMethod(mthd), 35 | filter((action) => action.entityId === id), 36 | map((action) => action.loading) 37 | ); 38 | 39 | return { 40 | get$: actionBased(HttpMethod.GET), 41 | add$: actionBased(HttpMethod.POST), 42 | update$: actionBased((method) => method === HttpMethod.PUT || method === HttpMethod.PATCH), 43 | delete$: actionBased(HttpMethod.DELETE), 44 | getEntity: (id: any) => idBased(id, HttpMethod.GET), 45 | updateEntity: (id: any) => idBased(id, (method) => method === HttpMethod.PUT || method === HttpMethod.PATCH), 46 | deleteEntity: (id: any) => idBased(id, HttpMethod.DELETE), 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { HttpHeaders, HttpParams } from '@angular/common/http'; 2 | import { AddEntitiesOptions } from '@datorama/akita'; 3 | import { Observable } from 'rxjs'; 4 | import { HttpMethod } from './ng-entity-service-notifier'; 5 | 6 | export interface NgEntityServiceParams { 7 | baseUrl?: string; 8 | resourceName?: string; 9 | } 10 | 11 | type _HttpHeaders = 12 | | HttpHeaders 13 | | { 14 | [header: string]: string | string[]; 15 | }; 16 | 17 | type _HttpParams = 18 | | HttpParams 19 | | { 20 | [param: string]: string | string[]; 21 | }; 22 | 23 | export type Msg = { 24 | successMsg?: string; 25 | errorMsg?: string; 26 | }; 27 | 28 | export type HttpConfig = { 29 | params?: _HttpParams; 30 | headers?: _HttpHeaders; 31 | url?: string; 32 | urlPostfix?: string; 33 | mapResponseFn?: (res: any) => Entity | Entity[] | Observable; 34 | }; 35 | 36 | interface StoreWrite { 37 | /** 38 | * Disables writing to the store 39 | * 40 | * You then have to manually write to the store. 41 | * This is useful when pairing the NgEntityService with the PaginatorPlugin 42 | */ 43 | skipWrite?: boolean; 44 | } 45 | 46 | export type HttpGetConfig = HttpConfig & { 47 | append?: boolean; // TODO fix type these are mutually exclusive 48 | upsert?: boolean; 49 | } & StoreWrite & 50 | Msg; 51 | 52 | export type HttpAddConfig = HttpConfig & Pick & StoreWrite & Msg; 53 | 54 | export type HttpUpdateConfig = HttpConfig & { 55 | method?: HttpMethod.PUT | HttpMethod.PATCH; 56 | } & StoreWrite & 57 | Msg; 58 | 59 | export type HttpDeleteConfig = HttpConfig & StoreWrite & Msg; 60 | -------------------------------------------------------------------------------- /packages/ng-entity-service/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /packages/ng-entity-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true 19 | }, 20 | "angularCompilerOptions": { 21 | "strictInjectionParameters": true, 22 | "strictInputAccessModifiers": true, 23 | "strictTemplates": true 24 | } 25 | } -------------------------------------------------------------------------------- /packages/ng-entity-service/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "inlineSources": true, 8 | "types": [] 9 | }, 10 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-entity-service/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ng-entity-service/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "akitaNx", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "akita-nx", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/ng-playground/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ng-playground', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: '/tsconfig.spec.json', 9 | stringifyContentPathRegex: '\\.(html|svg)$', 10 | }, 11 | }, 12 | coverageDirectory: '../../coverage/packages/ng-playground', 13 | transform: { 14 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: ['jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment'], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuard } from './auth/auth.guard'; 4 | import { LoginComponent } from './auth/login/login.component'; 5 | import { CartComponent } from './cart/cart.component'; 6 | import { ProductPageComponent } from './product-page/product-page.component'; 7 | import { ProductsComponent } from './products/products.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | component: ProductsComponent, 12 | path: '', 13 | pathMatch: 'full', 14 | canActivate: [AuthGuard], 15 | }, 16 | { 17 | component: ProductPageComponent, 18 | path: 'product/:id', 19 | canActivate: [AuthGuard], 20 | }, 21 | { 22 | component: CartComponent, 23 | path: 'cart', 24 | canActivate: [AuthGuard], 25 | }, 26 | { 27 | component: LoginComponent, 28 | path: 'login', 29 | }, 30 | { 31 | path: 'todos', 32 | canActivate: [AuthGuard], 33 | loadChildren: () => import('./todos-app/todos.module').then((m) => m.TodosModule), 34 | }, 35 | { 36 | path: 'contacts', 37 | loadChildren: () => import('./contacts/contacts.module').then((m) => m.ContactsModule), 38 | }, 39 | { 40 | path: 'stories', 41 | loadChildren: () => import('./stories/stories.module').then((m) => m.StoriesModule), 42 | }, 43 | { 44 | path: 'movies', 45 | loadChildren: () => import('./movies/movies.module').then((m) => m.MoviesModule), 46 | }, 47 | { 48 | path: 'widgets', 49 | loadChildren: () => import('./widgets/widgets.module').then((m) => m.WidgetsModule), 50 | }, 51 | { 52 | path: 'posts', 53 | loadChildren: () => import('./posts/posts.module').then((m) => m.PostsModule), 54 | }, 55 | ]; 56 | 57 | @NgModule({ 58 | imports: [RouterModule.forRoot(routes)], 59 | exports: [RouterModule], 60 | }) 61 | export class AppRoutingModule {} 62 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterQuery } from '@datorama/akita-ng-router-store'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html' 7 | }) 8 | export class AppComponent { 9 | constructor(r: RouterQuery) { 10 | // r.select().subscribe(console.log); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { map, Observable, take } from 'rxjs'; 4 | import { AuthQuery } from './state/auth.query'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class AuthGuard { 10 | constructor(private router: Router, private authQuery: AuthQuery) {} 11 | 12 | canActivate(): Observable { 13 | // For sync storage 14 | return this.authQuery.isLoggedIn$.pipe( 15 | map((isAuth) => { 16 | if (isAuth) { 17 | return true; 18 | } 19 | this.router.navigateByUrl('login'); 20 | return false; 21 | }), 22 | take(1) 23 | ); 24 | 25 | // For async storage 26 | // return combineLatest([ 27 | // this.authQuery.isLoggedIn$, 28 | // selectPersistStateInit(), 29 | // ]).pipe( 30 | // map(([isAuth]) => { 31 | // if(isAuth) { 32 | // return true; 33 | // } 34 | // this.router.navigateByUrl('login'); 35 | // return false; 36 | // }), 37 | // take(1) 38 | // ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { LoginModule } from './login/login.module'; 3 | 4 | @NgModule({ 5 | imports: [LoginModule], 6 | }) 7 | export class AuthModule { } 8 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | email 6 | 7 | 8 |
9 |
10 | lock_open 11 | 12 | 13 |
14 |
15 | 16 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; 3 | import { AuthService, Creds } from '../state/auth.service'; 4 | import { Router } from '@angular/router'; 5 | 6 | @Component({ 7 | templateUrl: './login.component.html' 8 | }) 9 | export class LoginComponent implements OnInit { 10 | login: UntypedFormGroup; 11 | 12 | constructor(private fb: UntypedFormBuilder, private router: Router, private authService: AuthService) {} 13 | 14 | ngOnInit(): void { 15 | this.login = this.fb.group({ 16 | email: this.fb.control(''), 17 | password: this.fb.control('') 18 | }); 19 | } 20 | 21 | submit() { 22 | this.authService.login(this.login.value as Creds).subscribe(() => { 23 | this.router.navigate(['/']); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { CommonModule } from '@angular/common'; 4 | import { LoginComponent } from './login.component'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule, ReactiveFormsModule], 8 | declarations: [LoginComponent] 9 | }) 10 | export class LoginModule {} 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/state/auth.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { AuthState, AuthStore } from './auth.store'; 3 | import { Query } from '@datorama/akita'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AuthQuery extends Query { 7 | isLoggedIn$ = this.select(user => !!user.token); 8 | 9 | constructor(protected store: AuthStore) { 10 | super(store); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/state/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { mapTo, tap, timer } from 'rxjs'; 3 | import { AuthStore } from './auth.store'; 4 | 5 | export type Creds = { 6 | email: string; 7 | password: string; 8 | }; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class AuthService { 14 | constructor(private authStore: AuthStore) {} 15 | 16 | login(creds: Creds) { 17 | return simulateRequest(creds).pipe(tap((user) => this.authStore.update(user))); 18 | } 19 | 20 | logout() { 21 | this.authStore.reset(); 22 | } 23 | } 24 | 25 | export function simulateRequest(creds: Creds) { 26 | return timer(400).pipe( 27 | mapTo({ 28 | id: 1, 29 | firstName: 'Netanel', 30 | lastName: 'Basal', 31 | token: 'token', 32 | }) 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/auth/state/auth.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { StoreConfig, Store, ID } from '@datorama/akita'; 3 | 4 | export interface AuthState { 5 | id: ID; 6 | firstName: string; 7 | lastName: string; 8 | token: string; 9 | } 10 | 11 | export function createInitialState(): AuthState { 12 | return { 13 | id: null, 14 | firstName: '', 15 | lastName: '', 16 | token: '' 17 | }; 18 | } 19 | 20 | @Injectable({ providedIn: 'root' }) 21 | @StoreConfig({ 22 | name: 'auth', 23 | resettable: true 24 | }) 25 | export class AuthStore extends Store { 26 | constructor() { 27 | super(createInitialState()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/cart.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 |
TitleDescriptionQuantityTotalRemove
{{item.title}}{{item.description}}{{item.quantity}}{{item.total}}$ 20 | 23 |
27 | 28 |

Your cart is empty

29 | 30 |

31 | credit_card 32 | Total: {{total$ | async}}$ 33 |

34 | 35 |
36 | 37 |
-------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Product } from '../products/state/products.model'; 4 | import { CartQuery } from './state/cart.query'; 5 | import { CartItem } from './state/cart.model'; 6 | import { CartService } from './state/cart.service'; 7 | 8 | @Component({ 9 | selector: 'app-cart', 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | templateUrl: './cart.component.html' 12 | }) 13 | export class CartComponent implements OnInit { 14 | items$: Observable<(CartItem & Product)[]>; 15 | total$: Observable; 16 | 17 | constructor(private cartQuery: CartQuery, private cartService: CartService) { 18 | } 19 | 20 | ngOnInit() { 21 | this.items$ = this.cartQuery.selectItems$; 22 | this.total$ = this.cartQuery.selectTotal$; 23 | } 24 | 25 | remove({ productId }: CartItem) { 26 | this.cartService.remove(productId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/cart.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CartComponent } from './cart.component'; 4 | 5 | const publicApi = [CartComponent]; 6 | 7 | @NgModule({ 8 | imports: [CommonModule], 9 | declarations: [publicApi], 10 | exports: [publicApi] 11 | }) 12 | export class CartModule {} 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/state/cart.model.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '../../products/state/products.model'; 2 | 3 | export type CartItem = { 4 | productId: Product['id']; 5 | quantity: number; 6 | total: number; 7 | }; 8 | 9 | export function createCartItem(params: Partial) { 10 | return { 11 | total: 0, 12 | quantity: 1, 13 | ...params 14 | } as CartItem; 15 | } 16 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/state/cart.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { QueryEntity } from '@datorama/akita'; 3 | import { combineLatest, map, shareReplay } from 'rxjs'; 4 | import { ProductsQuery } from '../../products/state/products.query'; 5 | import { CartState, CartStore } from './cart.store'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class CartQuery extends QueryEntity { 9 | constructor(protected store: CartStore, private productsQuery: ProductsQuery) { 10 | super(store); 11 | } 12 | 13 | selectItems$ = combineLatest([this.selectAll(), this.productsQuery.selectAll({ asObject: true })]).pipe(map(joinItems), shareReplay({ bufferSize: 1, refCount: true })); 14 | 15 | selectTotal$ = this.selectItems$.pipe(map((items) => items.reduce((acc, item) => acc + item.total, 0))); 16 | } 17 | 18 | function joinItems([cartItems, products]) { 19 | return cartItems.map((cartItem) => { 20 | const product = products[cartItem.productId]; 21 | return { 22 | ...cartItem, 23 | ...product, 24 | total: cartItem.quantity * product.price, 25 | }; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/state/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CartStore } from './cart.store'; 3 | import { CartQuery } from './cart.query'; 4 | import { createCartItem } from './cart.model'; 5 | import { ID } from '@datorama/akita'; 6 | import { Product } from '../../products/state/products.model'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class CartService { 12 | constructor(private cartStore: CartStore, private cartQuery: CartQuery) {} 13 | 14 | addProductToCart(productId: Product['id']) { 15 | const findItem = this.cartQuery.getEntity(productId); 16 | if (!!findItem) { 17 | return this.cartStore.updateQuantity(productId); 18 | } 19 | 20 | const item = createCartItem({ 21 | productId 22 | }); 23 | 24 | return this.cartStore.add(item); 25 | } 26 | 27 | subtract(productId: Product['id']) { 28 | const findItem = this.cartQuery.getEntity(productId); 29 | if (!!findItem) { 30 | if (findItem.quantity === 1) { 31 | return this.cartStore.remove(productId); 32 | } 33 | 34 | return this.cartStore.updateQuantity(findItem.productId, -1); 35 | } 36 | } 37 | 38 | remove(productId: ID) { 39 | this.cartStore.remove(productId); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/cart/state/cart.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CartItem } from './cart.model'; 3 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 4 | import { Product } from '../../products/state/products.model'; 5 | 6 | export interface CartState extends EntityState {} 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | @StoreConfig({ 10 | name: 'cart', 11 | idKey: 'productId' 12 | }) 13 | export class CartStore extends EntityStore { 14 | constructor() { 15 | super(); 16 | } 17 | 18 | updateQuantity(productId: Product['id'], operand = 1) { 19 | this.update(productId, entity => { 20 | const newQuantity = entity.quantity + operand; 21 | return { 22 | ...entity, 23 | quantity: newQuantity 24 | }; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/contacts-page/contacts-page.component.css: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | } -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/contacts.data.ts: -------------------------------------------------------------------------------- 1 | import { randEmail, randFirstName, randNumber, randStreetAddress } from '@ngneat/falso'; 2 | import { sortBy } from 'lodash'; 3 | import { map, timer } from 'rxjs'; 4 | 5 | const count = 96; 6 | const data = []; 7 | 8 | for (let i = 0; i < count; i++) { 9 | data.push({ 10 | id: randNumber(), 11 | email: randEmail(), 12 | name: randFirstName(), 13 | address: randStreetAddress(), 14 | }); 15 | } 16 | 17 | export function getData(params = { sortBy: 'email', perPage: 10, page: 1 }) { 18 | console.log('Fetching from server'); 19 | const offset = (params.page - 1) * +params.perPage; 20 | const sorted = sortBy(contacts, params.sortBy); 21 | const paginatedItems = sorted.slice(offset, offset + +params.perPage); 22 | 23 | return { 24 | currentPage: params.page, 25 | perPage: +params.perPage, 26 | total: contacts.length, 27 | lastPage: Math.ceil(contacts.length / +params.perPage), 28 | data: paginatedItems, 29 | }; 30 | } 31 | 32 | export const getContacts = function (params) { 33 | return timer(300).pipe(map(() => getData(params))); 34 | }; 35 | export const contacts = data; 36 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/contacts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ContactsPageComponent } from './contacts-page/contacts-page.component'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | import { ReactiveFormsModule } from '@angular/forms'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: ContactsPageComponent 11 | } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [ 16 | CommonModule, 17 | ReactiveFormsModule, 18 | RouterModule.forChild(routes) 19 | ], 20 | declarations: [ContactsPageComponent] 21 | }) 22 | export class ContactsModule { 23 | } 24 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/state/contact.model.ts: -------------------------------------------------------------------------------- 1 | export type Contact = { 2 | name: string; 3 | email: string; 4 | address: string; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/state/contacts.pagination.ts: -------------------------------------------------------------------------------- 1 | import { inject, InjectionToken } from '@angular/core'; 2 | import { ContactsQuery } from './contacts.query'; 3 | import { Paginator } from '@datorama/akita'; 4 | import { ContactState } from './contacts.store'; 5 | 6 | export const CONTACTS_PAGINATOR = new InjectionToken('CONTACTS_PAGINATOR', { 7 | providedIn: 'root', 8 | factory: () => { 9 | return new Paginator(inject(ContactsQuery)).withControls().withRange(); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/state/contacts.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ContactsStore, ContactState } from './contacts.store'; 3 | import { QueryEntity } from '@datorama/akita'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class ContactsQuery extends QueryEntity { 7 | constructor(protected store: ContactsStore) { 8 | super(store); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/state/contacts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Contact } from './contact.model'; 4 | import { PaginationResponse } from '@datorama/akita'; 5 | import { getContacts } from '../contacts.data'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class ContactsService { 9 | getPage(params): Observable> { 10 | return getContacts(params); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/contacts/state/contacts.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Contact } from './contact.model'; 3 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface ContactState extends EntityState {} 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | @StoreConfig({ name: 'contacts' }) 9 | export class ContactsStore extends EntityStore { 10 | constructor() { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/actors/state/actor.model.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '@datorama/akita'; 2 | 3 | export type Actor = { 4 | id: ID; 5 | name: string; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/actors/state/actors.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActorsStore, ActorsState } from './actors.store'; 3 | import { QueryEntity } from '@datorama/akita'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class ActorsQuery extends QueryEntity { 7 | constructor(protected store: ActorsStore) { 8 | super(store); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/actors/state/actors.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actor } from './actor.model'; 3 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface ActorsState extends EntityState {} 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | @StoreConfig({ name: 'actors' }) 9 | export class ActorsStore extends EntityStore { 10 | constructor() { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/genres/state/genre.model.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '@datorama/akita'; 2 | 3 | export type Genre = { 4 | id: ID; 5 | name: string; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/genres/state/genres.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { GenresStore, GenresState } from './genres.store'; 3 | import { QueryEntity } from '@datorama/akita'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class GenresQuery extends QueryEntity { 7 | constructor(protected store: GenresStore) { 8 | super(store); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/genres/state/genres.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Genre } from './genre.model'; 3 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface GenresState extends EntityState {} 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | @StoreConfig({ name: 'genres' }) 9 | export class GenresStore extends EntityStore { 10 | constructor() { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/movies.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MoviesComponent } from './movies/movies.component'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: MoviesComponent, 10 | }, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, RouterModule.forChild(routes)], 15 | declarations: [MoviesComponent], 16 | }) 17 | export class MoviesModule { } 18 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/movies/movies.component.css: -------------------------------------------------------------------------------- 1 | span:not(:last-child)::after { 2 | content: ', ' 3 | } -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/movies/movies.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 | 7 |

Movies

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
TitleActorsGenres
{{movie.title}}{{actor.name}}{{genre.name}}
25 | 26 |

Actors

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 |
Name
37 | 39 | edit 40 |
45 | 46 |
47 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/movies/movies.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { FullMovie } from '../state/movie.model'; 4 | import { ID } from '@datorama/akita'; 5 | import { MoviesQuery } from '../state/movies.query'; 6 | import { MoviesService } from '../state/movies.service'; 7 | import { ActorsQuery } from '../actors/state/actors.query'; 8 | import { Actor } from '../actors/state/actor.model'; 9 | import { memo } from 'helpful-decorators'; 10 | 11 | @Component({ 12 | selector: 'app-movies', 13 | templateUrl: './movies.component.html', 14 | styleUrls: ['./movies.component.css'] 15 | }) 16 | export class MoviesComponent implements OnInit { 17 | movies$: Observable; 18 | actors$: Observable; 19 | isLoading$: Observable; 20 | private edits = new Set(); 21 | 22 | constructor(private moviesQuery: MoviesQuery, private actorsQuery: ActorsQuery, private moviesService: MoviesService) {} 23 | 24 | ngOnInit() { 25 | this.isLoading$ = this.moviesQuery.selectLoading(); 26 | 27 | this.movies$ = this.moviesQuery.selectMovies(); 28 | this.actors$ = this.actorsQuery.selectAll(); 29 | this.moviesService.getMovies().subscribe(); 30 | } 31 | 32 | edit(id: ID, name: string) { 33 | this.moviesService.updateActorName(id, name); 34 | this.edits.delete(id); 35 | } 36 | 37 | toggleView(id: ID, actorName: HTMLInputElement) { 38 | if (this.edits.has(id)) { 39 | this.edits.delete(id); 40 | } else { 41 | this.edits.add(id); 42 | actorName.focus(); 43 | } 44 | } 45 | 46 | inEditMode(id: ID) { 47 | return this.edits.has(id); 48 | } 49 | 50 | @memo() 51 | isOpen(id: ID) { 52 | return this.moviesQuery.ui.selectEntity(id, 'isOpen'); 53 | } 54 | 55 | markAsOpen(id: ID) { 56 | this.moviesService.markAsOpen(id); 57 | } 58 | 59 | deleteActor(actor: Actor) { 60 | this.moviesService.deleteActor(actor.id); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/normalized.ts: -------------------------------------------------------------------------------- 1 | export const movies = { 2 | entities: { 3 | genres: { 4 | '1': { 5 | id: 1, 6 | name: 'Action' 7 | }, 8 | '2': { 9 | id: 2, 10 | name: 'Adventure' 11 | }, 12 | '3': { 13 | id: 3, 14 | name: 'Crime' 15 | }, 16 | '4': { 17 | id: 4, 18 | name: 'Drama' 19 | }, 20 | '5': { 21 | id: 5, 22 | name: 'Mystery' 23 | }, 24 | '6': { 25 | id: 6, 26 | name: 'Sci-Fi' 27 | } 28 | }, 29 | actors: { 30 | '288': { 31 | id: 288, 32 | name: 'Christian Bale' 33 | }, 34 | '323': { 35 | id: 323, 36 | name: 'Michael Caine' 37 | }, 38 | '5132': { 39 | id: 5132, 40 | name: 'Heath Ledger' 41 | }, 42 | '413168': { 43 | id: 413168, 44 | name: 'Hugh Jackman' 45 | }, 46 | '3822462': { 47 | id: 3822462, 48 | name: 'Rila Fukushima' 49 | }, 50 | '5148840': { 51 | id: 5148840, 52 | name: 'Tao Okamoto' 53 | } 54 | }, 55 | movies: { 56 | '468569': { 57 | id: 468569, 58 | title: 'The Dark Knight', 59 | genres: [1, 3, 4], 60 | actors: [288, 5132, 323] 61 | }, 62 | '482571': { 63 | id: 482571, 64 | title: 'The Prestige', 65 | genres: [4, 5, 6], 66 | actors: [413168, 288, 323] 67 | }, 68 | '1430132': { 69 | id: 1430132, 70 | title: 'The Wolverine', 71 | genres: [1, 2, 6], 72 | actors: [413168, 5148840, 3822462] 73 | } 74 | } 75 | }, 76 | result: [468569, 482571, 1430132] 77 | }; 78 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/state/movie.model.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '@datorama/akita'; 2 | import { Actor } from '../actors/state/actor.model'; 3 | import { Genre } from '../genres/state/genre.model'; 4 | 5 | export type Movie = { 6 | id: ID; 7 | title: string; 8 | genres: ID[]; 9 | actors: ID[]; 10 | }; 11 | 12 | export interface FullMovie { 13 | id: ID; 14 | title: string; 15 | genres: Genre[]; 16 | actors: Actor[]; 17 | } 18 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/state/movies.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { combineQueries, EntityUIQuery, QueryEntity } from '@datorama/akita'; 3 | import { map } from 'rxjs'; 4 | import { ActorsQuery } from '../actors/state/actors.query'; 5 | import { GenresQuery } from '../genres/state/genres.query'; 6 | import { MoviesState, MoviesStore, MoviesUIState } from './movies.store'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class MoviesQuery extends QueryEntity { 10 | ui: EntityUIQuery; 11 | 12 | constructor(protected store: MoviesStore, private actorsQuery: ActorsQuery, private genresQuery: GenresQuery) { 13 | super(store); 14 | this.createUIQuery(); 15 | } 16 | 17 | selectMovies() { 18 | return combineQueries([this.selectAll(), this.actorsQuery.selectAll({ asObject: true }), this.genresQuery.selectAll({ asObject: true })]).pipe( 19 | map(([movies, actors, genres]) => { 20 | return movies.map((movie) => { 21 | return { 22 | ...movie, 23 | actors: movie.actors.map((actorId) => actors[actorId]), 24 | genres: movie.genres.map((genreId) => genres[genreId]), 25 | }; 26 | }); 27 | }) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/state/movies.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { arrayRemove, ID, transaction, withTransaction } from '@datorama/akita'; 3 | import { mapTo, of, timer } from 'rxjs'; 4 | import { ActorsStore } from '../actors/state/actors.store'; 5 | import { GenresStore } from '../genres/state/genres.store'; 6 | import { movies } from '../normalized'; 7 | import { MoviesQuery } from './movies.query'; 8 | import { MoviesStore } from './movies.store'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class MoviesService { 12 | constructor(private moviesStore: MoviesStore, private actorsStore: ActorsStore, private genresStore: GenresStore, private moviesQuery: MoviesQuery) {} 13 | 14 | getMovies() { 15 | const request$ = timer(1000).pipe( 16 | mapTo(movies), 17 | withTransaction((response) => { 18 | this.actorsStore.set(response.entities.actors); 19 | this.genresStore.set(response.entities.genres); 20 | const movies = { 21 | entities: response.entities.movies, 22 | ids: response.result, 23 | }; 24 | this.moviesStore.set(movies); 25 | }) 26 | ); 27 | 28 | return this.moviesQuery.getHasCache() ? of() : request$; 29 | } 30 | 31 | updateActorName(id: ID, name: string) { 32 | this.actorsStore.update(id, { name }); 33 | } 34 | 35 | markAsOpen(id: ID) { 36 | this.moviesStore.ui.update(id, (entity) => ({ isOpen: !entity.isOpen })); 37 | } 38 | 39 | @transaction() 40 | deleteActor(id: ID) { 41 | this.actorsStore.remove(id); 42 | this.moviesStore.update(null, (entity) => ({ actors: arrayRemove(entity.actors, id) })); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/movies/state/movies.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Movie } from './movie.model'; 3 | import { EntityState, EntityStore, EntityUIStore, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface MovieUI { 6 | isOpen: boolean; 7 | } 8 | 9 | export interface MoviesState extends EntityState {} 10 | 11 | export interface MoviesUIState extends EntityState {} 12 | 13 | @Injectable({ providedIn: 'root' }) 14 | @StoreConfig({ name: 'movies' }) 15 | export class MoviesStore extends EntityStore { 16 | ui: EntityUIStore; 17 | 18 | constructor() { 19 | super(); 20 | this.createUIStore().setInitialEntityState({ isOpen: true }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/nav/nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Router } from '@angular/router'; 4 | import { AuthService } from '../auth/state/auth.service'; 5 | import { AuthQuery } from '../auth/state/auth.query'; 6 | import { CartQuery } from '../cart/state/cart.query'; 7 | 8 | @Component({ 9 | selector: 'app-nav', 10 | template: ` 11 | 29 | ` 30 | }) 31 | export class NavComponent { 32 | navItems = ['Todos', 'Contacts', 'Stories', 'Movies', 'Widgets', 'Posts']; 33 | count$: Observable; 34 | isLoggedIn$: Observable; 35 | 36 | constructor(private cartQuery: CartQuery, private authService: AuthService, private authQuery: AuthQuery, private router: Router) { 37 | this.count$ = this.cartQuery.selectCount(); 38 | this.isLoggedIn$ = this.authQuery.isLoggedIn$; 39 | } 40 | 41 | logout() { 42 | this.authService.logout(); 43 | this.router.navigateByUrl('login'); 44 | } 45 | 46 | resetStores() { 47 | this.router.navigateByUrl('login'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/posts.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Entity Service

3 | 4 | Idle 5 | 6 |
Loaders
7 |

Get => Loading...

8 |

POST => Loading...

9 |

PUT => Loading...

10 |

DELETE => Loading...

11 | 12 |

PUT id 3 => Loading...

13 |

DELETE id 3 => Loading...

14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |

Loading...

26 | 27 | 28 |
29 | {{ post.title }} 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/posts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { filterMethod, NgEntityServiceLoader, NgEntityServiceNotifier, ofType } from '@datorama/akita-ng-entity-service'; 3 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 4 | import { memo } from 'helpful-decorators'; 5 | import { PostsQuery, PostsService } from './state'; 6 | 7 | @UntilDestroy() 8 | @Component({ 9 | templateUrl: './posts.component.html', 10 | styleUrls: ['./posts.css'], 11 | }) 12 | export class PostsComponent implements OnInit { 13 | posts$ = this.postsQuery.selectAll(); 14 | loaders = this.loader.loadersFor(); 15 | 16 | constructor(private postsQuery: PostsQuery, private postsService: PostsService, private loader: NgEntityServiceLoader, private notifier: NgEntityServiceNotifier) {} 17 | 18 | ngOnInit() { 19 | this.notifier.action$.pipe(ofType('success'), filterMethod('DELETE'), untilDestroyed(this)).subscribe((v) => console.log(v)); 20 | 21 | this.postsService 22 | .get({ 23 | mapResponseFn: (res) => { 24 | return res; 25 | }, 26 | }) 27 | .subscribe(); 28 | this.loaders.deleteEntity(3); 29 | } 30 | 31 | fetchOne() { 32 | this.postsService.get(1).subscribe(console.log); 33 | } 34 | 35 | add() { 36 | this.postsService.add({ id: 1222, title: 'New Post', body: '' }, { prepend: true }).subscribe(); 37 | } 38 | 39 | update(id) { 40 | this.postsService.update(id, { title: 'New title' }).subscribe(); 41 | } 42 | 43 | remove(id) { 44 | this.postsService 45 | .delete(id, { 46 | successMsg: 'Deleted Successfully', 47 | }) 48 | .subscribe(); 49 | } 50 | 51 | @memo() 52 | updateEntityLoading(id) { 53 | return this.loaders.updateEntity(id); 54 | } 55 | 56 | @memo() 57 | deleteEntityLoading(id) { 58 | return this.loaders.deleteEntity(id); 59 | } 60 | 61 | ngOnDestroy() {} 62 | } 63 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/posts.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/ng-playground/src/app/posts/posts.css -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PostsComponent } from './posts.component'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: PostsComponent, 10 | }, 11 | ]; 12 | 13 | @NgModule({ 14 | declarations: [PostsComponent], 15 | imports: [CommonModule, RouterModule.forChild(routes)], 16 | }) 17 | export class PostsModule { } 18 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './posts.query'; 2 | export * from './posts.store'; 3 | export * from './posts.service'; 4 | export * from './post.model'; 5 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/state/post.model.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | id: number; 3 | title: string; 4 | body: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/state/posts.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { QueryEntity } from '@datorama/akita'; 3 | import { PostsStore, PostsState } from './posts.store'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class PostsQuery extends QueryEntity { 7 | 8 | constructor(protected store: PostsStore) { 9 | super(store); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/state/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { PostsState, PostsStore } from './posts.store'; 3 | import { NgEntityService } from '@datorama/akita-ng-entity-service'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class PostsService extends NgEntityService { 7 | constructor(protected store: PostsStore) { 8 | super(store); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/posts/state/posts.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 3 | import { Post } from './post.model'; 4 | 5 | export interface PostsState extends EntityState {} 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | @StoreConfig({ name: 'posts' }) 9 | export class PostsStore extends EntityStore { 10 | 11 | constructor() { 12 | super(); 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/product-page/product-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 4 | import { filter, map, switchMap } from 'rxjs'; 5 | import { ProductsQuery } from '../products/state/products.query'; 6 | import { ProductsService } from '../products/state/products.service'; 7 | 8 | @UntilDestroy() 9 | @Component({ 10 | template: ` 11 |
12 |

{{ product.title }}

13 |
{{ product.description }}
14 |
15 | `, 16 | }) 17 | export class ProductPageComponent implements OnInit, OnDestroy { 18 | product$ = this.productsQuery.selectEntity(this.productId); 19 | 20 | constructor(private activatedRoute: ActivatedRoute, private productsService: ProductsService, private productsQuery: ProductsQuery) {} 21 | 22 | ngOnInit() { 23 | this.activatedRoute.paramMap 24 | .pipe( 25 | map((params) => params.get('id')), 26 | filter((id) => !this.productsQuery.hasEntity(id)), 27 | untilDestroyed(this), 28 | switchMap((id) => this.productsService.getProduct(id)) 29 | ) 30 | .subscribe(); 31 | } 32 | 33 | get productId() { 34 | return this.activatedRoute.snapshot.params.id; 35 | } 36 | 37 | ngOnDestroy() {} 38 | } 39 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/product/product.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Price: {{product.price}}$

7 |

{{product.description}}

8 |
9 |
10 | 13 | 16 |
17 |
-------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Product } from '../state/products.model'; 3 | 4 | @Component({ 5 | selector: 'app-product', 6 | changeDetection: ChangeDetectionStrategy.OnPush, 7 | templateUrl: `./product.component.html` 8 | }) 9 | export class ProductComponent { 10 | @Input() product: Product; 11 | @Output() add = new EventEmitter(); 12 | @Output() subtract = new EventEmitter(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/products.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | view_list 5 | Products 6 |

7 | 8 | 9 | 10 | 11 |
12 | 13 | 17 |
18 | 19 | 20 |
21 | search 22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 | 47 |
48 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/products.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { UntypedFormControl } from '@angular/forms'; 3 | import { combineLatest, Observable, startWith, switchMap } from 'rxjs'; 4 | import { CartService } from '../cart/state/cart.service'; 5 | import { Product } from './state/products.model'; 6 | import { ProductsQuery } from './state/products.query'; 7 | import { ProductsService } from './state/products.service'; 8 | 9 | @Component({ 10 | selector: 'app-products', 11 | templateUrl: `./products.component.html`, 12 | }) 13 | export class ProductsComponent implements OnInit { 14 | products$: Observable; 15 | loading$: Observable; 16 | search = new UntypedFormControl(); 17 | sortControl = new UntypedFormControl('title'); 18 | 19 | constructor(private productsService: ProductsService, private cartService: CartService, private productsQuery: ProductsQuery) {} 20 | 21 | ngOnInit() { 22 | this.productsService.get().subscribe(); 23 | this.loading$ = this.productsQuery.selectLoading(); 24 | this.products$ = combineLatest([this.search.valueChanges.pipe(startWith('')), this.sortControl.valueChanges.pipe(startWith('title'))]).pipe( 25 | switchMap(([term, sortBy]) => this.productsQuery.getProducts(term, sortBy as keyof Product)) 26 | ); 27 | } 28 | 29 | addProductToCart({ id }: Product) { 30 | this.cartService.addProductToCart(id); 31 | } 32 | 33 | subtract({ id }: Product) { 34 | this.cartService.subtract(id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/products.mocks.ts: -------------------------------------------------------------------------------- 1 | export const products = [ 2 | { 3 | id: 3, 4 | title: 'Rx', 5 | description: 'Is a set of libraries to compose asynchronous and event-based programs using observable collections and Array style composition in JavaScript', 6 | price: 30 7 | }, 8 | { 9 | id: 1, 10 | title: 'JavaScript', 11 | description: 'JavaScript, often abbreviated as JS, is a high-level, interpreted programming language.', 12 | price: 10 13 | }, 14 | { 15 | id: 2, 16 | title: 'Angular', 17 | description: 'Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target.', 18 | price: 20 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ProductsComponent } from './products.component'; 4 | import { ProductComponent } from './product/product.component'; 5 | import { RouterModule } from '@angular/router'; 6 | import { ReactiveFormsModule } from '@angular/forms'; 7 | 8 | const publicApi = [ProductsComponent, ProductComponent]; 9 | 10 | @NgModule({ 11 | imports: [CommonModule, RouterModule, ReactiveFormsModule], 12 | declarations: [publicApi], 13 | exports: [publicApi], 14 | }) 15 | export class ProductsModule { } 16 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/state/products.model.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '@datorama/akita'; 2 | 3 | export type Product = { 4 | id: ID; 5 | title: string; 6 | description: string; 7 | price: number; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/state/products.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ProductsState, ProductsStore } from './products.store'; 3 | import { Product } from './products.model'; 4 | import { QueryConfig, QueryEntity } from '@datorama/akita'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | @QueryConfig({ sortBy: 'price' }) 8 | export class ProductsQuery extends QueryEntity { 9 | constructor(protected store: ProductsStore) { 10 | super(store); 11 | } 12 | 13 | getProducts(term: string, sortBy: keyof Product) { 14 | return this.selectAll({ 15 | sortBy, 16 | filterBy: entity => entity.title.toLowerCase().includes(term) 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/state/products.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { cacheable, ID } from '@datorama/akita'; 3 | import { map, mapTo, Observable, timer } from 'rxjs'; 4 | import { products } from '../products.mocks'; 5 | import { ProductsStore } from './products.store'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class ProductsService { 11 | constructor(private productsStore: ProductsStore) {} 12 | 13 | get(): Observable { 14 | const request = timer(500).pipe( 15 | mapTo(products), 16 | map((response) => this.productsStore.set(response)) 17 | ); 18 | 19 | return cacheable(this.productsStore, request); 20 | } 21 | 22 | getProduct(id: ID) { 23 | const product = products.find((current) => current.id === +id); 24 | 25 | return timer(500).pipe( 26 | mapTo(product), 27 | map(() => this.productsStore.add(product)) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/products/state/products.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Product } from './products.model'; 3 | import { EntityState, EntityStore, MultiActiveState, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface ProductsState extends EntityState, MultiActiveState {} 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | @StoreConfig({ name: 'products' }) 9 | export class ProductsStore extends EntityStore { 10 | constructor() { 11 | super({ active: [] }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/state/stories.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { StoriesStore, StoriesState } from './stories.store'; 3 | import { QueryEntity } from '@datorama/akita'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class StoriesQuery extends QueryEntity { 9 | constructor(protected store: StoriesStore) { 10 | super(store); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/state/stories.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { mapTo, tap, timer } from 'rxjs'; 3 | import { StoriesStore } from './stories.store'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class StoriesService { 7 | constructor(private storiesStore: StoriesStore) {} 8 | 9 | add(story) { 10 | this.storiesStore.setLoading(true); 11 | return timer(1000) 12 | .pipe(mapTo(story)) 13 | .pipe( 14 | tap((story) => { 15 | this.storiesStore.setLoading(false); 16 | this.storiesStore.add(story); 17 | }) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/state/stories.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Story } from './story.model'; 3 | import { EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 4 | 5 | export interface StoriesState extends EntityState { 6 | loading: boolean; 7 | someBoolean: boolean; 8 | skills: string[]; 9 | config: { 10 | tankOwners: string[]; 11 | time: string; 12 | isAdmin: boolean; 13 | }; 14 | } 15 | 16 | const initialState: StoriesState = { 17 | loading: false, 18 | someBoolean: true, 19 | skills: ['JS'], 20 | config: { 21 | time: '', 22 | tankOwners: ['one', 'two '], 23 | isAdmin: false 24 | } 25 | }; 26 | 27 | @Injectable({ providedIn: 'root' }) 28 | @StoreConfig({ name: 'stories' }) 29 | export class StoriesStore extends EntityStore { 30 | constructor() { 31 | super(initialState); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/state/story.model.ts: -------------------------------------------------------------------------------- 1 | export type Story = { 2 | title: string; 3 | story: string; 4 | draft: boolean; 5 | category: string; 6 | }; 7 | 8 | export function createStory() { 9 | return { 10 | title: '', 11 | story: '', 12 | draft: false, 13 | category: 'js' 14 | } as Story; 15 | } 16 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/stories.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { StoriesComponent } from './stories/stories.component'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { RouterModule, Routes } from '@angular/router'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: StoriesComponent 11 | } 12 | ]; 13 | 14 | @NgModule({ 15 | imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)], 16 | declarations: [StoriesComponent] 17 | }) 18 | export class StoriesModule {} 19 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/stories/stories/stories.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/ng-playground/src/app/stories/stories/stories.component.css -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/filter/filter.model.ts: -------------------------------------------------------------------------------- 1 | export enum VISIBILITY_FILTER { 2 | SHOW_COMPLETED = 'SHOW_COMPLETED', 3 | SHOW_ACTIVE = 'SHOW_ACTIVE', 4 | SHOW_ALL = 'SHOW_ALL' 5 | } 6 | 7 | export interface TodoFilter { 8 | label: string; 9 | value: VISIBILITY_FILTER; 10 | } 11 | 12 | export const initialFilters: TodoFilter[] = [ 13 | { label: 'All', value: VISIBILITY_FILTER.SHOW_ALL }, 14 | { label: 'Completed', value: VISIBILITY_FILTER.SHOW_COMPLETED }, 15 | { label: 'Active', value: VISIBILITY_FILTER.SHOW_ACTIVE } 16 | ]; 17 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/filter/todos-filters.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; 2 | import { UntypedFormControl } from '@angular/forms'; 3 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 4 | import { TodoFilter, VISIBILITY_FILTER } from './filter.model'; 5 | 6 | @UntilDestroy() 7 | @Component({ 8 | selector: 'app-todos-filters', 9 | template: ` 10 |
11 | 14 |
15 | `, 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class TodosFiltersComponent implements OnInit, OnDestroy { 19 | _active; 20 | @Input() 21 | set active(filter: VISIBILITY_FILTER) { 22 | this._active = filter; 23 | if (this.control) { 24 | this.control.patchValue(filter, { emitEvent: false }); 25 | } 26 | } 27 | @Input() 28 | filters: TodoFilter[]; 29 | @Output() 30 | update = new EventEmitter(); 31 | 32 | control: UntypedFormControl; 33 | 34 | ngOnInit() { 35 | this.control = new UntypedFormControl(this._active); 36 | 37 | this.control.valueChanges.pipe(untilDestroyed(this)).subscribe((c) => { 38 | this.update.emit(c); 39 | }); 40 | } 41 | 42 | ngOnDestroy() {} 43 | } 44 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/state/todo.model.ts: -------------------------------------------------------------------------------- 1 | import { guid, ID } from '@datorama/akita'; 2 | 3 | export type Todo = { 4 | id: ID; 5 | title: string; 6 | completed: boolean; 7 | }; 8 | 9 | export function createTodo(title: Todo['title']) { 10 | return { 11 | id: guid(), 12 | title, 13 | completed: false 14 | } as Todo; 15 | } 16 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/state/todos.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { QueryEntity } from '@datorama/akita'; 3 | import { combineLatest, map } from 'rxjs'; 4 | import { VISIBILITY_FILTER } from '../filter/filter.model'; 5 | import { Todo } from './todo.model'; 6 | import { TodosState, TodosStore } from './todos.store'; 7 | 8 | @Injectable({ providedIn: 'root' }) 9 | export class TodosQuery extends QueryEntity { 10 | selectVisibilityFilter$ = this.select((state) => state.ui.filter); 11 | 12 | selectVisibleTodos$ = combineLatest([this.selectVisibilityFilter$, this.selectAll()]).pipe( 13 | map(([filter, todos]) => { 14 | return this.getVisibleTodos(filter, todos); 15 | }) 16 | ); 17 | 18 | checkAll$ = this.selectCount((entity) => entity.completed); 19 | 20 | constructor(protected store: TodosStore) { 21 | super(store); 22 | } 23 | 24 | private getVisibleTodos(filter, todos): Todo[] { 25 | switch (filter) { 26 | case VISIBILITY_FILTER.SHOW_COMPLETED: 27 | return todos.filter((t) => t.completed); 28 | case VISIBILITY_FILTER.SHOW_ACTIVE: 29 | return todos.filter((t) => !t.completed); 30 | default: 31 | return todos; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/state/todos.service.ts: -------------------------------------------------------------------------------- 1 | import { TodosStore } from './todos.store'; 2 | import { createTodo, Todo } from './todo.model'; 3 | import { Injectable, Inject } from '@angular/core'; 4 | import { VISIBILITY_FILTER } from '../filter/filter.model'; 5 | import { ID, action } from '@datorama/akita'; 6 | 7 | @Injectable({ providedIn: 'root' }) 8 | export class TodosService { 9 | constructor(private todosStore: TodosStore) {} 10 | 11 | @action('Update filter') 12 | updateFilter(filter: VISIBILITY_FILTER) { 13 | this.todosStore.update({ 14 | ui: { 15 | filter 16 | } 17 | }); 18 | } 19 | 20 | complete({ id, completed }: Todo) { 21 | this.todosStore.update(id, { completed }); 22 | } 23 | 24 | add(title: string) { 25 | const todo = createTodo(title); 26 | this.todosStore.add(todo); 27 | } 28 | 29 | delete(id: ID) { 30 | this.todosStore.remove(id); 31 | } 32 | 33 | checkAll(completed: boolean) { 34 | this.todosStore.update(null, { 35 | completed 36 | }); 37 | } 38 | 39 | move(index: number) { 40 | console.log('TCL: move -> index', index); 41 | this.todosStore.move(index, index - 1); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/state/todos.store.ts: -------------------------------------------------------------------------------- 1 | import { Todo } from './todo.model'; 2 | import { VISIBILITY_FILTER } from '../filter/filter.model'; 3 | import { Injectable } from '@angular/core'; 4 | import { ActiveState, EntityState, EntityStore, StoreConfig } from '@datorama/akita'; 5 | 6 | export interface TodosState extends EntityState, ActiveState { 7 | ui: { 8 | filter: VISIBILITY_FILTER; 9 | }; 10 | } 11 | 12 | @Injectable({ providedIn: 'root' }) 13 | @StoreConfig({ name: 'todos' }) 14 | export class TodosStore extends EntityStore { 15 | constructor() { 16 | super({ 17 | ui: { filter: VISIBILITY_FILTER.SHOW_ALL } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/todo/todo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 | {{todo.title}} 8 |
9 | 10 | delete_forever 11 | 12 |
-------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/todo/todo.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; 2 | import { UntypedFormControl } from '@angular/forms'; 3 | import { ID } from '@datorama/akita'; 4 | import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; 5 | import { Todo } from '../state/todo.model'; 6 | 7 | @UntilDestroy() 8 | @Component({ 9 | selector: 'app-todo', 10 | templateUrl: './todo.component.html', 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | }) 13 | export class TodoComponent implements OnInit, OnDestroy { 14 | @Input() 15 | todo: Todo; 16 | @Output() 17 | complete = new EventEmitter(); 18 | @Output() 19 | delete = new EventEmitter(); 20 | 21 | control: UntypedFormControl; 22 | 23 | ngOnInit() { 24 | this.control = new UntypedFormControl(this.todo.completed); 25 | 26 | this.control.valueChanges.pipe(untilDestroyed(this)).subscribe((completed: boolean) => { 27 | this.complete.emit({ ...this.todo, completed }); 28 | }); 29 | } 30 | 31 | ngOnDestroy(): void {} 32 | } 33 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/todos-page/todos-page.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/ng-playground/src/app/todos-app/todos-page/todos-page.component.css -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/todos.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { TodoComponent } from './todo/todo.component'; 4 | import { TodosComponent } from './todos/todos.component'; 5 | import { ReactiveFormsModule } from '@angular/forms'; 6 | import { TodosFiltersComponent } from './filter/todos-filters.component'; 7 | import { TodosPageComponent } from './todos-page/todos-page.component'; 8 | import { RouterModule, Routes } from '@angular/router'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: TodosPageComponent 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)], 19 | exports: [TodosComponent, TodosFiltersComponent], 20 | declarations: [TodoComponent, TodosComponent, TodosFiltersComponent, TodosPageComponent] 21 | }) 22 | export class TodosModule {} 23 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/todos-app/todos/todos.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Todo } from '../state/todo.model'; 3 | import { ID } from '@datorama/akita'; 4 | 5 | @Component({ 6 | selector: 'app-todos', 7 | template: ` 8 |
9 |

Todos:

10 | 15 |
16 | `, 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class TodosComponent { 20 | @Input() 21 | todos: Todo[]; 22 | @Output() 23 | complete = new EventEmitter(); 24 | @Output() 25 | delete = new EventEmitter(); 26 | } 27 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/widgets/state/widget.model.ts: -------------------------------------------------------------------------------- 1 | import { ID } from '@datorama/akita'; 2 | 3 | export type Widget = { 4 | id: ID; 5 | name: string; 6 | }; 7 | 8 | let _id = 0; 9 | 10 | export function createWidget() { 11 | return { 12 | id: ++_id, 13 | name: `Widget ${_id}` 14 | } as Widget; 15 | } 16 | 17 | export function resetId(count?: number) { 18 | _id = count || 0; 19 | } 20 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/widgets/state/widgets.query.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { QueryEntity } from '@datorama/akita'; 3 | import { WidgetsState, WidgetsStore } from './widgets.store'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class WidgetsQuery extends QueryEntity { 7 | constructor(protected store: WidgetsStore) { 8 | super(store); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/widgets/state/widgets.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WidgetsStore } from './widgets.store'; 3 | import { createWidget } from './widget.model'; 4 | import { ID } from '@datorama/akita'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class WidgetsService { 8 | constructor(private widgetsStore: WidgetsStore) {} 9 | 10 | initWidgets() { 11 | const widgets = [createWidget(), createWidget(), createWidget(), createWidget(), createWidget()]; 12 | this.widgetsStore.set(widgets); 13 | } 14 | 15 | updateWidget(id: ID, name: string) { 16 | this.widgetsStore.update(id, { name }); 17 | } 18 | 19 | add() { 20 | this.widgetsStore.add(createWidget()); 21 | } 22 | 23 | remove(id?: ID) { 24 | this.widgetsStore.remove(id); 25 | } 26 | 27 | updateName(name: string) { 28 | this.widgetsStore.update({ name }); 29 | } 30 | 31 | addActive(id: ID) { 32 | this.widgetsStore.addActive(id); 33 | } 34 | 35 | removeActive(id: ID) { 36 | this.widgetsStore.removeActive(id); 37 | } 38 | 39 | toggleActive(id: ID) { 40 | this.widgetsStore.toggleActive(id); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/widgets/state/widgets.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Widget } from './widget.model'; 3 | import { EntityState, EntityStore, StoreConfig, MultiActiveState } from '@datorama/akita'; 4 | 5 | export interface WidgetsState extends EntityState, MultiActiveState { 6 | name: string; 7 | } 8 | 9 | const initState = { 10 | name: 'Akita widgets', 11 | active: [] 12 | }; 13 | 14 | @Injectable({ providedIn: 'root' }) 15 | @StoreConfig({ name: 'widgets' }) 16 | export class WidgetsStore extends EntityStore { 17 | constructor() { 18 | super(initState); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ng-playground/src/app/widgets/widgets.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { WidgetsComponent } from './widgets.component'; 4 | import { RouterModule, Routes } from '@angular/router'; 5 | 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: WidgetsComponent 10 | } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, RouterModule.forChild(routes)], 15 | declarations: [WidgetsComponent] 16 | }) 17 | export class WidgetsModule {} 18 | -------------------------------------------------------------------------------- /packages/ng-playground/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/ng-playground/src/assets/.gitkeep -------------------------------------------------------------------------------- /packages/ng-playground/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ng-playground/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /packages/ng-playground/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/packages/ng-playground/src/favicon.ico -------------------------------------------------------------------------------- /packages/ng-playground/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Akita Store 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/ng-playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /packages/ng-playground/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .padding { 3 | padding: 2em; 4 | } 5 | 6 | app-product { 7 | margin-right: 2em; 8 | } 9 | 10 | .flex { 11 | display: flex; 12 | } 13 | 14 | .align-center { 15 | align-items: center; 16 | } 17 | 18 | .mt { 19 | margin-top: 1em; 20 | } 21 | 22 | .p-width { 23 | width: 300px; 24 | } 25 | 26 | .mbb { 27 | margin-bottom: 5px; 28 | } 29 | 30 | .flex-end { 31 | justify-content: flex-end; 32 | } 33 | 34 | .mr { 35 | margin-right: 10px; 36 | } 37 | 38 | .nav-wrapper { 39 | padding: 0 25px; 40 | } 41 | 42 | .flex-column { 43 | flex-direction: column; 44 | } 45 | 46 | .height { 47 | min-height: 100vh; 48 | } 49 | 50 | .flex-1 { 51 | flex: 1; 52 | } 53 | 54 | .block { 55 | display: block; 56 | } 57 | 58 | .page-footer { 59 | padding-top: 0; 60 | } 61 | 62 | .mr5 { 63 | margin-right: 5px; 64 | } 65 | 66 | app-todo { 67 | display: block; 68 | } 69 | 70 | .sb { 71 | justify-content: space-between; 72 | } 73 | 74 | .pointer { 75 | cursor: pointer; 76 | } 77 | 78 | input:not([type]).actor-input { 79 | height: auto; 80 | margin-bottom: 0; 81 | border: none !important; 82 | } 83 | 84 | .actor-input.view { 85 | pointer-events: none; 86 | } 87 | 88 | .logo { 89 | width: 55px; 90 | transform: translateY(8px); 91 | } 92 | 93 | .blue-text.text-lighten-2 { 94 | color: #333333 !important; 95 | } 96 | 97 | code { 98 | color: #e83e8c; 99 | word-break: break-word; 100 | font-size: 14px; 101 | } 102 | 103 | .btn { 104 | background-color: #ee6e73 !important; 105 | } 106 | -------------------------------------------------------------------------------- /packages/ng-playground/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /packages/ng-playground/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [], 6 | "target": "ES2022", 7 | "useDefineForClassFields": false 8 | }, 9 | "files": ["src/main.ts", "src/polyfills.ts"], 10 | "include": ["src/**/*.d.ts"], 11 | "exclude": ["**/*.test.ts", "**/*.spec.ts", "jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-playground/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ng-playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ng-playground/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ng-router-store/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "akitaNx", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "akita-nx", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /packages/ng-router-store/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | # [8.0.0](https://github.com/salesforce/akita/compare/ng-router-store-7.0.0...ng-router-store-8.0.0) (2023-01-09) 6 | 7 | 8 | ### Features 9 | 10 | * upgrade to Angular v15 ([c9af3ea](https://github.com/salesforce/akita/commit/c9af3eae3a1cec9fba48760736124d26fc14486b)) 11 | 12 | 13 | ### BREAKING CHANGES 14 | 15 | * Upgrade to Angular v15 16 | 17 | - Remove the schematics package 18 | - Remove the devtools package and provide a new simpler Angular alternative in the docs 19 | -------------------------------------------------------------------------------- /packages/ng-router-store/README.md: -------------------------------------------------------------------------------- 1 | # ng-router-store 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ng-router-store` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /packages/ng-router-store/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'ng-router-store', 4 | preset: '../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: '/tsconfig.spec.json', 9 | stringifyContentPathRegex: '\\.(html|svg)$', 10 | }, 11 | }, 12 | coverageDirectory: '../../coverage/packages/ng-router-store', 13 | transform: { 14 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: ['jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/html-comment'], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/ng-router-store/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/packages/ng-router-store", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ng-router-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datorama/akita-ng-router-store", 3 | "version": "8.0.0", 4 | "description": "Bindings to connect the Angular Router with Akita store", 5 | "keywords": [ 6 | "angular", 7 | "akita", 8 | "akita router", 9 | "angular router" 10 | ], 11 | "homepage": "https://github.com/datorama/akita/tree/master/libs/akita-ng-router-store#readme", 12 | "bugs": { 13 | "url": "https://github.com/datorama/akita/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/datorama/akita", 18 | "directory": "libs/akita-ng-router-store" 19 | }, 20 | "license": "Apache-2.0", 21 | "maintainers": [ 22 | "Netanel Basal", 23 | "Shahar Kazaz" 24 | ], 25 | "peerDependencies": { 26 | "@datorama/akita": ">= 8.0.0", 27 | "@angular/core": ">= 15.0.0", 28 | "rxjs": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/ng-router-store/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-router-store", 3 | "projectType": "library", 4 | "sourceRoot": "packages/ng-router-store/src", 5 | "prefix": "akita-nx", 6 | "targets": { 7 | "build": { 8 | "executor": "@nrwl/angular:package", 9 | "outputs": ["{workspaceRoot}/dist/packages/ng-router-store"], 10 | "options": { 11 | "project": "packages/ng-router-store/ng-package.json" 12 | }, 13 | "configurations": { 14 | "production": { 15 | "tsConfig": "packages/ng-router-store/tsconfig.lib.prod.json" 16 | }, 17 | "development": { 18 | "tsConfig": "packages/ng-router-store/tsconfig.lib.json" 19 | } 20 | }, 21 | "defaultConfiguration": "production" 22 | }, 23 | "test": { 24 | "executor": "@nrwl/jest:jest", 25 | "outputs": ["{workspaceRoot}/coverage/packages/ng-router-store"], 26 | "options": { 27 | "jestConfig": "packages/ng-router-store/jest.config.ts", 28 | "passWithNoTests": true 29 | } 30 | }, 31 | "lint": { 32 | "executor": "@nrwl/linter:eslint", 33 | "options": { 34 | "lintFilePatterns": ["packages/ng-router-store/src/**/*.ts", "packages/ng-router-store/src/**/*.html"] 35 | } 36 | }, 37 | "version": { 38 | "executor": "@jscutlery/semver:version" 39 | } 40 | }, 41 | "tags": [] 42 | } 43 | -------------------------------------------------------------------------------- /packages/ng-router-store/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/router.module'; 2 | export * from './lib/router.query'; 3 | export * from './lib/router.service'; 4 | export * from './lib/router.store'; 5 | -------------------------------------------------------------------------------- /packages/ng-router-store/src/lib/router.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterService } from './router.service'; 3 | 4 | @NgModule() 5 | export class AkitaNgRouterStoreModule { 6 | constructor(private routerService: RouterService) { 7 | this.routerService.init(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ng-router-store/src/lib/router.store.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store, StoreConfig, HashMap } from '@datorama/akita'; 3 | 4 | export type ActiveRouteState = { 5 | url: string; 6 | urlAfterRedirects: string; 7 | fragment: string; 8 | params: HashMap; 9 | queryParams: HashMap; 10 | data: HashMap; 11 | navigationExtras: HashMap | undefined; 12 | }; 13 | 14 | export type RouterState = { 15 | state: ActiveRouteState | null; 16 | navigationId: number | null; 17 | }; 18 | 19 | export function createInitialRouterState(): RouterState { 20 | return { 21 | state: null, 22 | navigationId: null 23 | }; 24 | } 25 | 26 | @Injectable({ providedIn: 'root' }) 27 | @StoreConfig({ name: 'router' }) 28 | export class RouterStore extends Store { 29 | constructor() { 30 | super(createInitialRouterState()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ng-router-store/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /packages/ng-router-store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.lib.prod.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "forceConsistentCasingInFileNames": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "target": "es2020" 20 | }, 21 | "angularCompilerOptions": { 22 | "strictInjectionParameters": true, 23 | "strictInputAccessModifiers": true, 24 | "strictTemplates": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/ng-router-store/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "target": "ES2022", 10 | "useDefineForClassFields": false 11 | }, 12 | "exclude": ["src/test-setup.ts", "**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/ng-router-store/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false, 5 | "target": "ES2022", 6 | "useDefineForClassFields": false 7 | }, 8 | "angularCompilerOptions": { 9 | "compilationMode": "partial" 10 | }, 11 | "exclude": ["jest.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ng-router-store/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "emitDecoratorMetadata": false // see https://github.com/thymikee/jest-preset-angular/issues/1199 8 | }, 9 | "files": ["src/test-setup.ts"], 10 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/akita/9997049301febe22b4e9cdd2a3637bada2356aaf/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "ES2020", 12 | "module": "esnext", 13 | "lib": ["ES2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "strictPropertyInitialization": false, 17 | "strictNullChecks": false, 18 | "strict": false, 19 | "noImplicitAny": false, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@datorama/akita": ["packages/akita/src/index.ts"], 23 | "@datorama/akita-ng-entity-service": ["packages/ng-entity-service/src/index.ts"], 24 | "@datorama/akita-ng-router-store": ["packages/ng-router-store/src/index.ts"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "akita": "packages/akita", 5 | "ng-playground": "packages/ng-playground", 6 | "ng-router-store": "packages/ng-router-store", 7 | "ng-entity-service": "packages/ng-entity-service" 8 | } 9 | } 10 | --------------------------------------------------------------------------------