├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── master.yml ├── .gitignore ├── .huskyrc.json ├── .lintstagedrc.json ├── .opensource └── project.json ├── .prettierrc.json ├── .versionrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── docs ├── FIREBASE_MODULAR.md └── assets │ ├── readme_actions_emit.gif │ ├── readme_debug_data.png │ └── readme_get_all_once.png ├── integrations ├── compat │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── list-once │ │ │ │ │ ├── list-once.component.html │ │ │ │ │ ├── list-once.component.scss │ │ │ │ │ ├── list-once.component.spec.ts │ │ │ │ │ └── list-once.component.ts │ │ │ │ ├── list │ │ │ │ │ ├── list.component.html │ │ │ │ │ ├── list.component.scss │ │ │ │ │ ├── list.component.spec.ts │ │ │ │ │ └── list.component.ts │ │ │ │ ├── other │ │ │ │ │ ├── other.component.html │ │ │ │ │ ├── other.component.scss │ │ │ │ │ ├── other.component.spec.ts │ │ │ │ │ └── other.component.ts │ │ │ │ └── paged-list │ │ │ │ │ ├── paged-list.component.html │ │ │ │ │ ├── paged-list.component.scss │ │ │ │ │ ├── paged-list.component.spec.ts │ │ │ │ │ └── paged-list.component.ts │ │ │ ├── models │ │ │ │ ├── attendee.ts │ │ │ │ ├── classification.ts │ │ │ │ └── race.ts │ │ │ ├── services │ │ │ │ ├── attendees.firestore.ts │ │ │ │ ├── classifications.firestore.ts │ │ │ │ ├── inject-custom-dependecies.service.spec.ts │ │ │ │ ├── inject-custom-dependecies.service.ts │ │ │ │ ├── races.firestore.spec.ts │ │ │ │ └── races.firestore.ts │ │ │ └── states │ │ │ │ ├── attendees │ │ │ │ ├── attendees.actions.ts │ │ │ │ └── attendees.state.ts │ │ │ │ ├── classifications │ │ │ │ ├── classifications.actions.ts │ │ │ │ ├── classifications.state.spec.ts │ │ │ │ └── classifications.state.ts │ │ │ │ └── races │ │ │ │ ├── races.actions.ts │ │ │ │ ├── races.state.spec.ts │ │ │ │ └── races.state.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ └── styles.scss │ └── tsconfig.app.json ├── modular │ ├── src │ │ ├── _redirects │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── list-once │ │ │ │ │ ├── list-once.component.html │ │ │ │ │ ├── list-once.component.scss │ │ │ │ │ ├── list-once.component.spec.ts │ │ │ │ │ └── list-once.component.ts │ │ │ │ ├── list │ │ │ │ │ ├── list.component.html │ │ │ │ │ ├── list.component.scss │ │ │ │ │ ├── list.component.spec.ts │ │ │ │ │ └── list.component.ts │ │ │ │ ├── other │ │ │ │ │ ├── other.component.html │ │ │ │ │ ├── other.component.scss │ │ │ │ │ ├── other.component.spec.ts │ │ │ │ │ └── other.component.ts │ │ │ │ └── paged-list │ │ │ │ │ ├── paged-list.component.html │ │ │ │ │ ├── paged-list.component.scss │ │ │ │ │ ├── paged-list.component.spec.ts │ │ │ │ │ └── paged-list.component.ts │ │ │ ├── models │ │ │ │ ├── attendee.ts │ │ │ │ ├── classification.ts │ │ │ │ └── race.ts │ │ │ ├── services │ │ │ │ ├── attendees.firestore.ts │ │ │ │ ├── classifications.firestore.ts │ │ │ │ ├── inject-custom-dependecies.service.spec.ts │ │ │ │ ├── inject-custom-dependecies.service.ts │ │ │ │ ├── races.firestore.spec.ts │ │ │ │ └── races.firestore.ts │ │ │ └── states │ │ │ │ ├── attendees │ │ │ │ ├── attendees.actions.ts │ │ │ │ └── attendees.state.ts │ │ │ │ ├── classifications │ │ │ │ ├── classifications.actions.ts │ │ │ │ ├── classifications.state.spec.ts │ │ │ │ └── classifications.state.ts │ │ │ │ └── races │ │ │ │ ├── races.actions.ts │ │ │ │ ├── races.state.spec.ts │ │ │ │ └── races.state.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── icons │ │ │ │ ├── icon-128x128.png │ │ │ │ ├── icon-144x144.png │ │ │ │ ├── icon-152x152.png │ │ │ │ ├── icon-192x192.png │ │ │ │ ├── icon-384x384.png │ │ │ │ ├── icon-512x512.png │ │ │ │ ├── icon-72x72.png │ │ │ │ └── icon-96x96.png │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── manifest.webmanifest │ │ ├── polyfills.ts │ │ └── styles.scss │ └── tsconfig.app.json └── standalone │ ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── services │ │ │ └── test.firestore.ts │ │ └── states │ │ │ ├── test.actions.ts │ │ │ ├── test.selectors.ts │ │ │ └── test.state.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.scss │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── jest.config.js ├── ngsw-config.json ├── package.json ├── packages └── firestore-plugin │ ├── compat-public-api.ts │ ├── compat │ ├── ng-package.json │ └── src │ │ ├── lib │ │ ├── ngxs-firestore-compat.adapter.ts │ │ ├── ngxs-firestore-compat.service.ts │ │ └── ngxs-firestore-page-compat.service.ts │ │ └── public-api.ts │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── action-decorator-helpers.ts │ │ ├── actions.ts │ │ ├── attach-action.ts │ │ ├── internal-types.ts │ │ ├── ngxs-firestore-connect.actions.ts │ │ ├── ngxs-firestore-connect.service.spec.ts │ │ ├── ngxs-firestore-connect.service.ts │ │ ├── ngxs-firestore-connections.selector.ts │ │ ├── ngxs-firestore-page.service.spec.ts │ │ ├── ngxs-firestore-page.service.ts │ │ ├── ngxs-firestore.adapter.ts │ │ ├── ngxs-firestore.module.spec.ts │ │ ├── ngxs-firestore.module.ts │ │ ├── ngxs-firestore.service.spec.ts │ │ ├── ngxs-firestore.service.ts │ │ ├── ngxs-firestore.standalone.ts │ │ ├── ngxs-firestore.state.spec.ts │ │ ├── ngxs-firestore.state.ts │ │ ├── tokens.ts │ │ ├── types.ts │ │ └── utils.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ └── tsconfig.lib.prod.json ├── renovate.json ├── setupJest.ts ├── tools └── copy-readme.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tsconfig.tools.json ├── yarn.lock └── yarn.lock.readme.md /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 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.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | "env": { 16 | "browser": true, 17 | "es6": true, 18 | "node": true 19 | }, 20 | "extends": [ 21 | "prettier", 22 | ], 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "project": "tsconfig.json", 26 | "sourceType": "module" 27 | }, 28 | "plugins": [ 29 | "eslint-plugin-import", 30 | "@angular-eslint/eslint-plugin", 31 | "@typescript-eslint", 32 | "@typescript-eslint/tslint" 33 | ], 34 | "rules": { 35 | "@angular-eslint/component-class-suffix": "error", 36 | "@angular-eslint/component-selector": [ 37 | "off", 38 | { 39 | "type": "element", 40 | "prefix": "my-prefix", 41 | "style": "kebab-case" 42 | } 43 | ], 44 | "@angular-eslint/directive-class-suffix": "error", 45 | "@angular-eslint/no-output-rename": "error", 46 | "@angular-eslint/use-pipe-transform-interface": "error", 47 | "@typescript-eslint/consistent-type-definitions": "error", 48 | "@typescript-eslint/dot-notation": "off", 49 | "@typescript-eslint/explicit-member-accessibility": [ 50 | "off", 51 | { 52 | "accessibility": "explicit" 53 | } 54 | ], 55 | "@typescript-eslint/indent": "error", 56 | "@typescript-eslint/member-delimiter-style": [ 57 | "error", 58 | { 59 | "multiline": { 60 | "delimiter": "semi", 61 | "requireLast": true 62 | }, 63 | "singleline": { 64 | "delimiter": "semi", 65 | "requireLast": false 66 | } 67 | } 68 | ], 69 | "@typescript-eslint/member-ordering": "error", 70 | "@typescript-eslint/naming-convention": "error", 71 | "@typescript-eslint/no-empty-function": "off", 72 | "@typescript-eslint/no-empty-interface": "error", 73 | "@typescript-eslint/no-explicit-any": "error", 74 | "@typescript-eslint/no-inferrable-types": "off", 75 | "@typescript-eslint/no-misused-new": "error", 76 | "@typescript-eslint/no-non-null-assertion": "error", 77 | "@typescript-eslint/no-shadow": [ 78 | "error", 79 | { 80 | "hoist": "all" 81 | } 82 | ], 83 | "@typescript-eslint/no-unused-expressions": "error", 84 | "@typescript-eslint/prefer-function-type": "error", 85 | "@typescript-eslint/quotes": [ 86 | "error", 87 | "single" 88 | ], 89 | "@typescript-eslint/semi": [ 90 | "error", 91 | "always" 92 | ], 93 | "@typescript-eslint/tslint/config": [ 94 | "error", 95 | { 96 | "rules": { 97 | "import-spacing": true, 98 | "use-life-cycle-interface": true, 99 | "whitespace": true 100 | } 101 | } 102 | ], 103 | "@typescript-eslint/type-annotation-spacing": "error", 104 | "@typescript-eslint/unified-signatures": "error", 105 | "arrow-body-style": "error", 106 | "brace-style": [ 107 | "error", 108 | "1tbs" 109 | ], 110 | "constructor-super": "error", 111 | "curly": "error", 112 | "dot-notation": "off", 113 | "eol-last": "error", 114 | "eqeqeq": [ 115 | "error", 116 | "smart" 117 | ], 118 | "guard-for-in": "error", 119 | "id-denylist": "off", 120 | "id-match": "off", 121 | "import/no-deprecated": "warn", 122 | "indent": "error", 123 | "max-len": [ 124 | "error", 125 | { 126 | "code": 140 127 | } 128 | ], 129 | "no-bitwise": "error", 130 | "no-caller": "error", 131 | "no-console": [ 132 | "error", 133 | { 134 | "allow": [ 135 | "log", 136 | "warn", 137 | "dir", 138 | "timeLog", 139 | "assert", 140 | "clear", 141 | "count", 142 | "countReset", 143 | "group", 144 | "groupEnd", 145 | "table", 146 | "dirxml", 147 | "error", 148 | "groupCollapsed", 149 | "Console", 150 | "profile", 151 | "profileEnd", 152 | "timeStamp", 153 | "context" 154 | ] 155 | } 156 | ], 157 | "no-debugger": "error", 158 | "no-empty": "off", 159 | "no-empty-function": "off", 160 | "no-eval": "error", 161 | "no-fallthrough": "error", 162 | "no-new-wrappers": "error", 163 | "no-redeclare": "error", 164 | "no-restricted-imports": [ 165 | "error", 166 | "rxjs/Rx" 167 | ], 168 | "no-shadow": "error", 169 | "no-throw-literal": "error", 170 | "no-trailing-spaces": "error", 171 | "no-undef-init": "off", 172 | "no-underscore-dangle": "off", 173 | "no-unused-expressions": "error", 174 | "no-unused-labels": "error", 175 | "no-var": "error", 176 | "prefer-const": "error", 177 | "quotes": "error", 178 | "radix": "error", 179 | "semi": "error", 180 | "spaced-comment": [ 181 | "error", 182 | "always", 183 | { 184 | "markers": [ 185 | "/" 186 | ] 187 | } 188 | ] 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | on: push 3 | 4 | jobs: 5 | build-and-test: 6 | runs-on: ubuntu-latest 7 | name: Build and test 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '18.x' 13 | - name: Get yarn cache directory path 14 | id: yarn-cache-dir-path 15 | run: echo "::set-output name=dir::$(yarn cache dir)" 16 | - uses: actions/cache@v3 17 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 18 | with: 19 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 20 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-yarn- 23 | - run: yarn install 24 | - run: yarn lint 25 | - run: yarn format:check 26 | - run: yarn test:ci 27 | - run: yarn build 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@v1 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | dist 3 | dist-integration 4 | dist-integration-server 5 | node_modules 6 | .idea 7 | .vscode 8 | coverage 9 | yarn-error.log 10 | .cache 11 | package-lock.json 12 | *.iml 13 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": ["prettier --write", "git add"], 3 | "*.html": ["prettier --write", "git add"], 4 | "*.scss": ["prettier --write", "git add"], 5 | "*.json": ["prettier --write", "git add"], 6 | "*.md": ["prettier --write", "git add"] 7 | } 8 | -------------------------------------------------------------------------------- /.opensource/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NGXS Firestore Plugin", 3 | "platforms": ["Web"], 4 | "content": "README.md", 5 | "pages": { 6 | "README.md": "Read me" 7 | }, 8 | "related": [], 9 | "tabs": [{ "title": "NPM", "href": "https://www.npmjs.com/package/@ngxs-labs/firestore-plugin" }] 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "htmlWhitespaceSensitivity": "css", 9 | "jsxBracketSameLine": true, 10 | "bracketSpacing": true, 11 | "arrowParens": "always", 12 | "proseWrap": "always" 13 | } 14 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageFiles": [ 3 | { 4 | "filename": "packages/firestore-plugin/package.json", 5 | "type": "json" 6 | } 7 | ], 8 | "bumpFiles": [ 9 | { 10 | "filename": "packages/firestore-plugin/package.json", 11 | "type": "json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "integrations", 5 | "projects": { 6 | "ngxs-firestore": { 7 | "root": "packages/firestore-plugin", 8 | "sourceRoot": "packages/firestore-plugin/src", 9 | "projectType": "library", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "packages/firestore-plugin/tsconfig.lib.json", 16 | "project": "packages/firestore-plugin/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "packages/firestore-plugin/tsconfig.lib.prod.json" 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | "compat": { 27 | "projectType": "application", 28 | "schematics": { 29 | "@schematics/angular:component": { 30 | "style": "scss" 31 | }, 32 | "@schematics/angular:application": { 33 | "strict": true 34 | } 35 | }, 36 | "root": "integrations/compat", 37 | "sourceRoot": "integrations/compat/src", 38 | "prefix": "app", 39 | "architect": { 40 | "build": { 41 | "builder": "@angular-devkit/build-angular:application", 42 | "options": { 43 | "allowedCommonJsDependencies": ["chance"], 44 | "outputPath": { 45 | "base": "dist/integrations/compat" 46 | }, 47 | "index": "integrations/compat/src/index.html", 48 | "polyfills": ["integrations/compat/src/polyfills.ts"], 49 | "tsConfig": "integrations/compat/tsconfig.app.json", 50 | "inlineStyleLanguage": "scss", 51 | "assets": ["integrations/compat/src/favicon.ico", "integrations/compat/src/assets"], 52 | "styles": ["integrations/compat/src/styles.scss"], 53 | "scripts": [], 54 | "browser": "integrations/compat/src/main.ts" 55 | }, 56 | "configurations": { 57 | "production": { 58 | "budgets": [ 59 | { 60 | "type": "initial", 61 | "maximumWarning": "500kb", 62 | "maximumError": "2mb" 63 | }, 64 | { 65 | "type": "anyComponentStyle", 66 | "maximumWarning": "2kb", 67 | "maximumError": "4kb" 68 | } 69 | ], 70 | "fileReplacements": [ 71 | { 72 | "replace": "integrations/compat/src/environments/environment.ts", 73 | "with": "integrations/compat/src/environments/environment.prod.ts" 74 | } 75 | ], 76 | "outputHashing": "all" 77 | }, 78 | "development": { 79 | "optimization": false, 80 | "extractLicenses": false, 81 | "sourceMap": true, 82 | "namedChunks": true 83 | } 84 | }, 85 | "defaultConfiguration": "production" 86 | }, 87 | "serve": { 88 | "builder": "@angular-devkit/build-angular:dev-server", 89 | "configurations": { 90 | "production": { 91 | "buildTarget": "compat:build:production" 92 | }, 93 | "development": { 94 | "buildTarget": "compat:build:development" 95 | } 96 | }, 97 | "defaultConfiguration": "development" 98 | } 99 | } 100 | }, 101 | "modular": { 102 | "projectType": "application", 103 | "schematics": { 104 | "@schematics/angular:component": { 105 | "style": "scss" 106 | }, 107 | "@schematics/angular:application": { 108 | "strict": true 109 | } 110 | }, 111 | "root": "integrations/modular", 112 | "sourceRoot": "integrations/modular/src", 113 | "prefix": "app", 114 | "architect": { 115 | "build": { 116 | "builder": "@angular-devkit/build-angular:application", 117 | "options": { 118 | "outputPath": { 119 | "base": "dist/modular" 120 | }, 121 | "allowedCommonJsDependencies": ["chance"], 122 | "index": "integrations/modular/src/index.html", 123 | "polyfills": ["integrations/modular/src/polyfills.ts"], 124 | "tsConfig": "integrations/modular/tsconfig.app.json", 125 | "inlineStyleLanguage": "scss", 126 | "assets": [ 127 | "integrations/modular/src/favicon.ico", 128 | "integrations/modular/src/assets", 129 | "integrations/modular/src/_redirects", 130 | "integrations/modular/src/manifest.webmanifest" 131 | ], 132 | "styles": ["integrations/modular/src/styles.scss"], 133 | "scripts": [], 134 | "browser": "integrations/modular/src/main.ts" 135 | }, 136 | "configurations": { 137 | "production": { 138 | "budgets": [ 139 | { 140 | "type": "initial", 141 | "maximumWarning": "500kb", 142 | "maximumError": "2mb" 143 | }, 144 | { 145 | "type": "anyComponentStyle", 146 | "maximumWarning": "2kb", 147 | "maximumError": "4kb" 148 | } 149 | ], 150 | "fileReplacements": [ 151 | { 152 | "replace": "integrations/modular/src/environments/environment.ts", 153 | "with": "integrations/modular/src/environments/environment.prod.ts" 154 | } 155 | ], 156 | "outputHashing": "all" 157 | }, 158 | "development": { 159 | "optimization": false, 160 | "extractLicenses": false, 161 | "sourceMap": true, 162 | "namedChunks": true 163 | } 164 | }, 165 | "defaultConfiguration": "production" 166 | }, 167 | "serve": { 168 | "builder": "@angular-devkit/build-angular:dev-server", 169 | "configurations": { 170 | "production": { 171 | "buildTarget": "modular:build:production" 172 | }, 173 | "development": { 174 | "buildTarget": "modular:build:development" 175 | } 176 | }, 177 | "defaultConfiguration": "development" 178 | }, 179 | "extract-i18n": { 180 | "builder": "@angular-devkit/build-angular:extract-i18n", 181 | "options": { 182 | "buildTarget": "modular:build" 183 | } 184 | } 185 | } 186 | }, 187 | "standalone": { 188 | "projectType": "application", 189 | "schematics": { 190 | "@schematics/angular:component": { 191 | "style": "scss" 192 | } 193 | }, 194 | "root": "integrations/standalone", 195 | "sourceRoot": "integrations/standalone/src", 196 | "prefix": "app", 197 | "architect": { 198 | "build": { 199 | "builder": "@angular-devkit/build-angular:application", 200 | "options": { 201 | "outputPath": "dist/standalone", 202 | "index": "integrations/standalone/src/index.html", 203 | "browser": "integrations/standalone/src/main.ts", 204 | "polyfills": ["zone.js"], 205 | "tsConfig": "integrations/standalone/tsconfig.app.json", 206 | "inlineStyleLanguage": "scss", 207 | "assets": ["integrations/standalone/src/favicon.ico", "integrations/standalone/src/assets"], 208 | "styles": ["integrations/standalone/src/styles.scss"], 209 | "scripts": [] 210 | }, 211 | "configurations": { 212 | "production": { 213 | "budgets": [ 214 | { 215 | "type": "initial", 216 | "maximumWarning": "500kb", 217 | "maximumError": "2mb" 218 | }, 219 | { 220 | "type": "anyComponentStyle", 221 | "maximumWarning": "2kb", 222 | "maximumError": "4kb" 223 | } 224 | ], 225 | "outputHashing": "all" 226 | }, 227 | "development": { 228 | "optimization": false, 229 | "extractLicenses": false, 230 | "sourceMap": true 231 | } 232 | }, 233 | "defaultConfiguration": "production" 234 | }, 235 | "serve": { 236 | "builder": "@angular-devkit/build-angular:dev-server", 237 | "configurations": { 238 | "production": { 239 | "buildTarget": "standalone:build:production" 240 | }, 241 | "development": { 242 | "buildTarget": "standalone:build:development" 243 | } 244 | }, 245 | "defaultConfiguration": "development" 246 | }, 247 | "extract-i18n": { 248 | "builder": "@angular-devkit/build-angular:extract-i18n", 249 | "options": { 250 | "buildTarget": "standalone:build" 251 | } 252 | }, 253 | "test": { 254 | "builder": "@angular-devkit/build-angular:karma", 255 | "options": { 256 | "polyfills": ["zone.js", "zone.js/testing"], 257 | "tsConfig": "integrations/standalone/tsconfig.spec.json", 258 | "inlineStyleLanguage": "scss", 259 | "assets": ["integrations/standalone/src/favicon.ico", "integrations/standalone/src/assets"], 260 | "styles": ["integrations/standalone/src/styles.scss"], 261 | "scripts": [] 262 | } 263 | } 264 | } 265 | } 266 | }, 267 | "cli": { 268 | "analytics": false 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /docs/FIREBASE_MODULAR.md: -------------------------------------------------------------------------------- 1 | ## Firebase Modular 2 | 3 | With Firebase modular version, there are some changes you'll need to make in your `AppModule` import as well as you 4 | `@State` 5 | 6 | ### Provide Firestore and importing Module 7 | 8 | ```ts 9 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 10 | import { getFirestore, provideFirestore } from '@angular/fire/firestore'; 11 | import { NgxsModule } from '@ngxs/store'; 12 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 13 | 14 | @NgModule({ 15 | //... 16 | imports: [ 17 | //... 18 | provideFirebaseApp(() => initializeApp(environment.firebase)), 19 | provideFirestore(() => getFirestore()), 20 | NgxsModule.forRoot(...), 21 | NgxsFirestoreModule.forRoot(), 22 | ], 23 | //... 24 | }) 25 | export class AppModule {} 26 | ``` 27 | 28 | ### State 29 | 30 | ```ts 31 | import { query, where } from '@angular/fire/firestore'; 32 | //... 33 | 34 | export class RacesState implements NgxsOnInit { 35 | //... 36 | constructor(private racesFS: RacesFirestore, private ngxsFirestoreConnect: NgxsFirestoreConnect) {} 37 | 38 | ngxsOnInit() { 39 | // when querying syntax is different 40 | this.ngxsFirestoreConnect.connect(RacesActions.GetAll, { 41 | to: () => this.racesFS.collection$((ref) => query(ref, where('name', '==', 'tour de france'))) 42 | }); 43 | } 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/assets/readme_actions_emit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/docs/assets/readme_actions_emit.gif -------------------------------------------------------------------------------- /docs/assets/readme_debug_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/docs/assets/readme_debug_data.png -------------------------------------------------------------------------------- /docs/assets/readme_get_all_once.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/docs/assets/readme_get_all_once.png -------------------------------------------------------------------------------- /integrations/compat/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
24 |

NGXS Firestore:

25 |
{{ ngxsFirestoreState | json }}
26 |
27 | -------------------------------------------------------------------------------- /integrations/compat/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/app/app.component.scss -------------------------------------------------------------------------------- /integrations/compat/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { ngxsFirestoreConnections } from '@ngxs-labs/firestore-plugin'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | public ngxsFirestoreState$ = this.store.select(ngxsFirestoreConnections); 12 | 13 | constructor(private store: Store) {} 14 | } 15 | -------------------------------------------------------------------------------- /integrations/compat/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '../environments/environment'; 2 | 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NgModule } from '@angular/core'; 5 | import { RouterModule } from '@angular/router'; 6 | import { FormsModule } from '@angular/forms'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import { ListComponent } from './components/list/list.component'; 10 | 11 | import { AngularFireModule } from '@angular/fire/compat'; 12 | import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; 13 | 14 | import { NgxsModule } from '@ngxs/store'; 15 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 16 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 17 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 18 | 19 | import { RacesState } from './states/races/races.state'; 20 | import { OtherComponent } from './components/other/other.component'; 21 | import { NgxsActionsExecutingModule } from '@ngxs-labs/actions-executing'; 22 | import { ClassificationsState } from './states/classifications/classifications.state'; 23 | import { PagedListComponent } from './components/paged-list/paged-list.component'; 24 | import { AttendeesState } from './states/attendees/attendees.state'; 25 | import { ListOnceComponent } from './components/list-once/list-once.component'; 26 | 27 | @NgModule({ 28 | declarations: [AppComponent, ListComponent, OtherComponent, PagedListComponent, ListOnceComponent], 29 | imports: [ 30 | BrowserModule, 31 | FormsModule, 32 | RouterModule.forRoot([ 33 | { path: 'list', component: ListComponent }, 34 | { path: 'list-once', component: ListOnceComponent }, 35 | { path: 'paged-list', component: PagedListComponent }, 36 | { path: 'other', component: OtherComponent }, 37 | { path: '', redirectTo: '/list', pathMatch: 'full' } 38 | ]), 39 | AngularFireModule.initializeApp(environment.firebase), 40 | AngularFirestoreModule, 41 | NgxsModule.forRoot([RacesState, ClassificationsState, AttendeesState], { 42 | developmentMode: !environment.production 43 | }), 44 | NgxsLoggerPluginModule.forRoot({ 45 | disabled: environment.production 46 | }), 47 | NgxsFirestoreModule.forRoot({ 48 | timeoutWriteOperations: 1000 49 | }), 50 | NgxsActionsExecutingModule.forRoot(), 51 | NgxsReduxDevtoolsPluginModule.forRoot({ 52 | name: 'Ngxs Firestore', 53 | disabled: environment.production, 54 | actionSanitizer: (action) => ({ ...action, action: null }) 55 | }) 56 | ], 57 | providers: [], 58 | bootstrap: [AppComponent] 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list-once/list-once.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 |
8 |
9 |
{{ race.id }}
10 |

{{ race.order }}

11 |

{{ race.name }}

12 |

{{ race.description }}

13 | 14 |
{{ race | json }}
15 |
16 |
17 |
18 |
19 |
20 | Loading... 21 |
22 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list-once/list-once.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/app/components/list-once/list-once.component.scss -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list-once/list-once.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListOnceComponent } from './list-once.component'; 4 | import { Store, NgxsModule } from '@ngxs/store'; 5 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 6 | import { Subject } from 'rxjs'; 7 | 8 | describe('ListOnceComponent', () => { 9 | let component: ListOnceComponent; 10 | let fixture: ComponentFixture; 11 | let storeMock; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | storeMock = jest.fn().mockImplementation(() => ({ 16 | select: jest.fn().mockReturnValue(new Subject()), 17 | dispatch: jest.fn().mockReturnValue(new Subject()) 18 | })); 19 | 20 | TestBed.configureTestingModule({ 21 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 22 | declarations: [ListOnceComponent], 23 | providers: [{ provide: Store, useValue: storeMock() }] 24 | }).compileComponents(); 25 | }) 26 | ); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ListOnceComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list-once/list-once.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { RacesActions } from '../../states/races/races.actions'; 4 | import { RacesState } from '../../states/races/races.state'; 5 | import { map } from 'rxjs/operators'; 6 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 7 | 8 | @Component({ 9 | selector: 'app-list-once', 10 | templateUrl: './list-once.component.html', 11 | styleUrls: ['./list-once.component.scss'] 12 | }) 13 | export class ListOnceComponent implements OnInit, OnDestroy { 14 | races$ = this.store.select(RacesState.races); 15 | loading$ = this.store.select(actionsExecuting([RacesActions.GetAll, RacesActions.Get])); 16 | loaded$ = this.loading$.pipe(map((loading) => !loading)); 17 | 18 | constructor(private store: Store) {} 19 | 20 | ngOnInit() {} 21 | 22 | getAll() { 23 | this.store.dispatch(new RacesActions.GetAllOnce()); 24 | } 25 | 26 | get() { 27 | this.store.dispatch(new RacesActions.GetOnce('Pn7RYUL0]1!*JDqWw)S5')); 28 | } 29 | 30 | ngOnDestroy() {} 31 | } 32 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 |
18 |
Total: {{ total$ | async }}
19 |
20 | 21 |
22 |
23 |
24 |
{{ race.id }}
25 |

{{ race.order }}

26 |

{{ race.name }}

27 |

{{ race.description }}

28 | 29 |
{{ race | json }}
30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 |
39 |
44 |
45 |
{{ classification.id }}
46 |

{{ classification.order }}

47 |

{{ classification.name }}

48 | 49 |
{{ classification | json }}
50 |
51 |
52 |
53 |
54 |
55 | Loading... 56 |
57 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list/list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/app/components/list/list.component.scss -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListComponent } from './list.component'; 4 | import { Store, NgxsModule } from '@ngxs/store'; 5 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 6 | import { Subject } from 'rxjs'; 7 | 8 | describe('ListComponent', () => { 9 | let component: ListComponent; 10 | let fixture: ComponentFixture; 11 | let storeMock; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | storeMock = jest.fn().mockImplementation(() => ({ 16 | select: jest.fn().mockReturnValue(new Subject()), 17 | dispatch: jest.fn().mockReturnValue(new Subject()) 18 | })); 19 | 20 | TestBed.configureTestingModule({ 21 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 22 | declarations: [ListComponent], 23 | providers: [{ provide: Store, useValue: storeMock() }] 24 | }).compileComponents(); 25 | }) 26 | ); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ListComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { RacesActions } from '../../states/races/races.actions'; 4 | import { RacesState } from '../../states/races/races.state'; 5 | import { ClassificationsState } from '../../states/classifications/classifications.state'; 6 | import { ClassificationsActions } from '../../states/classifications/classifications.actions'; 7 | import { Race } from '../../models/race'; 8 | import { Chance } from 'chance'; 9 | import { map } from 'rxjs/operators'; 10 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 11 | import { Disconnect } from '@ngxs-labs/firestore-plugin'; 12 | 13 | @Component({ 14 | selector: 'app-list', 15 | templateUrl: './list.component.html', 16 | styleUrls: ['./list.component.scss'] 17 | }) 18 | export class ListComponent implements OnInit, OnDestroy { 19 | races$ = this.store.select(RacesState.races); 20 | classifications$ = this.store.select(ClassificationsState.classifications); 21 | total$ = this.races$.pipe(map((races) => races.length)); 22 | 23 | gettingAll$ = this.store.select(actionsExecuting([RacesActions.GetAll])); 24 | gettingSingle$ = this.store.select(actionsExecuting([RacesActions.Get])); 25 | creating$ = this.store.select(actionsExecuting([RacesActions.Create])); 26 | loading$ = this.store.select(actionsExecuting([RacesActions.GetAll, RacesActions.Get])); 27 | loaded$ = this.loading$.pipe(map((loading) => !loading)); 28 | disconnecting$ = this.store.select(actionsExecuting([Disconnect])); 29 | throwingError$ = this.store.select(actionsExecuting([RacesActions.Error])); 30 | gettingSubCollection$ = this.store.select(actionsExecuting([ClassificationsActions.GetAll])); 31 | 32 | constructor(private store: Store) {} 33 | 34 | ngOnInit() { 35 | // this.store.dispatch(new RacesActions.GetAll()); 36 | // this.store.dispatch(new RacesActions.Get('8iI)0md[dTAFC[wo!&[N')); 37 | // this.store.dispatch(new RacesActions.Get('AAAAAA')); 38 | } 39 | 40 | disconnect() { 41 | this.store.dispatch(new Disconnect(new RacesActions.Get(']cfct5iL8(H)@Sl#xTcS'))); 42 | } 43 | 44 | reconnect() { 45 | // this.store.dispatch(new RacesActions.GetAll()); 46 | // this.store.dispatch(new RacesActions.Get('8iI)0md[dTAFC[wo!&[N')); 47 | 48 | this.store.dispatch(new RacesActions.Get(']cfct5iL8(H)@Sl#xTcS')); 49 | } 50 | 51 | getAll() { 52 | this.store.dispatch(new RacesActions.GetAll()); 53 | } 54 | 55 | getSubCollection() { 56 | this.store.dispatch(new ClassificationsActions.GetAll('0NN6x6GKDGumGU5dtnk4')); 57 | } 58 | 59 | get() { 60 | // const ids = ['4(CPo6Fy(7Mo^YklK[Q8', 'FouQf@q4FHJcc&%cnmkT', 'LBWH5KvYp43ia)!IYpwv', ']cfct5iL8(H)@Sl#xTcS']; 61 | // for (let index = 0; index < ids.length; index++) { 62 | // setTimeout(() => this.store.dispatch(new RacesActions.Get(ids[index])), 1000 * index); 63 | // } 64 | 65 | this.store.dispatch(new RacesActions.Get('0V!^fMrWetbs68]ob6%M')); 66 | } 67 | 68 | create() { 69 | const chance = new Chance(); 70 | const race: Partial = {}; 71 | // race.id = chance.string({ length: 20 }); 72 | race.raceId = chance.string({ length: 20 }); 73 | race.name = chance.word(); 74 | race.title = chance.word(); 75 | race.description = chance.sentence(); 76 | this.store.dispatch(new RacesActions.Create(race)); 77 | 78 | // this.store.dispatch(new RacesActions.Create({ 79 | // id: 'test-id', 80 | // name: 'Test', 81 | // title: 'Test Title', 82 | // description: 'Test description', 83 | // })); 84 | } 85 | 86 | update(race: Race) { 87 | const chance = new Chance(); 88 | 89 | this.store.dispatch( 90 | new RacesActions.Update({ 91 | ...race, 92 | name: chance.string(), 93 | description: chance.word() 94 | }) 95 | ); 96 | } 97 | 98 | updateIfExists(race: Race) { 99 | const chance = new Chance(); 100 | 101 | this.store.dispatch( 102 | new RacesActions.UpdateIfExists({ 103 | ...race, 104 | name: chance.string(), 105 | description: chance.word() 106 | }) 107 | ); 108 | } 109 | 110 | delete(id: string) { 111 | this.store.dispatch(new RacesActions.Delete(id)); 112 | } 113 | 114 | ngOnDestroy() { 115 | this.store.dispatch(new Disconnect(RacesActions.GetAll)); 116 | } 117 | 118 | throwError() { 119 | this.store.dispatch(new RacesActions.Error()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/other/other.component.html: -------------------------------------------------------------------------------- 1 | Disconnected on page OnDestroy 2 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/other/other.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/app/components/other/other.component.scss -------------------------------------------------------------------------------- /integrations/compat/src/app/components/other/other.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { OtherComponent } from './other.component'; 3 | 4 | describe('OtherComponent', () => { 5 | let component: OtherComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach( 9 | waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [OtherComponent] 12 | }).compileComponents(); 13 | }) 14 | ); 15 | 16 | beforeEach(() => {}); 17 | 18 | it('should create', () => { 19 | fixture = TestBed.createComponent(OtherComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/other/other.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-other', 5 | templateUrl: './other.component.html', 6 | styleUrls: ['./other.component.scss'] 7 | }) 8 | export class OtherComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/paged-list/paged-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 |
8 |
{{ race.id }}
9 |

{{ race.order }}

10 |

{{ race.name }}

11 |

{{ race.description }}

12 |
13 |
14 |
15 | 16 |
17 | 20 | 21 | 24 |
25 |
26 |
27 |
28 |
{{ attendee.id }}
29 |

{{ attendee.order }}

30 |

{{ attendee.name }}

31 |

{{ attendee.description }}

32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/paged-list/paged-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/app/components/paged-list/paged-list.component.scss -------------------------------------------------------------------------------- /integrations/compat/src/app/components/paged-list/paged-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | 5 | import { PagedListComponent } from './paged-list.component'; 6 | 7 | describe('PagedListComponent', () => { 8 | let component: PagedListComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 14 | declarations: [PagedListComponent] 15 | }).compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(PagedListComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /integrations/compat/src/app/components/paged-list/paged-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 3 | import { GetLastPage, GetNextPage } from '@ngxs-labs/firestore-plugin'; 4 | import { Store } from '@ngxs/store'; 5 | import { AttendeesActions } from '../../states/attendees/attendees.actions'; 6 | import { AttendeesState } from '../../states/attendees/attendees.state'; 7 | import { RacesActions } from '../../states/races/races.actions'; 8 | import { RacesState } from '../../states/races/races.state'; 9 | 10 | @Component({ 11 | selector: 'app-paged-list', 12 | templateUrl: './paged-list.component.html', 13 | styleUrls: ['./paged-list.component.scss'] 14 | }) 15 | export class PagedListComponent implements OnInit { 16 | races$ = this.store.select(RacesState.races); 17 | attendees$ = this.store.select(AttendeesState.attendees); 18 | nextPageExecuting$ = this.store.select(actionsExecuting([GetNextPage])); 19 | lastPageExecuting$ = this.store.select(actionsExecuting([GetLastPage])); 20 | 21 | constructor(private store: Store) {} 22 | 23 | ngOnInit() { 24 | this.store.dispatch(new RacesActions.GetPages()); 25 | this.store.dispatch(new AttendeesActions.GetPages()); 26 | } 27 | 28 | nextPage() { 29 | const pageId = this.store.selectSnapshot(RacesState.pageId); 30 | this.store.dispatch(new GetNextPage(pageId)); 31 | } 32 | 33 | lastPage() { 34 | const pageId = this.store.selectSnapshot(RacesState.pageId); 35 | this.store.dispatch(new GetLastPage(pageId)); 36 | } 37 | 38 | nextPageAttendees() { 39 | const pageId = this.store.selectSnapshot(AttendeesState.pageId); 40 | this.store.dispatch(new GetNextPage(pageId)); 41 | } 42 | 43 | lastPageAttendees() { 44 | const pageId = this.store.selectSnapshot(AttendeesState.pageId); 45 | this.store.dispatch(new GetLastPage(pageId)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /integrations/compat/src/app/models/attendee.ts: -------------------------------------------------------------------------------- 1 | export interface Attendee { 2 | id: string; 3 | order: string; 4 | name: string; 5 | description: string; 6 | } 7 | -------------------------------------------------------------------------------- /integrations/compat/src/app/models/classification.ts: -------------------------------------------------------------------------------- 1 | export interface Classification { 2 | id: string; 3 | name: string; 4 | order: string; 5 | } 6 | -------------------------------------------------------------------------------- /integrations/compat/src/app/models/race.ts: -------------------------------------------------------------------------------- 1 | export interface Race { 2 | id: string; 3 | raceId: string; 4 | title: string; 5 | description: string; 6 | name: string; 7 | order: string; 8 | 9 | readonly testProp: string; 10 | } 11 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/attendees.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin/compat'; 3 | import { Attendee } from '../models/attendee'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AttendeesFirestore extends NgxsFirestore { 9 | protected path = 'attendees'; 10 | } 11 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/classifications.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin/compat'; 3 | import { Race } from '../models/race'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ClassificationsFirestore extends NgxsFirestore { 9 | idField = 'classificationId'; 10 | private _raceId = ''; 11 | protected get path() { 12 | return `races/${this.raceId}/classifications`; 13 | } 14 | 15 | public setRaceId(raceId: string) { 16 | this._raceId = raceId; 17 | } 18 | 19 | protected get raceId() { 20 | return this._raceId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/inject-custom-dependecies.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AngularFirestore } from '@angular/fire/compat/firestore'; 3 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 4 | import { NgxsModule } from '@ngxs/store'; 5 | import { CustomDependency, InjectCustomDependenciesService } from './inject-custom-dependecies.service'; 6 | 7 | describe('InjectCustomDependenciesService', () => { 8 | beforeEach(() => 9 | TestBed.configureTestingModule({ 10 | imports: [NgxsModule.forRoot([]), NgxsFirestoreModule.forRoot()], 11 | providers: [ 12 | { provide: CustomDependency, useClass: CustomDependency }, 13 | { provide: AngularFirestore, useValue: jest.fn() } 14 | ] 15 | }) 16 | ); 17 | 18 | it('should be created', () => { 19 | const service: InjectCustomDependenciesService = TestBed.get(InjectCustomDependenciesService); 20 | expect(service).toBeTruthy(); 21 | }); 22 | 23 | it('should inject a custom dep', () => { 24 | const service: InjectCustomDependenciesService = TestBed.get(InjectCustomDependenciesService); 25 | expect(service.customeDependency.works()).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/inject-custom-dependecies.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { NgxsFirestore, NgxsFirestoreAdapter } from '@ngxs-labs/firestore-plugin/compat'; 3 | 4 | @Injectable() 5 | export class CustomDependency { 6 | works() { 7 | return true; 8 | } 9 | } 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class InjectCustomDependenciesService extends NgxsFirestore { 15 | protected path = 'races'; 16 | 17 | constructor(@Inject(NgxsFirestoreAdapter) adapter: NgxsFirestoreAdapter, public customeDependency: CustomDependency) { 18 | super(adapter); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/races.firestore.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RacesFirestore } from './races.firestore'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 5 | import { AngularFirestore } from '@angular/fire/compat/firestore'; 6 | 7 | describe('RacesFirestore', () => { 8 | beforeEach( 9 | waitForAsync(() => 10 | TestBed.configureTestingModule({ 11 | imports: [NgxsModule.forRoot([]), NgxsFirestoreModule.forRoot()], 12 | providers: [{ provide: AngularFirestore, useValue: jest.fn() }] 13 | }) 14 | ) 15 | ); 16 | 17 | it('should be created', () => { 18 | const service: RacesFirestore = TestBed.inject(RacesFirestore); 19 | expect(service).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /integrations/compat/src/app/services/races.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin/compat'; 3 | import { Race } from '../models/race'; 4 | import firebase from 'firebase/compat/app'; 5 | import 'firebase/compat/firestore'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class RacesFirestore extends NgxsFirestore { 11 | protected path = 'races'; 12 | idField = 'raceId'; 13 | metadataField = '_metadata'; 14 | 15 | converter: firebase.firestore.FirestoreDataConverter = { 16 | toFirestore: (value) => { 17 | const db = { ...value }; 18 | delete db.testProp; 19 | return db; 20 | }, 21 | fromFirestore: (snapshot, options) => { 22 | const data = snapshot.data(options); 23 | 24 | return { ...data, testProp: data['id'] + data['title'] }; 25 | } 26 | }; 27 | 28 | updateIfExists(id: string, data: Race) { 29 | return this.adapter.firestore 30 | .doc(this.adapter.firestore.doc(`${this.path}/${id}`).ref.withConverter(this.converter as any)) 31 | .update(data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/attendees/attendees.actions.ts: -------------------------------------------------------------------------------- 1 | export namespace AttendeesActions { 2 | export class GetPages { 3 | public static readonly type = '[Attendees] GetPages'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/attendees/attendees.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { AttendeesActions } from './attendees.actions'; 3 | import { NgxsFirestoreConnect, Emitted, StreamEmitted } from '@ngxs-labs/firestore-plugin'; 4 | import { NgxsFirestorePageService } from '@ngxs-labs/firestore-plugin/compat'; 5 | import { patch } from '@ngxs/store/operators'; 6 | import { Injectable } from '@angular/core'; 7 | import { Attendee } from '../../models/attendee'; 8 | import { AttendeesFirestore } from '../../services/attendees.firestore'; 9 | 10 | export interface AttendeesStateModel { 11 | attendees: Attendee[]; 12 | pageId: string; 13 | } 14 | 15 | @State({ 16 | name: 'attendees', 17 | defaults: { 18 | attendees: [], 19 | pageId: '' 20 | } 21 | }) 22 | @Injectable() 23 | export class AttendeesState implements NgxsOnInit { 24 | @Selector() static attendees(state: AttendeesStateModel) { 25 | return state.attendees; 26 | } 27 | 28 | @Selector() static pageId(state: AttendeesStateModel) { 29 | return state.pageId; 30 | } 31 | 32 | constructor( 33 | private attendeesFS: AttendeesFirestore, 34 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 35 | private nxgsFirestorePage: NgxsFirestorePageService 36 | ) {} 37 | 38 | ngxsOnInit(ctx: StateContext) { 39 | this.ngxsFirestoreConnect.connect(AttendeesActions.GetPages, { 40 | to: () => { 41 | const obs$ = this.nxgsFirestorePage.create( 42 | (pageFn) => 43 | this.attendeesFS.collection$((ref) => { 44 | return pageFn(ref); 45 | }), 46 | 5, 47 | [{ fieldPath: 'id' }] 48 | ); 49 | 50 | return obs$; 51 | } 52 | }); 53 | } 54 | 55 | @Action(StreamEmitted(AttendeesActions.GetPages)) 56 | getPageEmitted( 57 | ctx: StateContext, 58 | { action, payload }: Emitted 59 | ) { 60 | ctx.setState(patch({ attendees: payload.results || [], pageId: payload.pageId })); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/classifications/classifications.actions.ts: -------------------------------------------------------------------------------- 1 | export namespace ClassificationsActions { 2 | export class Get { 3 | public static readonly type = '[Classifications] Get'; 4 | constructor(public payload: string) {} 5 | } 6 | 7 | export class GetAll { 8 | public static readonly type = '[Classifications] GetAll'; 9 | constructor(public raceId: string) {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/classifications/classifications.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { NgxsModule, Store } from '@ngxs/store'; 3 | import { ClassificationsState } from './classifications.state'; 4 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 5 | import { BehaviorSubject } from 'rxjs'; 6 | import { ClassificationsActions } from './classifications.actions'; 7 | import { ClassificationsFirestore } from '../../services/classifications.firestore'; 8 | import { Classification } from '../../models/classification'; 9 | 10 | describe('Classifications State', () => { 11 | let store: Store; 12 | let mockClassificationsFS; 13 | let mockCollection$: jest.Mock; 14 | 15 | beforeEach( 16 | waitForAsync(() => { 17 | mockClassificationsFS = jest.fn(() => ({ 18 | collection$: mockCollection$, 19 | setRaceId: jest.fn() 20 | })); 21 | 22 | mockCollection$ = jest.fn(); 23 | 24 | TestBed.configureTestingModule({ 25 | imports: [NgxsModule.forRoot([ClassificationsState]), NgxsFirestoreModule.forRoot()], 26 | providers: [{ provide: ClassificationsFirestore, useValue: mockClassificationsFS() }] 27 | }).compileComponents(); 28 | store = TestBed.inject(Store); 29 | }) 30 | ); 31 | 32 | it('should getall classifications', () => { 33 | mockCollection$.mockReturnValue(new BehaviorSubject([{ id: 'a' }])); 34 | store.dispatch(new ClassificationsActions.GetAll('id')); 35 | expect(store.selectSnapshot(ClassificationsState.classifications)).toEqual([{ id: 'a' } as Classification]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/classifications/classifications.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { ClassificationsActions } from './classifications.actions'; 3 | import { NgxsFirestoreConnect, Emitted, StreamEmitted } from '@ngxs-labs/firestore-plugin'; 4 | import { iif, insertItem, patch, updateItem } from '@ngxs/store/operators'; 5 | import { Injectable } from '@angular/core'; 6 | import { Classification } from '../../models/classification'; 7 | import { ClassificationsFirestore } from '../../services/classifications.firestore'; 8 | 9 | export interface ClassificationsStateModel { 10 | classifications: Classification[]; 11 | } 12 | 13 | @State({ 14 | name: 'classifications', 15 | defaults: { 16 | classifications: [] 17 | } 18 | }) 19 | @Injectable() 20 | export class ClassificationsState implements NgxsOnInit { 21 | @Selector() static classifications(state: ClassificationsStateModel) { 22 | return state.classifications; 23 | } 24 | 25 | constructor( 26 | private classificationsFS: ClassificationsFirestore, 27 | private ngxsFirestoreConnect: NgxsFirestoreConnect 28 | ) {} 29 | 30 | ngxsOnInit(ctx: StateContext) { 31 | this.ngxsFirestoreConnect.connect(ClassificationsActions.GetAll, { 32 | to: ({ raceId }) => { 33 | this.classificationsFS.setRaceId(raceId); 34 | return this.classificationsFS.collection$(); 35 | } 36 | }); 37 | 38 | this.ngxsFirestoreConnect.connect(ClassificationsActions.Get, { 39 | to: ({ payload }) => this.classificationsFS.doc$(payload) 40 | }); 41 | } 42 | 43 | @Action(StreamEmitted(ClassificationsActions.Get)) 44 | getEmitted( 45 | ctx: StateContext, 46 | { payload }: Emitted 47 | ) { 48 | if (payload) { 49 | ctx.setState( 50 | patch({ 51 | classifications: iif( 52 | (classifications) => !!classifications.find((classification) => classification.id === payload.id), 53 | updateItem((classification) => classification.id === payload.id, patch(payload)), 54 | insertItem(payload) 55 | ) 56 | }) 57 | ); 58 | } 59 | } 60 | 61 | @Action(StreamEmitted(ClassificationsActions.GetAll)) 62 | getAllEmitted( 63 | ctx: StateContext, 64 | { payload }: Emitted 65 | ) { 66 | ctx.setState(patch({ classifications: payload })); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/races/races.actions.ts: -------------------------------------------------------------------------------- 1 | import { Race } from '../../models/race'; 2 | 3 | namespace RacesActionsPayloads { 4 | export type Update = Partial; 5 | export type Upsert = Partial; 6 | export type Create = Partial; 7 | } 8 | 9 | export namespace RacesActions { 10 | export class GetAllOnce { 11 | public static readonly type = '[Races] GetAllOnce'; 12 | } 13 | export class GetOnce { 14 | public static readonly type = '[Races] GetOnce'; 15 | constructor(public payload: string) {} 16 | } 17 | export class Get { 18 | public static readonly type = '[Races] Get'; 19 | constructor(public payload: string) {} 20 | } 21 | export class GetAll { 22 | public static readonly type = '[Races] GetAll'; 23 | } 24 | 25 | export class GetPages { 26 | public static readonly type = '[Races] GetPages'; 27 | } 28 | 29 | export class Create { 30 | public static readonly type = '[Races] Create'; 31 | constructor(public payload: RacesActionsPayloads.Create) {} 32 | } 33 | export class Upsert { 34 | public static readonly type = '[Races] Upsert'; 35 | constructor(public payload: RacesActionsPayloads.Upsert) {} 36 | } 37 | export class Update { 38 | public static readonly type = '[Races] Update'; 39 | constructor(public payload: RacesActionsPayloads.Update) {} 40 | } 41 | export class UpdateIfExists { 42 | public static readonly type = '[Races] UpdateIfExists'; 43 | constructor(public payload: RacesActionsPayloads.Update) {} 44 | } 45 | export class Delete { 46 | public static readonly type = '[Races] Delete'; 47 | constructor(public payload: string) {} 48 | } 49 | export class Error { 50 | public static readonly type = '[Races] Error'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/races/races.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { NgxsModule, Store } from '@ngxs/store'; 3 | import { RacesState } from './races.state'; 4 | import { RacesActions } from './races.actions'; 5 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 6 | import { NgxsFirestorePageIdService } from '@ngxs-labs/firestore-plugin/compat'; 7 | import { BehaviorSubject } from 'rxjs'; 8 | import { RacesFirestore } from '../../services/races.firestore'; 9 | import { Race } from '../../models/race'; 10 | 11 | describe('Races State', () => { 12 | let store: Store; 13 | let mockRacesFS; 14 | let mockCollection$: jest.Mock; 15 | 16 | beforeEach( 17 | waitForAsync(() => { 18 | mockRacesFS = jest.fn(() => ({ 19 | collection$: mockCollection$ 20 | })); 21 | 22 | mockCollection$ = jest.fn(); 23 | 24 | TestBed.configureTestingModule({ 25 | imports: [NgxsModule.forRoot([RacesState]), NgxsFirestoreModule.forRoot()], 26 | providers: [ 27 | { provide: RacesFirestore, useValue: mockRacesFS() }, 28 | { provide: NgxsFirestorePageIdService, useValue: { createId: jest.fn() } } 29 | ] 30 | }).compileComponents(); 31 | store = TestBed.inject(Store); 32 | }) 33 | ); 34 | 35 | it('should getall races', () => { 36 | mockCollection$.mockReturnValue(new BehaviorSubject([{ raceId: 'a' }])); 37 | store.dispatch(new RacesActions.GetAll()); 38 | expect(store.selectSnapshot(RacesState.races)).toEqual([{ raceId: 'a' } as Race]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /integrations/compat/src/app/states/races/races.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { RacesActions } from './races.actions'; 3 | import { tap } from 'rxjs/operators'; 4 | import { 5 | NgxsFirestoreConnect, 6 | Connected, 7 | Emitted, 8 | Disconnected, 9 | StreamConnected, 10 | StreamEmitted, 11 | StreamDisconnected, 12 | StreamErrored, 13 | Errored 14 | } from '@ngxs-labs/firestore-plugin'; 15 | import { NgxsFirestorePageService } from '@ngxs-labs/firestore-plugin/compat'; 16 | import { Race } from '../../models/race'; 17 | import { RacesFirestore } from '../../services/races.firestore'; 18 | import { patch, insertItem, iif, updateItem } from '@ngxs/store/operators'; 19 | import { Injectable } from '@angular/core'; 20 | 21 | export interface RacesStateModel { 22 | races: Race[]; 23 | pageId: string; 24 | activeRaces: Race[]; 25 | } 26 | 27 | @State({ 28 | name: 'races', 29 | defaults: { 30 | races: [], 31 | pageId: '', 32 | activeRaces: [] 33 | } 34 | }) 35 | @Injectable() 36 | export class RacesState implements NgxsOnInit { 37 | @Selector() static races(state: RacesStateModel) { 38 | return state.races; 39 | } 40 | @Selector() static activeRaces(state: RacesStateModel) { 41 | return state.activeRaces; 42 | } 43 | @Selector() static pageId(state: RacesStateModel) { 44 | return state.pageId; 45 | } 46 | 47 | constructor( 48 | private racesFS: RacesFirestore, 49 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 50 | private ngxsFirestorePage: NgxsFirestorePageService 51 | ) {} 52 | 53 | ngxsOnInit(_ctx: StateContext) { 54 | this.ngxsFirestoreConnect.connect(RacesActions.GetAll, { 55 | to: () => this.racesFS.collection$(), 56 | connectedActionFinishesOn: 'FirstEmit' 57 | }); 58 | 59 | this.ngxsFirestoreConnect.connect(RacesActions.Get, { 60 | to: ({ payload }) => this.racesFS.doc$(payload) 61 | }); 62 | 63 | this.ngxsFirestoreConnect.connect(RacesActions.GetPages, { 64 | to: () => { 65 | const obs$ = this.ngxsFirestorePage.create( 66 | (pageFn) => this.racesFS.collection$((ref) => pageFn(ref).where('title', '>=', 's')), 67 | 5, 68 | [{ fieldPath: 'title' }] 69 | ); 70 | 71 | return obs$; 72 | } 73 | }); 74 | 75 | this.ngxsFirestoreConnect.connect(RacesActions.Error, { 76 | to: () => 77 | this.racesFS.collection$((ref) => 78 | ref 79 | .where('aaa', '==', 0) 80 | .where('bbb', '==', 0) 81 | .orderBy('aaa') 82 | ) 83 | }); 84 | } 85 | 86 | @Action(StreamErrored(RacesActions.Error)) 87 | error(ctx: StateContext, { error }: Errored) {} 88 | 89 | @Action(StreamEmitted(RacesActions.GetPages)) 90 | getPageEmitted( 91 | ctx: StateContext, 92 | { action, payload }: Emitted 93 | ) { 94 | ctx.setState(patch({ races: payload.results || [], pageId: payload.pageId })); 95 | } 96 | 97 | @Action(StreamConnected(RacesActions.Get)) 98 | getConnected(ctx: StateContext, { action }: Connected) { 99 | console.log('[RacesActions.Get] Connected'); 100 | } 101 | 102 | @Action(StreamEmitted(RacesActions.Get)) 103 | getEmitted(ctx: StateContext, { action, payload }: Emitted) { 104 | if (payload) { 105 | ctx.setState( 106 | patch({ 107 | races: iif( 108 | (races) => !!races.find((race) => race.raceId === payload.raceId), 109 | updateItem((race) => race.raceId === payload.raceId, patch(payload)), 110 | insertItem(payload) 111 | ) 112 | }) 113 | ); 114 | } 115 | } 116 | 117 | @Action(StreamDisconnected(RacesActions.Get)) 118 | getDisconnected(ctx: StateContext, { action }: Disconnected) { 119 | console.log('[RacesActions.Get] Disconnected'); 120 | } 121 | 122 | @Action(StreamEmitted(RacesActions.GetAll)) 123 | getAllEmitted(ctx: StateContext, { action, payload }: Emitted) { 124 | ctx.setState(patch({ races: payload })); 125 | } 126 | 127 | @Action([RacesActions.GetAllOnce]) 128 | getAllOnce({ patchState }: StateContext) { 129 | return this.racesFS.collectionOnce$().pipe( 130 | tap((races) => { 131 | patchState({ races }); 132 | }) 133 | ); 134 | } 135 | 136 | @Action([RacesActions.GetOnce]) 137 | getOnce({ getState, patchState }: StateContext, { payload }: RacesActions.GetOnce) { 138 | return this.racesFS.docOnce$(payload, { source: 'default' }).pipe( 139 | tap((race) => { 140 | if (!race) { 141 | return; 142 | } 143 | 144 | const races = [...getState().races]; 145 | const exists = races.findIndex((r) => r.raceId === payload); 146 | if (exists > -1) { 147 | races.splice(exists, 1, race); 148 | patchState({ races }); 149 | } else { 150 | patchState({ races: races.concat(race) }); 151 | } 152 | }) 153 | ); 154 | } 155 | 156 | @Action(RacesActions.Create) 157 | create({ patchState, dispatch }: StateContext, { payload }: RacesActions.Create) { 158 | return this.racesFS.create$(payload); 159 | } 160 | 161 | @Action(RacesActions.Upsert) 162 | upsert({ patchState, dispatch }: StateContext, { payload }: RacesActions.Upsert) { 163 | return this.racesFS.upsert$(payload); 164 | } 165 | 166 | @Action(RacesActions.Update) 167 | update({ patchState, dispatch }: StateContext, { payload }: RacesActions.Update) { 168 | return this.racesFS.update$(payload.raceId!, payload); 169 | } 170 | 171 | @Action(RacesActions.UpdateIfExists) 172 | updateIfExists({ patchState, dispatch }: StateContext, { payload }: RacesActions.Update) { 173 | return this.racesFS.updateIfExists(payload.raceId!, payload as Race); 174 | } 175 | 176 | @Action(RacesActions.Delete) 177 | delete({ patchState, dispatch }: StateContext, { payload }: RacesActions.Delete) { 178 | return this.racesFS.delete$(payload); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /integrations/compat/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/assets/.gitkeep -------------------------------------------------------------------------------- /integrations/compat/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 5 | authDomain: 'joaq-lab.firebaseapp.com', 6 | databaseURL: 'https://joaq-lab.firebaseio.com', 7 | projectId: 'joaq-lab', 8 | storageBucket: 'joaq-lab.appspot.com', 9 | messagingSenderId: '794748950011', 10 | appId: '1:794748950011:web:815fe385e7317c11' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /integrations/compat/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | firebase: { 8 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 9 | authDomain: 'joaq-lab.firebaseapp.com', 10 | databaseURL: 'https://joaq-lab.firebaseio.com', 11 | projectId: 'joaq-lab', 12 | storageBucket: 'joaq-lab.appspot.com', 13 | messagingSenderId: '794748950011', 14 | appId: '1:794748950011:web:815fe385e7317c11' 15 | } 16 | }; 17 | 18 | /* 19 | * In development mode, to ignore zone related error stack frames such as 20 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 21 | * import the following file, but please comment it out in production mode 22 | * because it will have performance impact when throw error 23 | */ 24 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 25 | -------------------------------------------------------------------------------- /integrations/compat/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/compat/src/favicon.ico -------------------------------------------------------------------------------- /integrations/compat/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ngxs Firestore Compat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /integrations/compat/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 | -------------------------------------------------------------------------------- /integrations/compat/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /integrations/compat/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap'; 2 | 3 | html { 4 | height: 100%; 5 | @extend .bg-light; 6 | } 7 | 8 | body { 9 | @extend .bg-light; 10 | } 11 | -------------------------------------------------------------------------------- /integrations/compat/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /integrations/modular/src/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /integrations/modular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
24 |

NGXS Firestore:

25 |
{{ ngxsFirestoreState | json }}
26 |
27 | -------------------------------------------------------------------------------- /integrations/modular/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/app/app.component.scss -------------------------------------------------------------------------------- /integrations/modular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { ngxsFirestoreConnections } from '@ngxs-labs/firestore-plugin'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | public ngxsFirestoreState$ = this.store.select(ngxsFirestoreConnections); 12 | 13 | constructor(private store: Store) {} 14 | } 15 | -------------------------------------------------------------------------------- /integrations/modular/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '../environments/environment'; 2 | 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { NgModule } from '@angular/core'; 5 | import { RouterModule } from '@angular/router'; 6 | import { FormsModule } from '@angular/forms'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import { ListComponent } from './components/list/list.component'; 10 | 11 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 12 | import { getFirestore, provideFirestore } from '@angular/fire/firestore'; 13 | 14 | import { NgxsModule } from '@ngxs/store'; 15 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 16 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 17 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 18 | 19 | import { RacesState } from './states/races/races.state'; 20 | import { OtherComponent } from './components/other/other.component'; 21 | import { NgxsActionsExecutingModule } from '@ngxs-labs/actions-executing'; 22 | import { ClassificationsState } from './states/classifications/classifications.state'; 23 | import { PagedListComponent } from './components/paged-list/paged-list.component'; 24 | import { AttendeesState } from './states/attendees/attendees.state'; 25 | import { ListOnceComponent } from './components/list-once/list-once.component'; 26 | import { ServiceWorkerModule } from '@angular/service-worker'; 27 | 28 | @NgModule({ 29 | declarations: [AppComponent, ListComponent, OtherComponent, PagedListComponent, ListOnceComponent], 30 | imports: [ 31 | BrowserModule, 32 | FormsModule, 33 | RouterModule.forRoot([ 34 | { path: 'list', component: ListComponent }, 35 | { path: 'list-once', component: ListOnceComponent }, 36 | { path: 'paged-list', component: PagedListComponent }, 37 | { path: 'other', component: OtherComponent }, 38 | { path: '', redirectTo: '/list', pathMatch: 'full' } 39 | ]), 40 | NgxsModule.forRoot([RacesState, ClassificationsState, AttendeesState], { 41 | developmentMode: !environment.production 42 | }), 43 | NgxsLoggerPluginModule.forRoot({ 44 | disabled: environment.production 45 | }), 46 | NgxsFirestoreModule.forRoot({ 47 | timeoutWriteOperations: 1000 48 | }), 49 | NgxsActionsExecutingModule.forRoot(), 50 | NgxsReduxDevtoolsPluginModule.forRoot({ 51 | name: 'Ngxs Firestore', 52 | disabled: environment.production, 53 | actionSanitizer: (action) => ({ ...action, action: null }) 54 | }), 55 | ServiceWorkerModule.register('ngsw-worker.js', { 56 | enabled: environment.production, 57 | // Register the ServiceWorker as soon as the app is stable 58 | // or after 30 seconds (whichever comes first). 59 | registrationStrategy: 'registerWhenStable:30000' 60 | }) 61 | ], 62 | providers: [provideFirebaseApp(() => initializeApp(environment.firebase)), provideFirestore(() => getFirestore())], 63 | bootstrap: [AppComponent] 64 | }) 65 | export class AppModule {} 66 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list-once/list-once.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 |
7 |
8 |
9 |
{{ race.id }}
10 |

{{ race.order }}

11 |

{{ race.name }}

12 |

{{ race.description }}

13 | 14 |
{{ race | json }}
15 |
16 |
17 |
18 |
19 |
20 | Loading... 21 |
22 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list-once/list-once.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/app/components/list-once/list-once.component.scss -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list-once/list-once.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListOnceComponent } from './list-once.component'; 4 | import { Store, NgxsModule } from '@ngxs/store'; 5 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 6 | import { Subject } from 'rxjs'; 7 | 8 | describe('ListOnceComponent', () => { 9 | let component: ListOnceComponent; 10 | let fixture: ComponentFixture; 11 | let storeMock; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | storeMock = jest.fn().mockImplementation(() => ({ 16 | select: jest.fn().mockReturnValue(new Subject()), 17 | dispatch: jest.fn().mockReturnValue(new Subject()) 18 | })); 19 | 20 | TestBed.configureTestingModule({ 21 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 22 | declarations: [ListOnceComponent], 23 | providers: [{ provide: Store, useValue: storeMock() }] 24 | }).compileComponents(); 25 | }) 26 | ); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ListOnceComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list-once/list-once.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { RacesActions } from '../../states/races/races.actions'; 4 | import { RacesState } from '../../states/races/races.state'; 5 | import { map } from 'rxjs/operators'; 6 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 7 | 8 | @Component({ 9 | selector: 'app-list-once', 10 | templateUrl: './list-once.component.html', 11 | styleUrls: ['./list-once.component.scss'] 12 | }) 13 | export class ListOnceComponent implements OnInit, OnDestroy { 14 | races$ = this.store.select(RacesState.races); 15 | loading$ = this.store.select(actionsExecuting([RacesActions.GetAll, RacesActions.Get])); 16 | loaded$ = this.loading$.pipe(map((loading) => !loading)); 17 | 18 | constructor(private store: Store) {} 19 | 20 | ngOnInit() {} 21 | 22 | getAll() { 23 | this.store.dispatch(new RacesActions.GetAllOnce()); 24 | } 25 | 26 | get() { 27 | this.store.dispatch(new RacesActions.GetOnce('Pn7RYUL0]1!*JDqWw)S5')); 28 | } 29 | 30 | ngOnDestroy() {} 31 | } 32 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 8 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
Total: {{ total$ | async }}
22 |
23 | 24 |
25 |
26 |
27 |
{{ race.id }}
28 |

{{ race.order }}

29 |

{{ race.name }}

30 |

{{ race.description }}

31 | 32 |
{{ race | json }}
33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |
47 |
48 |
{{ classification.id }}
49 |

{{ classification.order }}

50 |

{{ classification.name }}

51 | 52 |
{{ classification | json }}
53 |
54 |
55 |
56 |
57 |
58 | Loading... 59 |
60 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list/list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/app/components/list/list.component.scss -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListComponent } from './list.component'; 4 | import { Store, NgxsModule } from '@ngxs/store'; 5 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 6 | import { Subject } from 'rxjs'; 7 | 8 | describe('ListComponent', () => { 9 | let component: ListComponent; 10 | let fixture: ComponentFixture; 11 | let storeMock; 12 | 13 | beforeEach( 14 | waitForAsync(() => { 15 | storeMock = jest.fn().mockImplementation(() => ({ 16 | select: jest.fn().mockReturnValue(new Subject()), 17 | dispatch: jest.fn().mockReturnValue(new Subject()) 18 | })); 19 | 20 | TestBed.configureTestingModule({ 21 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 22 | declarations: [ListComponent], 23 | providers: [{ provide: Store, useValue: storeMock() }] 24 | }).compileComponents(); 25 | }) 26 | ); 27 | 28 | beforeEach(() => { 29 | fixture = TestBed.createComponent(ListComponent); 30 | component = fixture.componentInstance; 31 | fixture.detectChanges(); 32 | }); 33 | 34 | it('should create', () => { 35 | expect(component).toBeTruthy(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { RacesActions } from './../../states/races/races.actions'; 4 | import { RacesState } from './../../states/races/races.state'; 5 | import { ClassificationsState } from './../../states/classifications/classifications.state'; 6 | import { ClassificationsActions } from './../../states/classifications/classifications.actions'; 7 | import { Race } from './../../models/race'; 8 | import { Chance } from 'chance'; 9 | import { map } from 'rxjs/operators'; 10 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 11 | import { Disconnect } from '@ngxs-labs/firestore-plugin'; 12 | 13 | @Component({ 14 | selector: 'app-list', 15 | templateUrl: './list.component.html', 16 | styleUrls: ['./list.component.scss'] 17 | }) 18 | export class ListComponent implements OnInit, OnDestroy { 19 | races$ = this.store.select(RacesState.races); 20 | classifications$ = this.store.select(ClassificationsState.classifications); 21 | total$ = this.races$.pipe(map((races) => races.length)); 22 | 23 | gettingAll$ = this.store.select(actionsExecuting([RacesActions.GetAll])); 24 | gettingSingle$ = this.store.select(actionsExecuting([RacesActions.Get])); 25 | creating$ = this.store.select(actionsExecuting([RacesActions.Create])); 26 | loading$ = this.store.select(actionsExecuting([RacesActions.GetAll, RacesActions.Get])); 27 | loaded$ = this.loading$.pipe(map((loading) => !loading)); 28 | disconnecting$ = this.store.select(actionsExecuting([Disconnect])); 29 | throwingError$ = this.store.select(actionsExecuting([RacesActions.Error])); 30 | gettingSubCollection$ = this.store.select(actionsExecuting([ClassificationsActions.GetAll])); 31 | 32 | constructor(private store: Store) {} 33 | 34 | ngOnInit() { 35 | // this.store.dispatch(new RacesActions.GetAll()); 36 | // this.store.dispatch(new RacesActions.Get('8iI)0md[dTAFC[wo!&[N')); 37 | // this.store.dispatch(new RacesActions.Get('AAAAAA')); 38 | } 39 | 40 | disconnect() { 41 | this.store.dispatch(new Disconnect(new RacesActions.Get(']cfct5iL8(H)@Sl#xTcS'))); 42 | } 43 | 44 | reconnect() { 45 | // this.store.dispatch(new RacesActions.GetAll()); 46 | // this.store.dispatch(new RacesActions.Get('8iI)0md[dTAFC[wo!&[N')); 47 | 48 | this.store.dispatch(new RacesActions.Get(']cfct5iL8(H)@Sl#xTcS')); 49 | } 50 | 51 | getAll() { 52 | this.store.dispatch(new RacesActions.GetAll()); 53 | } 54 | 55 | getSubCollection() { 56 | this.store.dispatch(new ClassificationsActions.GetAll('0NN6x6GKDGumGU5dtnk4')); 57 | } 58 | 59 | getCollectionGroup() { 60 | this.store.dispatch(new RacesActions.CollectionGroup()); 61 | } 62 | 63 | get() { 64 | // const ids = ['4(CPo6Fy(7Mo^YklK[Q8', 'FouQf@q4FHJcc&%cnmkT', 'LBWH5KvYp43ia)!IYpwv', ']cfct5iL8(H)@Sl#xTcS']; 65 | // for (let index = 0; index < ids.length; index++) { 66 | // setTimeout(() => this.store.dispatch(new RacesActions.Get(ids[index])), 1000 * index); 67 | // } 68 | 69 | this.store.dispatch(new RacesActions.Get('0V!^fMrWetbs68]ob6%M')); 70 | } 71 | 72 | create() { 73 | const chance = new Chance(); 74 | const race: Partial = {}; 75 | // race.id = chance.string({ length: 20 }); 76 | race.raceId = chance.string({ length: 20 }); 77 | race.name = chance.word(); 78 | race.title = chance.word(); 79 | race.description = chance.sentence(); 80 | this.store.dispatch(new RacesActions.Create(race)); 81 | 82 | // this.store.dispatch(new RacesActions.Create({ 83 | // id: 'test-id', 84 | // name: 'Test', 85 | // title: 'Test Title', 86 | // description: 'Test description', 87 | // })); 88 | } 89 | 90 | update(race: Race) { 91 | const chance = new Chance(); 92 | 93 | this.store.dispatch( 94 | new RacesActions.Update({ 95 | ...race, 96 | name: chance.string(), 97 | description: chance.word() 98 | }) 99 | ); 100 | } 101 | 102 | updateIfExists(race: Race) { 103 | const chance = new Chance(); 104 | 105 | this.store.dispatch( 106 | new RacesActions.UpdateIfExists({ 107 | ...race, 108 | name: chance.string(), 109 | description: chance.word() 110 | }) 111 | ); 112 | } 113 | 114 | delete(id: string) { 115 | this.store.dispatch(new RacesActions.Delete(id)); 116 | } 117 | 118 | ngOnDestroy() { 119 | this.store.dispatch(new Disconnect(RacesActions.GetAll)); 120 | } 121 | 122 | throwError() { 123 | this.store.dispatch(new RacesActions.Error()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/other/other.component.html: -------------------------------------------------------------------------------- 1 | Disconnected on page OnDestroy 2 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/other/other.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/app/components/other/other.component.scss -------------------------------------------------------------------------------- /integrations/modular/src/app/components/other/other.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { OtherComponent } from './other.component'; 3 | 4 | describe('OtherComponent', () => { 5 | let component: OtherComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach( 9 | waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [OtherComponent] 12 | }).compileComponents(); 13 | }) 14 | ); 15 | 16 | beforeEach(() => {}); 17 | 18 | it('should create', () => { 19 | fixture = TestBed.createComponent(OtherComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/other/other.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-other', 5 | templateUrl: './other.component.html', 6 | styleUrls: ['./other.component.scss'] 7 | }) 8 | export class OtherComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/paged-list/paged-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 |
7 |
8 |
{{ race.id }}
9 |

{{ race.order }}

10 |

{{ race.name }}

11 |

{{ race.description }}

12 |
13 |
14 |
15 | 16 |
17 | 20 | 21 | 24 |
25 |
26 |
27 |
28 |
{{ attendee.id }}
29 |

{{ attendee.order }}

30 |

{{ attendee.name }}

31 |

{{ attendee.description }}

32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/paged-list/paged-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/app/components/paged-list/paged-list.component.scss -------------------------------------------------------------------------------- /integrations/modular/src/app/components/paged-list/paged-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | 5 | import { PagedListComponent } from './paged-list.component'; 6 | 7 | describe('PagedListComponent', () => { 8 | let component: PagedListComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | imports: [NgxsModule.forRoot(), NgxsFirestoreModule.forRoot()], 14 | declarations: [PagedListComponent] 15 | }).compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(PagedListComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /integrations/modular/src/app/components/paged-list/paged-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { actionsExecuting } from '@ngxs-labs/actions-executing'; 3 | import { GetLastPage, GetNextPage } from '@ngxs-labs/firestore-plugin'; 4 | import { Store } from '@ngxs/store'; 5 | import { AttendeesActions } from './../../states/attendees/attendees.actions'; 6 | import { AttendeesState } from './../../states/attendees/attendees.state'; 7 | import { RacesActions } from './../../states/races/races.actions'; 8 | import { RacesState } from './../../states/races/races.state'; 9 | 10 | @Component({ 11 | selector: 'app-paged-list', 12 | templateUrl: './paged-list.component.html', 13 | styleUrls: ['./paged-list.component.scss'] 14 | }) 15 | export class PagedListComponent implements OnInit { 16 | races$ = this.store.select(RacesState.races); 17 | attendees$ = this.store.select(AttendeesState.attendees); 18 | nextPageExecuting$ = this.store.select(actionsExecuting([GetNextPage])); 19 | lastPageExecuting$ = this.store.select(actionsExecuting([GetLastPage])); 20 | 21 | constructor(private store: Store) {} 22 | 23 | ngOnInit() { 24 | this.store.dispatch(new RacesActions.GetPages()); 25 | this.store.dispatch(new AttendeesActions.GetPages()); 26 | } 27 | 28 | nextPage() { 29 | const pageId = this.store.selectSnapshot(RacesState.pageId); 30 | this.store.dispatch(new GetNextPage(pageId)); 31 | } 32 | 33 | lastPage() { 34 | const pageId = this.store.selectSnapshot(RacesState.pageId); 35 | this.store.dispatch(new GetLastPage(pageId)); 36 | } 37 | 38 | nextPageAttendees() { 39 | const pageId = this.store.selectSnapshot(AttendeesState.pageId); 40 | this.store.dispatch(new GetNextPage(pageId)); 41 | } 42 | 43 | lastPageAttendees() { 44 | const pageId = this.store.selectSnapshot(AttendeesState.pageId); 45 | this.store.dispatch(new GetLastPage(pageId)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /integrations/modular/src/app/models/attendee.ts: -------------------------------------------------------------------------------- 1 | export interface Attendee { 2 | id: string; 3 | order: string; 4 | name: string; 5 | description: string; 6 | } 7 | -------------------------------------------------------------------------------- /integrations/modular/src/app/models/classification.ts: -------------------------------------------------------------------------------- 1 | export interface Classification { 2 | id: string; 3 | name: string; 4 | order: string; 5 | } 6 | -------------------------------------------------------------------------------- /integrations/modular/src/app/models/race.ts: -------------------------------------------------------------------------------- 1 | export interface Race { 2 | id: string; 3 | raceId: string; 4 | title: string; 5 | description: string; 6 | name: string; 7 | order: string; 8 | 9 | readonly testProp: string; 10 | } 11 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/attendees.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin'; 3 | import 'firebase/app'; 4 | import 'firebase/firestore'; 5 | import { Attendee } from '../models/attendee'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class AttendeesFirestore extends NgxsFirestore { 11 | protected path = 'attendees'; 12 | } 13 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/classifications.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin'; 3 | import { Race } from '../models/race'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ClassificationsFirestore extends NgxsFirestore { 9 | idField = 'classificationId'; 10 | private _raceId = ''; 11 | protected get path() { 12 | return `races/${this.raceId}/classifications`; 13 | } 14 | 15 | public setRaceId(raceId: string) { 16 | this._raceId = raceId; 17 | } 18 | 19 | protected get raceId() { 20 | return this._raceId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/inject-custom-dependecies.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Firestore } from '@angular/fire/firestore'; 3 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 4 | import { NgxsModule } from '@ngxs/store'; 5 | import { CustomDependency, InjectCustomDependenciesService } from './inject-custom-dependecies.service'; 6 | 7 | describe('InjectCustomDependenciesService', () => { 8 | beforeEach(() => 9 | TestBed.configureTestingModule({ 10 | imports: [NgxsModule.forRoot([]), NgxsFirestoreModule.forRoot()], 11 | providers: [ 12 | { provide: CustomDependency, useClass: CustomDependency }, 13 | { provide: Firestore, useValue: jest.fn() } 14 | ] 15 | }) 16 | ); 17 | 18 | it('should be created', () => { 19 | const service: InjectCustomDependenciesService = TestBed.inject(InjectCustomDependenciesService); 20 | expect(service).toBeTruthy(); 21 | }); 22 | 23 | it('should inject a custom dep', () => { 24 | const service: InjectCustomDependenciesService = TestBed.inject(InjectCustomDependenciesService); 25 | expect(service.customeDependency.works()).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/inject-custom-dependecies.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { NgxsFirestore, NgxsFirestoreAdapter } from '@ngxs-labs/firestore-plugin'; 3 | 4 | @Injectable() 5 | export class CustomDependency { 6 | works() { 7 | return true; 8 | } 9 | } 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class InjectCustomDependenciesService extends NgxsFirestore { 15 | protected path = 'races'; 16 | 17 | constructor(@Inject(NgxsFirestoreAdapter) adapter: NgxsFirestoreAdapter, public customeDependency: CustomDependency) { 18 | super(adapter); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/races.firestore.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { RacesFirestore } from './races.firestore'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 5 | import { Firestore } from '@angular/fire/firestore'; 6 | 7 | describe('RacesFirestore', () => { 8 | beforeEach( 9 | waitForAsync(() => 10 | TestBed.configureTestingModule({ 11 | imports: [NgxsModule.forRoot([]), NgxsFirestoreModule.forRoot()], 12 | providers: [{ provide: Firestore, useValue: jest.fn() }] 13 | }) 14 | ) 15 | ); 16 | 17 | it('should be created', () => { 18 | const service: RacesFirestore = TestBed.inject(RacesFirestore); 19 | expect(service).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /integrations/modular/src/app/services/races.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin'; 3 | import { doc, FirestoreDataConverter, updateDoc } from '@angular/fire/firestore'; 4 | import { Race } from '../models/race'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class RacesFirestore extends NgxsFirestore { 10 | protected path = 'races'; 11 | idField = 'raceId'; 12 | metadataField = '_metadata'; 13 | 14 | converter: FirestoreDataConverter = { 15 | toFirestore: (value) => { 16 | const db = { ...value }; 17 | delete db.testProp; 18 | return db; 19 | }, 20 | fromFirestore: (snapshot, options) => { 21 | const data = snapshot.data(options); 22 | 23 | return { ...data, testProp: data['id'] + data['title'] }; 24 | } 25 | }; 26 | 27 | updateIfExists(id: string, data: Race) { 28 | const docRef = doc(this.adapter.firestore, `${this.path}/${id}`).withConverter(this.converter); 29 | return updateDoc(docRef, data as any); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/attendees/attendees.actions.ts: -------------------------------------------------------------------------------- 1 | export namespace AttendeesActions { 2 | export class GetPages { 3 | public static readonly type = '[Attendees] GetPages'; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/attendees/attendees.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { AttendeesActions } from './attendees.actions'; 3 | import { NgxsFirestoreConnect, Emitted, StreamEmitted, NgxsFirestorePageService } from '@ngxs-labs/firestore-plugin'; 4 | import { patch } from '@ngxs/store/operators'; 5 | import { Injectable } from '@angular/core'; 6 | import { Attendee } from './../../models/attendee'; 7 | import { AttendeesFirestore } from './../../services/attendees.firestore'; 8 | 9 | export interface AttendeesStateModel { 10 | attendees: Attendee[]; 11 | pageId: string; 12 | } 13 | 14 | @State({ 15 | name: 'attendees', 16 | defaults: { 17 | attendees: [], 18 | pageId: '' 19 | } 20 | }) 21 | @Injectable() 22 | export class AttendeesState implements NgxsOnInit { 23 | @Selector() static attendees(state: AttendeesStateModel) { 24 | return state.attendees; 25 | } 26 | 27 | @Selector() static pageId(state: AttendeesStateModel) { 28 | return state.pageId; 29 | } 30 | 31 | constructor( 32 | private attendeesFS: AttendeesFirestore, 33 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 34 | private nxgsFirestorePage: NgxsFirestorePageService 35 | ) {} 36 | 37 | ngxsOnInit(ctx: StateContext) { 38 | this.ngxsFirestoreConnect.connect(AttendeesActions.GetPages, { 39 | to: () => { 40 | const obs$ = this.nxgsFirestorePage.create( 41 | (pageFn) => 42 | this.attendeesFS.collection$((ref) => { 43 | return pageFn(ref); 44 | }), 45 | 5, 46 | [{ fieldPath: 'id' }] 47 | ); 48 | 49 | return obs$; 50 | } 51 | }); 52 | } 53 | 54 | @Action(StreamEmitted(AttendeesActions.GetPages)) 55 | getPageEmitted( 56 | ctx: StateContext, 57 | { action, payload }: Emitted 58 | ) { 59 | ctx.setState(patch({ attendees: payload.results || [], pageId: payload.pageId })); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/classifications/classifications.actions.ts: -------------------------------------------------------------------------------- 1 | export namespace ClassificationsActions { 2 | export class Get { 3 | public static readonly type = '[Classifications] Get'; 4 | constructor(public payload: string) {} 5 | } 6 | 7 | export class GetAll { 8 | public static readonly type = '[Classifications] GetAll'; 9 | constructor(public raceId: string) {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/classifications/classifications.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { NgxsModule, Store } from '@ngxs/store'; 3 | import { ClassificationsState } from './classifications.state'; 4 | import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin'; 5 | import { BehaviorSubject } from 'rxjs'; 6 | import { ClassificationsActions } from './classifications.actions'; 7 | import { Classification } from './../../models/classification'; 8 | import { ClassificationsFirestore } from './../../services/classifications.firestore'; 9 | 10 | describe('Classifications State', () => { 11 | let store: Store; 12 | let mockClassificationsFS; 13 | let mockCollection$: jest.Mock; 14 | 15 | beforeEach( 16 | waitForAsync(() => { 17 | mockClassificationsFS = jest.fn(() => ({ 18 | collection$: mockCollection$, 19 | setRaceId: jest.fn() 20 | })); 21 | 22 | mockCollection$ = jest.fn(); 23 | 24 | TestBed.configureTestingModule({ 25 | imports: [NgxsModule.forRoot([ClassificationsState]), NgxsFirestoreModule.forRoot()], 26 | providers: [{ provide: ClassificationsFirestore, useValue: mockClassificationsFS() }] 27 | }).compileComponents(); 28 | store = TestBed.inject(Store); 29 | }) 30 | ); 31 | 32 | it('should getall classifications', () => { 33 | mockCollection$.mockReturnValue(new BehaviorSubject([{ id: 'a' }])); 34 | store.dispatch(new ClassificationsActions.GetAll('id')); 35 | expect(store.selectSnapshot(ClassificationsState.classifications)).toEqual([{ id: 'a' } as Classification]); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/classifications/classifications.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { ClassificationsActions } from './classifications.actions'; 3 | import { NgxsFirestoreConnect, Emitted, StreamEmitted } from '@ngxs-labs/firestore-plugin'; 4 | import { Classification } from './../../models/classification'; 5 | import { ClassificationsFirestore } from './../../services/classifications.firestore'; 6 | import { iif, insertItem, patch, updateItem } from '@ngxs/store/operators'; 7 | import { Injectable } from '@angular/core'; 8 | 9 | export interface ClassificationsStateModel { 10 | classifications: Classification[]; 11 | } 12 | 13 | @State({ 14 | name: 'classifications', 15 | defaults: { 16 | classifications: [] 17 | } 18 | }) 19 | @Injectable() 20 | export class ClassificationsState implements NgxsOnInit { 21 | @Selector() static classifications(state: ClassificationsStateModel) { 22 | return state.classifications; 23 | } 24 | 25 | constructor( 26 | private classificationsFS: ClassificationsFirestore, 27 | private ngxsFirestoreConnect: NgxsFirestoreConnect 28 | ) {} 29 | 30 | ngxsOnInit(ctx: StateContext) { 31 | this.ngxsFirestoreConnect.connect(ClassificationsActions.GetAll, { 32 | to: ({ raceId }) => { 33 | this.classificationsFS.setRaceId(raceId); 34 | return this.classificationsFS.collection$(); 35 | } 36 | }); 37 | 38 | this.ngxsFirestoreConnect.connect(ClassificationsActions.Get, { 39 | to: ({ payload }) => this.classificationsFS.doc$(payload) 40 | }); 41 | } 42 | 43 | @Action(StreamEmitted(ClassificationsActions.Get)) 44 | getEmitted( 45 | ctx: StateContext, 46 | { payload }: Emitted 47 | ) { 48 | if (payload) { 49 | ctx.setState( 50 | patch({ 51 | classifications: iif( 52 | (classifications) => !!classifications.find((classification) => classification.id === payload.id), 53 | updateItem((classification) => classification.id === payload.id, patch(payload)), 54 | insertItem(payload) 55 | ) 56 | }) 57 | ); 58 | } 59 | } 60 | 61 | @Action(StreamEmitted(ClassificationsActions.GetAll)) 62 | getAllEmitted( 63 | ctx: StateContext, 64 | { payload }: Emitted 65 | ) { 66 | ctx.setState(patch({ classifications: payload })); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/races/races.actions.ts: -------------------------------------------------------------------------------- 1 | import { Race } from './../../models/race'; 2 | 3 | namespace RacesActionsPayloads { 4 | export type Update = Partial; 5 | export type Upsert = Partial; 6 | export type Create = Partial; 7 | } 8 | 9 | export namespace RacesActions { 10 | export class GetAllOnce { 11 | public static readonly type = '[Races] GetAllOnce'; 12 | } 13 | export class GetOnce { 14 | public static readonly type = '[Races] GetOnce'; 15 | constructor(public payload: string) {} 16 | } 17 | export class Get { 18 | public static readonly type = '[Races] Get'; 19 | constructor(public payload: string) {} 20 | } 21 | export class GetAll { 22 | public static readonly type = '[Races] GetAll'; 23 | } 24 | 25 | export class GetPages { 26 | public static readonly type = '[Races] GetPages'; 27 | } 28 | 29 | export class GetByField { 30 | public static readonly type = '[Races] GetByField'; 31 | } 32 | 33 | export class CollectionGroup { 34 | public static readonly type = '[Races] CollectionGroup'; 35 | } 36 | 37 | export class Create { 38 | public static readonly type = '[Races] Create'; 39 | constructor(public payload: RacesActionsPayloads.Create) {} 40 | } 41 | export class Upsert { 42 | public static readonly type = '[Races] Upsert'; 43 | constructor(public payload: RacesActionsPayloads.Upsert) {} 44 | } 45 | export class Update { 46 | public static readonly type = '[Races] Update'; 47 | constructor(public payload: RacesActionsPayloads.Update) {} 48 | } 49 | export class UpdateIfExists { 50 | public static readonly type = '[Races] UpdateIfExists'; 51 | constructor(public payload: RacesActionsPayloads.Update) {} 52 | } 53 | export class Delete { 54 | public static readonly type = '[Races] Delete'; 55 | constructor(public payload: string) {} 56 | } 57 | export class Error { 58 | public static readonly type = '[Races] Error'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/races/races.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { NgxsModule, Store } from '@ngxs/store'; 3 | import { RacesState } from './races.state'; 4 | import { RacesActions } from './races.actions'; 5 | import { RacesFirestore } from './../../services/races.firestore'; 6 | import { NgxsFirestoreModule, NgxsFirestorePageIdService } from '@ngxs-labs/firestore-plugin'; 7 | import { BehaviorSubject } from 'rxjs'; 8 | import { Race } from './../../models/race'; 9 | 10 | describe('Races State', () => { 11 | let store: Store; 12 | let mockRacesFS; 13 | let mockCollection$: jest.Mock; 14 | 15 | beforeEach( 16 | waitForAsync(() => { 17 | mockRacesFS = jest.fn(() => ({ 18 | collection$: mockCollection$ 19 | })); 20 | 21 | mockCollection$ = jest.fn(); 22 | 23 | TestBed.configureTestingModule({ 24 | imports: [NgxsModule.forRoot([RacesState]), NgxsFirestoreModule.forRoot()], 25 | providers: [ 26 | { provide: RacesFirestore, useValue: mockRacesFS() }, 27 | { provide: NgxsFirestorePageIdService, useValue: { createId: jest.fn() } } 28 | ] 29 | }).compileComponents(); 30 | store = TestBed.inject(Store); 31 | }) 32 | ); 33 | 34 | it('should getall races', () => { 35 | mockCollection$.mockReturnValue(new BehaviorSubject([{ raceId: 'a' }])); 36 | store.dispatch(new RacesActions.GetAll()); 37 | expect(store.selectSnapshot(RacesState.races)).toEqual([{ raceId: 'a' } as Race]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /integrations/modular/src/app/states/races/races.state.ts: -------------------------------------------------------------------------------- 1 | import { State, Action, StateContext, NgxsOnInit, Selector } from '@ngxs/store'; 2 | import { RacesActions } from './races.actions'; 3 | import { tap } from 'rxjs/operators'; 4 | import { 5 | NgxsFirestoreConnect, 6 | Connected, 7 | Emitted, 8 | Disconnected, 9 | StreamConnected, 10 | StreamEmitted, 11 | StreamDisconnected, 12 | StreamErrored, 13 | Errored, 14 | NgxsFirestorePageService 15 | } from '@ngxs-labs/firestore-plugin'; 16 | import { Race } from './../../models/race'; 17 | import { RacesFirestore } from './../../services/races.firestore'; 18 | import { patch, insertItem, iif, updateItem } from '@ngxs/store/operators'; 19 | import { Injectable } from '@angular/core'; 20 | import { orderBy, query, where } from '@angular/fire/firestore'; 21 | 22 | export interface RacesStateModel { 23 | races: Race[]; 24 | pageId: string; 25 | activeRaces: Race[]; 26 | } 27 | 28 | @State({ 29 | name: 'races', 30 | defaults: { 31 | races: [], 32 | pageId: '', 33 | activeRaces: [] 34 | } 35 | }) 36 | @Injectable() 37 | export class RacesState implements NgxsOnInit { 38 | @Selector() static races(state: RacesStateModel) { 39 | return state.races; 40 | } 41 | @Selector() static activeRaces(state: RacesStateModel) { 42 | return state.activeRaces; 43 | } 44 | @Selector() static pageId(state: RacesStateModel) { 45 | return state.pageId; 46 | } 47 | 48 | constructor( 49 | private racesFS: RacesFirestore, 50 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 51 | private ngxsFirestorePage: NgxsFirestorePageService 52 | ) {} 53 | 54 | ngxsOnInit(_ctx: StateContext) { 55 | this.ngxsFirestoreConnect.connect(RacesActions.GetAll, { 56 | to: () => this.racesFS.collection$(), 57 | connectedActionFinishesOn: 'FirstEmit' 58 | }); 59 | 60 | this.ngxsFirestoreConnect.connect(RacesActions.Get, { 61 | to: ({ payload }) => this.racesFS.doc$(payload) 62 | }); 63 | 64 | this.ngxsFirestoreConnect.connect(RacesActions.GetPages, { 65 | to: () => { 66 | const obs$ = this.ngxsFirestorePage.create( 67 | (pageFn) => this.racesFS.collection$((ref) => query(pageFn(ref), where('title', '>=', 's'))), 68 | 5, 69 | [{ fieldPath: 'title' }] 70 | ); 71 | 72 | return obs$; 73 | } 74 | }); 75 | 76 | this.ngxsFirestoreConnect.connect(RacesActions.GetByField, { 77 | to: () => 78 | this.racesFS.collection$((ref) => query(ref, where('aaa', '==', 0), where('bbb', '==', 0), orderBy('aaa'))) 79 | }); 80 | 81 | this.ngxsFirestoreConnect.connect(RacesActions.CollectionGroup, { 82 | to: () => this.racesFS.collectionGroup$((ref) => query(ref, where('subcollection', '==', 'subcollection'))) 83 | }); 84 | } 85 | 86 | @Action(StreamErrored(RacesActions.Error)) 87 | error(ctx: StateContext, { error }: Errored) {} 88 | 89 | @Action(StreamEmitted(RacesActions.GetPages)) 90 | getPageEmitted( 91 | ctx: StateContext, 92 | { action, payload }: Emitted 93 | ) { 94 | ctx.setState(patch({ races: payload.results || [], pageId: payload.pageId })); 95 | } 96 | 97 | @Action(StreamConnected(RacesActions.Get)) 98 | getConnected(ctx: StateContext, { action }: Connected) { 99 | console.log('[RacesActions.Get] Connected'); 100 | } 101 | 102 | @Action(StreamEmitted(RacesActions.Get)) 103 | getEmitted(ctx: StateContext, { action, payload }: Emitted) { 104 | if (payload) { 105 | ctx.setState( 106 | patch({ 107 | races: iif( 108 | (races) => !!races.find((race) => race.raceId === payload.raceId), 109 | updateItem((race) => race.raceId === payload.raceId, patch(payload)), 110 | insertItem(payload) 111 | ) 112 | }) 113 | ); 114 | } 115 | } 116 | 117 | @Action(StreamDisconnected(RacesActions.Get)) 118 | getDisconnected(ctx: StateContext, { action }: Disconnected) { 119 | console.log('[RacesActions.Get] Disconnected'); 120 | } 121 | 122 | @Action(StreamEmitted(RacesActions.GetAll)) 123 | getAllEmitted(ctx: StateContext, { action, payload }: Emitted) { 124 | ctx.setState(patch({ races: payload })); 125 | } 126 | 127 | @Action(StreamEmitted(RacesActions.CollectionGroup)) 128 | CollectionGroup(ctx: StateContext, { action, payload }: Emitted) { 129 | debugger; 130 | ctx.setState(patch({ races: payload })); 131 | } 132 | 133 | @Action([RacesActions.GetAllOnce]) 134 | getAllOnce({ patchState }: StateContext) { 135 | return this.racesFS.collectionOnce$().pipe( 136 | tap((races) => { 137 | patchState({ races }); 138 | }) 139 | ); 140 | } 141 | 142 | @Action([RacesActions.GetOnce]) 143 | getOnce({ getState, patchState }: StateContext, { payload }: RacesActions.GetOnce) { 144 | return this.racesFS.docOnce$(payload, { source: 'default' }).pipe( 145 | tap((race) => { 146 | if (!race) { 147 | return; 148 | } 149 | 150 | const races = [...getState().races]; 151 | const exists = races.findIndex((r) => r.raceId === payload); 152 | if (exists > -1) { 153 | races.splice(exists, 1, race); 154 | patchState({ races }); 155 | } else { 156 | patchState({ races: races.concat(race) }); 157 | } 158 | }) 159 | ); 160 | } 161 | 162 | @Action(RacesActions.Create) 163 | create({ patchState, dispatch }: StateContext, { payload }: RacesActions.Create) { 164 | return this.racesFS.create$(payload); 165 | } 166 | 167 | @Action(RacesActions.Upsert) 168 | upsert({ patchState, dispatch }: StateContext, { payload }: RacesActions.Upsert) { 169 | return this.racesFS.upsert$(payload); 170 | } 171 | 172 | @Action(RacesActions.Update) 173 | update({ patchState, dispatch }: StateContext, { payload }: RacesActions.Update) { 174 | return this.racesFS.update$(payload.raceId!, payload); 175 | } 176 | 177 | @Action(RacesActions.UpdateIfExists) 178 | updateIfExists({ patchState, dispatch }: StateContext, { payload }: RacesActions.Update) { 179 | return this.racesFS.updateIfExists(payload.raceId!, payload as Race); 180 | } 181 | 182 | @Action(RacesActions.Delete) 183 | delete({ patchState, dispatch }: StateContext, { payload }: RacesActions.Delete) { 184 | return this.racesFS.delete$(payload); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /integrations/modular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/.gitkeep -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /integrations/modular/src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /integrations/modular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 5 | authDomain: 'joaq-lab.firebaseapp.com', 6 | databaseURL: 'https://joaq-lab.firebaseio.com', 7 | projectId: 'joaq-lab', 8 | storageBucket: 'joaq-lab.appspot.com', 9 | messagingSenderId: '794748950011', 10 | appId: '1:794748950011:web:815fe385e7317c11' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /integrations/modular/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | firebase: { 8 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 9 | authDomain: 'joaq-lab.firebaseapp.com', 10 | databaseURL: 'https://joaq-lab.firebaseio.com', 11 | projectId: 'joaq-lab', 12 | storageBucket: 'joaq-lab.appspot.com', 13 | messagingSenderId: '794748950011', 14 | appId: '1:794748950011:web:815fe385e7317c11' 15 | } 16 | }; 17 | 18 | /* 19 | * In development mode, to ignore zone related error stack frames such as 20 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 21 | * import the following file, but please comment it out in production mode 22 | * because it will have performance impact when throw error 23 | */ 24 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 25 | -------------------------------------------------------------------------------- /integrations/modular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/modular/src/favicon.ico -------------------------------------------------------------------------------- /integrations/modular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ngxs Firestore Modular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /integrations/modular/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 | -------------------------------------------------------------------------------- /integrations/modular/src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modular", 3 | "short_name": "modular", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /integrations/modular/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /integrations/modular/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap'; 2 | 3 | html { 4 | height: 100%; 5 | @extend .bg-light; 6 | } 7 | 8 | body { 9 | @extend .bg-light; 10 | } 11 | -------------------------------------------------------------------------------- /integrations/modular/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Hello from {{ name }}!

2 | 3 | Learn more about Angular 4 | 5 | 6 |
{{ all$ | async | json }}
7 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/standalone/src/app/app.component.scss -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | import { importProvidersFrom } from '@angular/core'; 4 | import { NgxsModule } from '@ngxs/store'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async () => { 8 | await TestBed.configureTestingModule({ 9 | imports: [AppComponent], 10 | providers: [importProvidersFrom(NgxsModule.forRoot([]))] 11 | }).compileComponents(); 12 | }); 13 | 14 | it('should create the app', () => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.componentInstance; 17 | expect(app).toBeTruthy(); 18 | }); 19 | 20 | it(`should have the 'standalone' title`, () => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.componentInstance; 23 | expect(app.name).toEqual('Angular'); 24 | }); 25 | 26 | it('should render title', () => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.nativeElement as HTMLElement; 30 | expect(compiled.querySelector('h1')?.textContent).toContain('Hello from Angular!'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AsyncPipe, JsonPipe } from '@angular/common'; 3 | import { all } from './states/test.selectors'; 4 | import { Store } from '@ngxs/store'; 5 | import { GetAll } from './states/test.actions'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | standalone: true, 10 | imports: [AsyncPipe, JsonPipe], 11 | templateUrl: './app.component.html', 12 | styleUrl: './app.component.scss' 13 | }) 14 | export class AppComponent { 15 | name = 'Angular'; 16 | all$ = this.store.select(all); 17 | 18 | constructor(private store: Store) {} 19 | 20 | ngOnInit() { 21 | this.store.dispatch(new GetAll()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { provideRouter } from '@angular/router'; 3 | 4 | import { routes } from './app.routes'; 5 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 6 | import { getFirestore, provideFirestore } from '@angular/fire/firestore'; 7 | import { NgxsModule } from '@ngxs/store'; 8 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 9 | import { environment } from '../environments/environment'; 10 | import { TestState } from './states/test.state'; 11 | import { provideNgxsFirestore } from '@ngxs-labs/firestore-plugin'; 12 | 13 | export const appConfig: ApplicationConfig = { 14 | providers: [ 15 | provideRouter(routes), 16 | importProvidersFrom( 17 | NgxsModule.forRoot([TestState], { 18 | developmentMode: !environment.production 19 | }), 20 | NgxsLoggerPluginModule.forRoot({ 21 | disabled: environment.production 22 | }) 23 | ), 24 | provideFirebaseApp(() => initializeApp(environment.firebase)), 25 | provideFirestore(() => getFirestore()), 26 | provideNgxsFirestore({ 27 | developmentMode: true 28 | }) 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/services/test.firestore.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { NgxsFirestore } from '@ngxs-labs/firestore-plugin'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class TestFirestore extends NgxsFirestore { 6 | path = 'test'; 7 | } 8 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/states/test.actions.ts: -------------------------------------------------------------------------------- 1 | export class GetAll { 2 | static readonly type = '[Test] GetAll'; 3 | } 4 | 5 | export class Create { 6 | static readonly type = '[Test] Create'; 7 | constructor(public payload: any) {} 8 | } 9 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/states/test.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@ngxs/store'; 2 | import { TestState, TestStateModel } from './test.state'; 3 | 4 | export const all = createSelector([TestState], (state: TestStateModel) => { 5 | return state.items; 6 | }); 7 | -------------------------------------------------------------------------------- /integrations/standalone/src/app/states/test.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Action, NgxsOnInit, State, StateContext } from '@ngxs/store'; 3 | import { TestFirestore } from './../services/test.firestore'; 4 | import { Create, GetAll } from './test.actions'; 5 | import { Emitted, NgxsFirestoreConnect, StreamEmitted } from '@ngxs-labs/firestore-plugin'; 6 | 7 | export interface TestStateModel { 8 | items: any[]; 9 | } 10 | 11 | @State({ 12 | name: 'test', 13 | defaults: { 14 | items: [] 15 | } 16 | }) 17 | @Injectable() 18 | export class TestState implements NgxsOnInit { 19 | constructor(private testFS: TestFirestore, private ngxsFirestoreConnect: NgxsFirestoreConnect) {} 20 | 21 | ngxsOnInit(ctx: StateContext) { 22 | this.ngxsFirestoreConnect.connect(GetAll, { 23 | to: () => this.testFS.collection$() 24 | }); 25 | } 26 | 27 | @Action(StreamEmitted(GetAll)) 28 | getAll(ctx: StateContext, { payload }: Emitted) { 29 | ctx.patchState({ 30 | items: payload || [] 31 | }); 32 | } 33 | 34 | @Action(Create) 35 | async sendMessage(ctx: StateContext, { payload }: any) { 36 | return this.testFS.create$(payload); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /integrations/standalone/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/standalone/src/assets/.gitkeep -------------------------------------------------------------------------------- /integrations/standalone/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | firebase: { 4 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 5 | authDomain: 'joaq-lab.firebaseapp.com', 6 | databaseURL: 'https://joaq-lab.firebaseio.com', 7 | projectId: 'joaq-lab', 8 | storageBucket: 'joaq-lab.appspot.com', 9 | messagingSenderId: '794748950011', 10 | appId: '1:794748950011:web:815fe385e7317c11' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /integrations/standalone/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | firebase: { 8 | apiKey: 'AIzaSyDODLVMuwrvGobMXkI89DktOLgwf0mCM24', 9 | authDomain: 'joaq-lab.firebaseapp.com', 10 | databaseURL: 'https://joaq-lab.firebaseio.com', 11 | projectId: 'joaq-lab', 12 | storageBucket: 'joaq-lab.appspot.com', 13 | messagingSenderId: '794748950011', 14 | appId: '1:794748950011:web:815fe385e7317c11' 15 | } 16 | }; 17 | 18 | /* 19 | * In development mode, to ignore zone related error stack frames such as 20 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 21 | * import the following file, but please comment it out in production mode 22 | * because it will have performance impact when throw error 23 | */ 24 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 25 | -------------------------------------------------------------------------------- /integrations/standalone/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngxs-labs/firestore-plugin/11c3620dffb215279816b5c54d3f4de503a4042c/integrations/standalone/src/favicon.ico -------------------------------------------------------------------------------- /integrations/standalone/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Standalone 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /integrations/standalone/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); 6 | -------------------------------------------------------------------------------- /integrations/standalone/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /integrations/standalone/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /integrations/standalone/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { pathsToModuleNameMapper: resolver } = require('ts-jest'); 3 | const { compilerOptions } = require('./tsconfig'); 4 | const moduleNameMapper = resolver(compilerOptions.paths, { prefix: '/' }); 5 | 6 | module.exports = { 7 | verbose: true, 8 | watch: false, 9 | cache: false, 10 | preset: 'jest-preset-angular', 11 | rootDir: path.resolve('.'), 12 | testMatch: ['/**/ngxs-firestore*.spec.ts'], 13 | collectCoverageFrom: ['/integrations/**/*.ts', '/packages/**/*.ts'], 14 | setupFilesAfterEnv: ['/setupJest.ts'], 15 | coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/.angular/'], 16 | bail: true, 17 | moduleNameMapper, 18 | modulePathIgnorePatterns: ['/dist/', '/node_modules/', '/.angular/'], 19 | modulePaths: [''] 20 | }; 21 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] 10 | } 11 | }, 12 | { 13 | "name": "assets", 14 | "installMode": "lazy", 15 | "updateMode": "prefetch", 16 | "resources": { 17 | "files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"] 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngxs-firestore", 3 | "scripts": { 4 | "start": "ng serve", 5 | "build": "ng build ngxs-firestore --configuration production && yarn copy-readme", 6 | "build-integration": "ng build modular --configuration production", 7 | "copy-readme": "ts-node --project tsconfig.tools.json ./tools/copy-readme", 8 | "test": "jest --config ./jest.config.js", 9 | "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --config ./jest.config.js", 10 | "test:ci": "jest --config ./jest.config.js --collect-coverage", 11 | "format": "prettier --write \"{src,integration,tools}/**/*.{html,scss,json,ts}\"", 12 | "format:check": "prettier \"{src,integration,tools}/**/*.{html,scss,json,ts}\"", 13 | "lint": "eslint -c .eslintrc.js --ext .ts", 14 | "release": "standard-version --release-as patch" 15 | }, 16 | "dependencies": { 17 | "@angular/animations": "^18.0.5", 18 | "@angular/common": "^18.0.5", 19 | "@angular/compiler": "^18.0.5", 20 | "@angular/core": "^18.0.5", 21 | "@angular/fire": "^18.0.1", 22 | "@angular/forms": "^18.0.5", 23 | "@angular/platform-browser": "^18.0.5", 24 | "@angular/platform-browser-dynamic": "^18.0.5", 25 | "@angular/router": "^18.0.5", 26 | "@angular/service-worker": "^18.0.5", 27 | "@ngxs-labs/actions-executing": "18.0.0", 28 | "@ngxs/logger-plugin": "18.0.0", 29 | "@ngxs/store": "18.0.0", 30 | "bootstrap": "^5.0.0", 31 | "chance": "^1.1.4", 32 | "core-js": "^3.3.2", 33 | "firebase": "^10.9.0", 34 | "rxfire": "^6.0.0", 35 | "rxjs": "^7.8.1", 36 | "tslib": "^2.0.0", 37 | "zone.js": "~0.14.2" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/architect": "0.1800.6", 41 | "@angular-devkit/build-angular": "^18.0.6", 42 | "@angular-eslint/eslint-plugin": "^13.0.1", 43 | "@angular/cli": "18.0.6", 44 | "@angular/compiler-cli": "^18.0.5", 45 | "@angular/language-service": "^18.0.5", 46 | "@commitlint/cli": "^8.1.0", 47 | "@commitlint/config-angular": "^8.1.0", 48 | "@commitlint/config-conventional": "^8.3.4", 49 | "@ngxs/devtools-plugin": "^18.0.0", 50 | "@types/chance": "^1.0.7", 51 | "@types/jasmine": "~3.6.0", 52 | "@types/jest": "^27.4.0", 53 | "@types/node": "^12.11.1", 54 | "@types/semver": "^6.0.1", 55 | "@typescript-eslint/eslint-plugin": "^5.10.2", 56 | "@typescript-eslint/eslint-plugin-tslint": "^5.10.2", 57 | "@typescript-eslint/parser": "^5.10.2", 58 | "angular-http-server": "^1.9.0", 59 | "codelyzer": "^6.0.0", 60 | "eslint": "^8.8.0", 61 | "eslint-config-prettier": "^8.3.0", 62 | "eslint-plugin-import": "^2.25.4", 63 | "husky": "^3.0.4", 64 | "jest": "^29.5.0", 65 | "jest-preset-angular": "^14.1.1", 66 | "lint-staged": "^9.2.5", 67 | "ng-packagr": "^18.0.0", 68 | "prettier": "^1.18.2", 69 | "standard-version": "^8.0.2", 70 | "ts-jest": "^29.1.1", 71 | "ts-node": "^8.3.0", 72 | "typescript": "~5.4.5" 73 | }, 74 | "resolutions": { 75 | "**/**/lodash": "^4.17.19", 76 | "**/**/dot-prop": "^5.1.1" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat-public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The public api for consumers of @ngxs-labs/package 3 | */ 4 | export * from './compat/src/public-api'; 5 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "./../compat-public-api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat/src/lib/ngxs-firestore-compat.adapter.ts: -------------------------------------------------------------------------------- 1 | import { AngularFirestore } from '@angular/fire/compat/firestore'; 2 | import { Inject, Injectable, Optional } from '@angular/core'; 3 | import { NgxsFirestoreModuleOptions, NGXS_FIRESTORE_MODULE_OPTIONS } from '../../../src/lib/tokens'; 4 | import { Store } from '@ngxs/store'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class NgxsFirestoreAdapter { 8 | constructor( 9 | @Inject(AngularFirestore) public firestore: AngularFirestore, 10 | @Inject(Store) public store: Store, 11 | @Optional() @Inject(NGXS_FIRESTORE_MODULE_OPTIONS) public options: NgxsFirestoreModuleOptions 12 | ) {} 13 | } 14 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat/src/lib/ngxs-firestore-compat.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | QueryFn, 3 | QueryDocumentSnapshot, 4 | Action, 5 | DocumentSnapshot, 6 | DocumentChangeAction, 7 | DocumentData, 8 | DocumentReference, 9 | QueryGroupFn 10 | } from '@angular/fire/compat/firestore'; 11 | import { Observable, from, of } from 'rxjs'; 12 | import { Inject, Injectable } from '@angular/core'; 13 | import { map, mapTo, timeoutWith } from 'rxjs/operators'; 14 | import { NgxsFirestoreAdapter } from './ngxs-firestore-compat.adapter'; 15 | import firebase from 'firebase/compat/app'; 16 | import 'firebase/compat/firestore'; 17 | 18 | @Injectable() 19 | export abstract class NgxsFirestore { 20 | constructor(@Inject(NgxsFirestoreAdapter) protected adapter: NgxsFirestoreAdapter) {} 21 | 22 | protected abstract path: string; 23 | protected idField: string = 'id'; 24 | protected metadataField: string | false = false; 25 | protected timeoutWriteOperations: number | false = false; 26 | protected optimisticUpdates: boolean = false; 27 | protected converter: firebase.firestore.FirestoreDataConverter = { 28 | toFirestore: (value) => { 29 | return value as DocumentData; 30 | }, 31 | fromFirestore: (snapshot, options) => { 32 | return { ...(snapshot.data(options)) }; 33 | } 34 | }; 35 | 36 | public createId() { 37 | return this.adapter.firestore.createId(); 38 | } 39 | 40 | public doc$(id: string): Observable { 41 | return this.adapter.firestore 42 | .doc(this.docRef(id) as any) 43 | .snapshotChanges() 44 | .pipe( 45 | map((docSnapshot: Action>) => { 46 | if (docSnapshot.payload.exists) { 47 | return this.getDataWithId(docSnapshot.payload); 48 | } else { 49 | return undefined; 50 | } 51 | }) 52 | ); 53 | } 54 | 55 | public docOnce$( 56 | id: string, 57 | getOptions: firebase.firestore.GetOptions = { source: 'default' } 58 | ): Observable { 59 | return this.adapter.firestore 60 | .doc(this.docRef(id) as any) 61 | .get(getOptions) 62 | .pipe( 63 | map((docSnapshot) => { 64 | if (docSnapshot.exists) { 65 | return this.getDataWithId(docSnapshot); 66 | } else { 67 | return undefined; 68 | } 69 | }) 70 | ); 71 | } 72 | 73 | public collection$(queryFn: QueryFn = (ref) => ref): Observable { 74 | return this.adapter.firestore 75 | .collection(this.path, (ref) => { 76 | return queryFn(ref.withConverter(this.converter as any)); 77 | }) 78 | .snapshotChanges() 79 | .pipe( 80 | map((docSnapshots: DocumentChangeAction[]) => 81 | docSnapshots.map((docSnapshot) => { 82 | return this.getDataWithId(docSnapshot.payload.doc); 83 | }) 84 | ) 85 | ); 86 | } 87 | 88 | public collectionOnce$( 89 | queryFn: QueryFn = (ref) => ref, 90 | getOptions: firebase.firestore.GetOptions = { source: 'default' } 91 | ): Observable { 92 | return this.adapter.firestore 93 | .collection(this.path, (ref) => { 94 | return queryFn(ref.withConverter(this.converter as any)); 95 | }) 96 | .get(getOptions) 97 | .pipe( 98 | map((querySnapshot) => { 99 | const docSnapshots = querySnapshot.docs; 100 | const items = docSnapshots.map((docSnapshot) => { 101 | return this.getDataWithId(docSnapshot); 102 | }); 103 | return items; 104 | }) 105 | ); 106 | } 107 | 108 | public collectionGroup$(queryFn: QueryGroupFn = (ref) => ref): Observable { 109 | return this.adapter.firestore 110 | .collectionGroup(this.path, (ref) => { 111 | return queryFn(ref.withConverter(this.converter) as any) as any; 112 | }) 113 | .snapshotChanges() 114 | .pipe( 115 | map((docSnapshots: DocumentChangeAction[]) => 116 | docSnapshots.map((docSnapshot) => { 117 | const doc = docSnapshot.payload.doc; 118 | const data = doc.data(); 119 | const id = (data && (data)[this.idField]) || doc.id; 120 | if (this.metadataField) { 121 | return ({ ...data, [this.idField]: id, [this.metadataField]: doc.metadata } as unknown) as T; 122 | } else { 123 | return ({ ...data, [this.idField]: id } as unknown) as T; 124 | } 125 | }) 126 | ) 127 | ); 128 | } 129 | 130 | public update$(id: string, value: Partial, setOptions: firebase.firestore.SetOptions = { merge: true }) { 131 | return this.docSet(id, value, setOptions); 132 | } 133 | 134 | public delete$(id: string) { 135 | return from(this.doc(id).delete()).pipe(); 136 | } 137 | 138 | public create$(value: Partial): Observable { 139 | return this.upsert$(value); 140 | } 141 | 142 | public upsert$(value: Partial, setOptions: firebase.firestore.SetOptions = { merge: true }): Observable { 143 | let id; 144 | let newValue; 145 | 146 | if (Object.keys(value).includes(this.idField) && !!(value)[this.idField]) { 147 | id = (value)[this.idField]; 148 | newValue = Object.assign({}, value); 149 | } else { 150 | id = this.createId(); 151 | newValue = Object.assign({}, value, { [this.idField]: id }); 152 | } 153 | 154 | return this.docSet(id, newValue, setOptions); 155 | } 156 | 157 | private getDataWithId(doc: firebase.firestore.QueryDocumentSnapshot | QueryDocumentSnapshot): T { 158 | const data = doc.data(); 159 | const id = (data && (data)[this.idField]) || doc.id; 160 | if (this.metadataField) { 161 | return ({ ...data, [this.idField]: id, [this.metadataField]: doc.metadata } as unknown) as T; 162 | } else { 163 | return ({ ...data, [this.idField]: id } as unknown) as T; 164 | } 165 | } 166 | 167 | private doc(id: string) { 168 | return this.adapter.firestore.doc(this.docRef(id) as DocumentReference); 169 | } 170 | 171 | private docSet(id: string, value: any, setOptions: firebase.firestore.SetOptions = { merge: true }) { 172 | if (this.metadataField) { 173 | delete value[this.metadataField]; 174 | } 175 | 176 | const optimisticUpdates = this.adapter.options?.optimisticUpdates || this.optimisticUpdates; 177 | if (this.isOffline() || optimisticUpdates) { 178 | this.doc(id).set(value, setOptions); 179 | return of(id); 180 | } 181 | 182 | const timeoutWriteOperations = this.adapter.options?.timeoutWriteOperations || this.timeoutWriteOperations; 183 | if (timeoutWriteOperations) { 184 | return from(this.doc(id).set(value, setOptions)).pipe(timeoutWith(timeoutWriteOperations, of(id)), mapTo(id)); 185 | } else { 186 | return from(this.doc(id).set(value, setOptions)).pipe(mapTo(id)); 187 | } 188 | } 189 | 190 | private docRef(id: string) { 191 | return this.adapter.firestore.doc(`${this.path}/${id}`).ref.withConverter(this.converter as any); 192 | } 193 | 194 | private isOffline() { 195 | return navigator.onLine !== undefined && !navigator.onLine; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat/src/lib/ngxs-firestore-page-compat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { defer, Observable } from 'rxjs'; 3 | import { filter, map, startWith, switchMap } from 'rxjs/operators'; 4 | import { Actions, getActionTypeFromInstance, ofActionDispatched } from '@ngxs/store'; 5 | import { GetNextPage, GetLastPage } from '../../../src/lib/actions'; 6 | import { AngularFirestore, QueryFn, FieldPath } from '@angular/fire/compat/firestore'; 7 | import { FirestorePage } from '../../../src/lib/internal-types'; 8 | 9 | type OrderBy = { fieldPath: string | FieldPath; directionStr?: 'desc' | 'asc' }; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class NgxsFirestorePageIdService { 13 | constructor(private firestore: AngularFirestore) {} 14 | 15 | createId() { 16 | return this.firestore.createId(); 17 | } 18 | } 19 | 20 | @Injectable({ providedIn: 'root' }) 21 | export class NgxsFirestorePageService { 22 | constructor(private actions$: Actions, private pageId: NgxsFirestorePageIdService) {} 23 | 24 | create( 25 | queryFn: (pageFn: QueryFn) => Observable, 26 | size: number, 27 | orderBy: OrderBy[] 28 | ): Observable<{ results: T; pageId: string }> { 29 | return defer(() => { 30 | const pages: FirestorePage[] = []; 31 | 32 | return this.actions$.pipe( 33 | ofActionDispatched(GetNextPage, GetLastPage), 34 | startWith('INIT' as 'INIT'), 35 | map((action: 'INIT' | GetNextPage | GetLastPage) => { 36 | const actionType = <'GetNextPage' | 'GetLastPage'>getActionTypeFromInstance(action); 37 | const payload = action === 'INIT' ? this.pageId.createId() : action.payload; 38 | return { payload, actionType: actionType || 'GetNextPage' }; 39 | }), 40 | filter(({ payload, actionType }) => { 41 | return pages.length === 0 || !!pages.find((page) => page.id === payload); 42 | }), 43 | map(({ payload, actionType }) => { 44 | const thePage = pages.find((page) => page.id === payload); 45 | let limit = thePage?.limit || 0; 46 | 47 | if (actionType === 'GetNextPage') { 48 | limit += size; 49 | } else if (limit - size > 0) { 50 | limit -= size; 51 | } 52 | 53 | // firestore max linit is 10000 54 | const skip = thePage?.limit === limit || limit > 10000; 55 | 56 | if (thePage) { 57 | thePage.limit = limit; 58 | } else { 59 | pages.push({ id: payload, limit }); 60 | } 61 | 62 | return { pageId: payload, limit, skip }; 63 | }), 64 | filter(({ skip }) => { 65 | return !skip; 66 | }), 67 | switchMap(({ pageId, limit }) => { 68 | return queryFn((ref) => { 69 | return orderBy 70 | .reduce((prev: any, curr) => { 71 | return prev.orderBy(curr.fieldPath, curr.directionStr || 'asc'); 72 | }, ref) 73 | .limit(limit); 74 | }).pipe( 75 | map((results) => { 76 | return { results, pageId, pageSize: limit }; 77 | }) 78 | ); 79 | }) 80 | ); 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/firestore-plugin/compat/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The public api for consumers of @ngxs-labs/package 3 | */ 4 | export * from './lib/ngxs-firestore-compat.adapter'; 5 | export * from './lib/ngxs-firestore-compat.service'; 6 | export * from './lib/ngxs-firestore-page-compat.service'; 7 | -------------------------------------------------------------------------------- /packages/firestore-plugin/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngxs-firestore", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/firestore-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ngxs-labs/firestore-plugin", 3 | "version": "18.0.5", 4 | "peerDependencies": { 5 | "@ngxs/store": ">=4.0.0 <19.0.0", 6 | "@angular/fire": ">=7.0.0 <19.0.0", 7 | "firebase": ">=9.0.0 <10.0.0" 8 | }, 9 | "homepage": "https://github.com/ngxs-labs/firebase-plugin#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ngxs-labs/firestore-plugin" 13 | }, 14 | "license": "MIT", 15 | "private": false, 16 | "keywords": [ 17 | "firestore", 18 | "ngxs", 19 | "ngxs-labs" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/action-decorator-helpers.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from '@ngxs/store'; 2 | 3 | export function StreamConnected(actionType: ActionType) { 4 | return class { 5 | static readonly type = `${actionType.type} Connected`; 6 | constructor(public action: any) {} 7 | }; 8 | } 9 | 10 | export function StreamEmitted(actionType: ActionType) { 11 | return class { 12 | static readonly type = `${actionType.type} Emitted`; 13 | constructor(public action: any, public payload: any) {} 14 | }; 15 | } 16 | 17 | export function StreamDisconnected(actionType: ActionType) { 18 | return class { 19 | static readonly type = `${actionType.type} Disconnected`; 20 | constructor(public action: any) {} 21 | }; 22 | } 23 | 24 | export function StreamErrored(actionType: ActionType) { 25 | return class { 26 | static readonly type = `${actionType.type} Errored`; 27 | constructor(public action: any, public error: any) {} 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/actions.ts: -------------------------------------------------------------------------------- 1 | export class DisconnectAll { 2 | static readonly type = '[NgxsFirestore] DisconnectAll'; 3 | } 4 | 5 | export class Disconnect { 6 | static readonly type = '[NgxsFirestore] Disconnect'; 7 | constructor(public payload: any) {} 8 | } 9 | 10 | export class GetNextPage { 11 | static readonly type = 'GetNextPage'; 12 | constructor(public payload: string) {} 13 | } 14 | 15 | export class GetLastPage { 16 | static readonly type = 'GetLastPage'; 17 | constructor(public payload: string) {} 18 | } 19 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/attach-action.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionOptions, ActionType, StateContext } from '@ngxs/store'; 2 | 3 | /** 4 | * This key is used to retrieve static metadatas on state classes. 5 | * This constant is taken from the core codebase 6 | */ 7 | const META_OPTIONS_KEY = 'NGXS_OPTIONS_META'; 8 | 9 | export function attachAction( 10 | storeClass: any, 11 | action: ActionType, 12 | fn: (ctx: StateContext, action: A) => any, 13 | options?: ActionOptions 14 | ): void { 15 | if (!storeClass[META_OPTIONS_KEY]) { 16 | throw new Error('storeClass is not a valid NGXS Store'); 17 | } 18 | 19 | const methodName = getActionMethodName(action); 20 | 21 | storeClass.prototype[methodName] = function(_state: any, _action: any): any { 22 | return fn(_state, _action); 23 | }; 24 | 25 | Action(action, options)({ constructor: storeClass }, methodName, null as any); 26 | } 27 | 28 | const getActionMethodName = (action: ActionType) => { 29 | const actionName = action.type.replace(/[^a-zA-Z0-9]+/g, ''); 30 | return `${actionName}`; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/internal-types.ts: -------------------------------------------------------------------------------- 1 | export interface FirestorePage { 2 | limit: number; 3 | id: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore-connect.actions.ts: -------------------------------------------------------------------------------- 1 | namespace NgxsFirestoreDebugPayloads { 2 | export interface StreamEmitted { 3 | id: string; 4 | items: any; 5 | } 6 | } 7 | 8 | export namespace NgxsFirestoreConnectActions { 9 | export class StreamConnected { 10 | static readonly type = '[NgxsFirestore] Connected'; 11 | constructor(public payload: string) {} 12 | } 13 | export class StreamEmitted { 14 | static readonly type = '[NgxsFirestore] Emitted'; 15 | constructor(public payload: NgxsFirestoreDebugPayloads.StreamEmitted) {} 16 | } 17 | export class StreamDisconnected { 18 | static readonly type = '[NgxsFirestore] Disconnected'; 19 | constructor(public payload: string) {} 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore-connect.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnDestroy, Optional } from '@angular/core'; 2 | import { Store, ActionType, Actions, ofActionDispatched } from '@ngxs/store'; 3 | import { tap, catchError, mergeMap, takeUntil, finalize, filter, take, share } from 'rxjs/operators'; 4 | import { Observable, race, Subscription, Subject, defer, of } from 'rxjs'; 5 | import { StreamConnected, StreamEmitted, StreamDisconnected, StreamErrored } from './action-decorator-helpers'; 6 | import { NgxsFirestoreConnectActions } from './ngxs-firestore-connect.actions'; 7 | import { DisconnectAll, Disconnect } from './actions'; 8 | import { attachAction } from './attach-action'; 9 | import { NgxsFirestoreState } from './ngxs-firestore.state'; 10 | import { NgxsFirestoreModuleOptions, NGXS_FIRESTORE_MODULE_OPTIONS } from './tokens'; 11 | 12 | interface ActionTypeDef { 13 | type: string; 14 | new (...args: any): T; 15 | } 16 | 17 | function defaultTrackBy(action: any) { 18 | return ''; 19 | } 20 | 21 | function streamId(opts: { actionType: ActionType; action: any; trackBy: (action: any) => string }) { 22 | let id = `${opts.actionType.type}`; 23 | if (opts.trackBy(opts.action)) { 24 | id = id.concat(` (${opts.trackBy(opts.action)})`); 25 | } 26 | return id; 27 | } 28 | 29 | function tapOnce(fn: (value: any) => void) { 30 | return (source: Observable) => 31 | defer(() => { 32 | let first = true; 33 | return source.pipe( 34 | tap((payload) => { 35 | if (first) { 36 | fn(payload); 37 | } 38 | first = false; 39 | }) 40 | ); 41 | }).pipe(share()); 42 | } 43 | 44 | @Injectable({ providedIn: 'root' }) 45 | export class NgxsFirestoreConnect implements OnDestroy { 46 | private firestoreConnectionsSub: Subscription[] = []; 47 | private activeFirestoreConnections: string[] = []; 48 | private actionsPending: string[] = []; 49 | 50 | constructor( 51 | private store: Store, 52 | private actions: Actions, 53 | @Optional() @Inject(NGXS_FIRESTORE_MODULE_OPTIONS) private options: NgxsFirestoreModuleOptions 54 | ) {} 55 | 56 | /** 57 | * Connect a query that will dispatch a `StreamEmitted` action on each emission. 58 | * 59 | * @param actionType Action to connect with 60 | * @param opts.to Firestore Query to connect with 61 | * @param opts.trackBy used to allow multiple connections for a same action, and Disconnect them individually 62 | * @param opts.connectedActionFinishesOn complete connected action on first emit or stream completed 63 | * @param opts.cancelPrevious cancel previous connected action, 64 | * - false: will NOT cancel connected action, and subsequent dispatches will be skipped 65 | * - true : will cancel previous connected action, if used combined with trackBy will cancel stream with same id 66 | * - 'cancel-if-track-by-changed': will cancel previous connected action only when trackBy changed 67 | */ 68 | connect( 69 | actionType: ActionTypeDef, 70 | opts: { 71 | to: (action: T) => Observable; 72 | trackBy?: (action: T) => string; 73 | connectedActionFinishesOn?: 'FirstEmit' | 'StreamCompleted'; 74 | cancelPrevious?: boolean | 'cancel-if-track-by-changed'; 75 | } 76 | ) { 77 | const connectedActionFinishesOn = opts.connectedActionFinishesOn || 'FirstEmit'; 78 | const trackBy = opts.trackBy || defaultTrackBy; 79 | const cancelPrevious = opts.cancelPrevious || false; 80 | 81 | interface CompletedHandler { 82 | actionCompletedHandlerSubject: Subject; 83 | } 84 | 85 | const subjects: { [key: string]: CompletedHandler } = {}; 86 | function getSubjects(id: string): CompletedHandler { 87 | if (!subjects[id]) { 88 | const actionCompletedHandlerSubject = new Subject(); 89 | subjects[id] = { 90 | actionCompletedHandlerSubject 91 | }; 92 | } 93 | 94 | return subjects[id]; 95 | } 96 | 97 | attachAction(NgxsFirestoreState, actionType, (_stateContext, action) => { 98 | const { actionCompletedHandlerSubject } = getSubjects(streamId({ actionType, action, trackBy })); 99 | 100 | const completed$ = actionCompletedHandlerSubject.asObservable().pipe(take(1)); 101 | 102 | if (cancelPrevious === true) { 103 | return completed$; 104 | } 105 | 106 | if (this.activeFirestoreConnections.includes(streamId({ actionType, action, trackBy }))) { 107 | return; 108 | } 109 | 110 | if (this.actionsPending.includes(streamId({ actionType, action, trackBy }))) { 111 | return completed$; 112 | } 113 | 114 | return completed$; 115 | }); 116 | 117 | const actionDispatched$ = this.actions.pipe( 118 | ofActionDispatched(actionType), 119 | // filter actions not connected already 120 | // or cancelPrevious 121 | filter((action) => { 122 | return ( 123 | cancelPrevious === true || 124 | !this.activeFirestoreConnections.includes(streamId({ actionType, action, trackBy })) 125 | ); 126 | }), 127 | // filter actions dispatched on same tick 128 | filter((action) => { 129 | return cancelPrevious === true || !this.actionsPending.includes(streamId({ actionType, action, trackBy })); 130 | }), 131 | tap((action) => { 132 | this.actionsPending.push(streamId({ actionType, action, trackBy })); 133 | }) 134 | ); 135 | 136 | const firestoreStreamHandler$ = (action: T) => { 137 | const streamFn = opts.to; 138 | return streamFn(action).pipe( 139 | // connected 140 | tapOnce((_) => { 141 | const StreamConnectedClass = StreamConnected(actionType); 142 | this.store.dispatch(new StreamConnectedClass(action)); 143 | this.activeFirestoreConnections.push(streamId({ actionType, action, trackBy })); 144 | // remove from actionsPending once connected 145 | this.actionsPending.splice(this.actionsPending.indexOf(streamId({ actionType, action, trackBy })), 1); 146 | 147 | if (this.options?.developmentMode) { 148 | this.store.dispatch( 149 | new NgxsFirestoreConnectActions.StreamConnected(streamId({ actionType, action, trackBy })) 150 | ); 151 | } 152 | }), 153 | // emmited 154 | tap((payload) => { 155 | const StreamEmittedClass = StreamEmitted(actionType); 156 | this.store.dispatch(new StreamEmittedClass(action, payload)); 157 | 158 | if (this.options?.developmentMode) { 159 | this.store.dispatch( 160 | new NgxsFirestoreConnectActions.StreamEmitted({ 161 | id: streamId({ actionType, action, trackBy }), 162 | items: payload 163 | }) 164 | ); 165 | } 166 | }), 167 | // completed if FirstEmit 168 | tapOnce(() => { 169 | if (connectedActionFinishesOn === 'FirstEmit') { 170 | const { actionCompletedHandlerSubject } = getSubjects(streamId({ actionType, action, trackBy })); 171 | actionCompletedHandlerSubject.next(action); 172 | } 173 | }), 174 | // disconnect on Disconnect 175 | takeUntil( 176 | race( 177 | this.actions.pipe(ofActionDispatched(DisconnectAll)), 178 | this.actions.pipe(ofActionDispatched(Disconnect)).pipe( 179 | filter((disconnectAction) => { 180 | const { payload } = disconnectAction; 181 | if (!payload) { 182 | return false; 183 | } 184 | const disconnectActionStreamId = streamId({ 185 | actionType: payload.constructor?.type ? payload.constructor : payload, 186 | action: disconnectAction.payload, 187 | trackBy 188 | }); 189 | const connectedActionStreamId = streamId({ actionType, action, trackBy }); 190 | if ( 191 | disconnectActionStreamId === connectedActionStreamId || 192 | // will disconnect all matching actions 193 | // valid when we want to disconnect multiple actions that were 194 | // dispatched with a payload 195 | connectedActionStreamId.startsWith(disconnectActionStreamId) 196 | ) { 197 | return true; 198 | } 199 | 200 | return false; 201 | }) 202 | ) 203 | ) 204 | ), 205 | // disconnect on action re-dispatched 206 | takeUntil( 207 | this.actions.pipe( 208 | ofActionDispatched(actionType), 209 | filter((dispatchedAction) => { 210 | if (cancelPrevious === false) { 211 | return false; 212 | } 213 | //SELF 214 | if (dispatchedAction === action) { 215 | return false; 216 | } 217 | const dispatchedActionStreamId = streamId({ 218 | actionType, 219 | action: dispatchedAction, 220 | trackBy 221 | }); 222 | 223 | if (cancelPrevious === true) { 224 | return dispatchedActionStreamId === streamId({ actionType, action, trackBy }); 225 | } else if (cancelPrevious === 'cancel-if-track-by-changed') { 226 | return dispatchedActionStreamId !== streamId({ actionType, action, trackBy }); 227 | } else { 228 | return false; 229 | } 230 | }) 231 | ) 232 | ), 233 | finalize(() => { 234 | const StreamDisconnectedClass = StreamDisconnected(actionType); 235 | this.store.dispatch(new StreamDisconnectedClass(action)); 236 | 237 | if (this.options?.developmentMode) { 238 | this.store.dispatch( 239 | new NgxsFirestoreConnectActions.StreamDisconnected(streamId({ actionType, action, trackBy })) 240 | ); 241 | } 242 | this.activeFirestoreConnections.splice( 243 | this.activeFirestoreConnections.indexOf(streamId({ actionType, action, trackBy })), 244 | 1 245 | ); 246 | 247 | // completed if StreamCompleted 248 | if (connectedActionFinishesOn === 'StreamCompleted') { 249 | const { actionCompletedHandlerSubject } = getSubjects(streamId({ actionType, action, trackBy })); 250 | actionCompletedHandlerSubject.next(action); 251 | } 252 | }), 253 | catchError((err) => { 254 | const { actionCompletedHandlerSubject } = getSubjects(streamId({ actionType, action, trackBy })); 255 | actionCompletedHandlerSubject.error(err); 256 | 257 | const StreamErroredClass = StreamErrored(actionType); 258 | this.store.dispatch(new StreamErroredClass(action, err)); 259 | 260 | return of({}); 261 | }) 262 | ); 263 | }; 264 | 265 | this.firestoreConnectionsSub.push(actionDispatched$.pipe(mergeMap(firestoreStreamHandler$)).subscribe()); 266 | } 267 | 268 | ngOnDestroy() { 269 | if (this.firestoreConnectionsSub) { 270 | this.firestoreConnectionsSub.forEach((sub) => sub.unsubscribe()); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore-connections.selector.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@ngxs/store'; 2 | import { NgxsFirestoreState, NgxsFirestoreStateModel } from './ngxs-firestore.state'; 3 | 4 | export const ngxsFirestoreConnections = createSelector([NgxsFirestoreState], (state: NgxsFirestoreStateModel) => { 5 | return state.connections; 6 | }); 7 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore-page.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { fakeAsync, TestBed, tick } from '@angular/core/testing'; 3 | import { doc, DocumentReference, Firestore } from '@angular/fire/firestore'; 4 | import { Action, NgxsModule, NgxsOnInit, Selector, State, StateContext, Store } from '@ngxs/store'; 5 | import { patch } from '@ngxs/store/operators'; 6 | import { BehaviorSubject } from 'rxjs'; 7 | import { StreamEmitted } from './action-decorator-helpers'; 8 | import { GetLastPage, GetNextPage } from './actions'; 9 | import { NgxsFirestoreConnect } from './ngxs-firestore-connect.service'; 10 | import { NgxsFirestorePageService } from './ngxs-firestore-page.service'; 11 | import { NgxsFirestoreModule } from './ngxs-firestore.module'; 12 | import { Emitted, Page } from './types'; 13 | 14 | jest.mock('@angular/fire/firestore'); 15 | 16 | describe('NgxsFirestorePage', () => { 17 | let store: Store; 18 | 19 | const mockFirestoreStream = jest.fn(); 20 | const mockCreateId = jest.fn(); 21 | const mockDoc = jest.mocked(doc); 22 | mockDoc.mockImplementation( 23 | () => (({ id: mockCreateId(), withConverter: jest.fn() } as unknown) as DocumentReference) 24 | ); 25 | 26 | class TestActionGetPages { 27 | static type = 'TEST ACTION GET PAGES'; 28 | } 29 | 30 | class AnotherTestActionGetPages { 31 | static type = 'ANOTHER TEST ACTION GET PAGES'; 32 | } 33 | 34 | class MaxPageSizeTestActionGetPages { 35 | static type = 'MAX PAGE SIZE TEST ACTION GET PAGES'; 36 | } 37 | 38 | type TestStateModel = { 39 | pageId: string; 40 | pageSize: number; 41 | results: string[]; 42 | }; 43 | 44 | @State({ 45 | name: 'test' 46 | }) 47 | @Injectable() 48 | class TestState implements NgxsOnInit { 49 | @Selector() static pageId(state: TestStateModel) { 50 | return state.pageId; 51 | } 52 | 53 | @Selector() static pageSize(state: TestStateModel) { 54 | return state.pageSize; 55 | } 56 | 57 | @Selector() static results(state: TestStateModel) { 58 | return state.results; 59 | } 60 | 61 | constructor( 62 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 63 | private ngxsFirestorePage: NgxsFirestorePageService 64 | ) {} 65 | 66 | ngxsOnInit() { 67 | this.ngxsFirestoreConnect.connect(TestActionGetPages, { 68 | to: () => 69 | this.ngxsFirestorePage.create((pageFn) => mockFirestoreStream((ref: any) => pageFn(ref)), 5, [ 70 | { fieldPath: 'title' } 71 | ]) 72 | }); 73 | } 74 | 75 | @Action(StreamEmitted(TestActionGetPages)) 76 | getPageEmitted(ctx: StateContext, { action, payload }: Emitted>) { 77 | ctx.setState(patch({ results: payload.results || [], pageId: payload.pageId, pageSize: payload.pageSize })); 78 | } 79 | } 80 | 81 | type AnotherTestStateModel = TestStateModel; 82 | 83 | @State({ 84 | name: 'another_test' 85 | }) 86 | @Injectable() 87 | class AnotherTestState implements NgxsOnInit { 88 | @Selector() static pageId(state: AnotherTestStateModel) { 89 | return state.pageId; 90 | } 91 | 92 | @Selector() static pageSize(state: AnotherTestStateModel) { 93 | return state.pageSize; 94 | } 95 | 96 | @Selector() static results(state: AnotherTestStateModel) { 97 | return state.results; 98 | } 99 | 100 | constructor( 101 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 102 | private ngxsFirestorePage: NgxsFirestorePageService 103 | ) {} 104 | 105 | ngxsOnInit() { 106 | this.ngxsFirestoreConnect.connect(AnotherTestActionGetPages, { 107 | to: () => 108 | this.ngxsFirestorePage.create((pageFn) => mockFirestoreStream((ref: any) => pageFn(ref)), 5, [ 109 | { fieldPath: 'title' } 110 | ]) 111 | }); 112 | } 113 | 114 | @Action(StreamEmitted(AnotherTestActionGetPages)) 115 | getPageEmitted( 116 | ctx: StateContext, 117 | { action, payload }: Emitted> 118 | ) { 119 | ctx.setState(patch({ results: payload.results || [], pageId: payload.pageId, pageSize: payload.pageSize })); 120 | } 121 | } 122 | 123 | type MaxPageSizeTestStateModel = TestStateModel; 124 | 125 | @State({ 126 | name: 'max_page_size_test' 127 | }) 128 | @Injectable() 129 | class MaxPageSizeTestState implements NgxsOnInit { 130 | @Selector() static pageId(state: MaxPageSizeTestStateModel) { 131 | return state.pageId; 132 | } 133 | 134 | @Selector() static pageSize(state: MaxPageSizeTestStateModel) { 135 | return state.pageSize; 136 | } 137 | 138 | @Selector() static results(state: MaxPageSizeTestStateModel) { 139 | return state.results; 140 | } 141 | 142 | constructor( 143 | private ngxsFirestoreConnect: NgxsFirestoreConnect, 144 | private ngxsFirestorePage: NgxsFirestorePageService 145 | ) {} 146 | 147 | ngxsOnInit() { 148 | this.ngxsFirestoreConnect.connect(MaxPageSizeTestActionGetPages, { 149 | to: () => 150 | this.ngxsFirestorePage.create((pageFn) => mockFirestoreStream((ref: any) => pageFn(ref)), 5000, [ 151 | { fieldPath: 'title' } 152 | ]) 153 | }); 154 | } 155 | 156 | @Action(StreamEmitted(MaxPageSizeTestActionGetPages)) 157 | getPageEmitted( 158 | ctx: StateContext, 159 | { action, payload }: Emitted> 160 | ) { 161 | ctx.setState(patch({ results: payload.results || [], pageId: payload.pageId, pageSize: payload.pageSize })); 162 | } 163 | } 164 | 165 | const page1 = ['1', '2', '3']; 166 | // const page2 = ['1', '2', '3', '4', '5', '6']; 167 | // const page3 = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; 168 | 169 | beforeEach(() => { 170 | TestBed.configureTestingModule({ 171 | imports: [NgxsModule.forRoot([TestState, AnotherTestState, MaxPageSizeTestState]), NgxsFirestoreModule.forRoot()], 172 | providers: [{ provide: Firestore, useValue: jest.fn() }] 173 | }); 174 | store = TestBed.inject(Store); 175 | // actions = TestBed.inject(Actions); 176 | 177 | mockFirestoreStream.mockClear(); 178 | }); 179 | 180 | test('should increase pageSize on each getnextpage', fakeAsync(() => { 181 | mockCreateId.mockReturnValue('pageId'); 182 | 183 | const stream = new BehaviorSubject(page1); 184 | mockFirestoreStream.mockReturnValue(stream.asObservable()); 185 | 186 | store.dispatch(new TestActionGetPages()).subscribe((_) => {}); 187 | tick(1); 188 | 189 | expect(store.selectSnapshot(TestState.results)).toEqual(page1); 190 | expect(mockFirestoreStream).toHaveBeenCalledTimes(1); 191 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 192 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 193 | 194 | store.dispatch(new GetNextPage('pageId')).subscribe((_) => {}); 195 | tick(1); 196 | expect(mockFirestoreStream).toHaveBeenCalledTimes(2); 197 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 198 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(10); 199 | 200 | store.dispatch(new GetNextPage('pageId')).subscribe((_) => {}); 201 | tick(1); 202 | expect(mockFirestoreStream).toHaveBeenCalledTimes(3); 203 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 204 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(15); 205 | })); 206 | 207 | test('should not allow lastpage until at least two pages have been fetched', fakeAsync(() => { 208 | mockCreateId.mockReturnValue('pageId'); 209 | 210 | const stream = new BehaviorSubject(page1); 211 | mockFirestoreStream.mockReturnValue(stream.asObservable()); 212 | 213 | store.dispatch(new TestActionGetPages()).subscribe((_) => {}); 214 | tick(1); 215 | 216 | expect(mockFirestoreStream).toHaveBeenCalledTimes(1); 217 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 218 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 219 | 220 | store.dispatch(new GetLastPage('pageId')).subscribe((_) => {}); 221 | tick(1); 222 | 223 | expect(mockFirestoreStream).toHaveBeenCalledTimes(1); 224 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 225 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 226 | 227 | store.dispatch(new GetNextPage('pageId')).subscribe((_) => {}); 228 | tick(1); 229 | 230 | expect(mockFirestoreStream).toHaveBeenCalledTimes(2); 231 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 232 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(10); 233 | 234 | store.dispatch(new GetLastPage('pageId')).subscribe((_) => {}); 235 | tick(1); 236 | 237 | expect(mockFirestoreStream).toHaveBeenCalledTimes(3); 238 | expect(store.selectSnapshot(TestState.pageId)).toEqual('pageId'); 239 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 240 | })); 241 | 242 | test('should skip if page > 10000', fakeAsync(() => { 243 | mockCreateId.mockReturnValue('pageId'); 244 | 245 | const stream = new BehaviorSubject(page1); 246 | mockFirestoreStream.mockReturnValue(stream.asObservable()); 247 | 248 | store.dispatch(new MaxPageSizeTestActionGetPages()).subscribe((_) => {}); 249 | tick(1); 250 | 251 | expect(mockFirestoreStream).toHaveBeenCalledTimes(1); 252 | expect(store.selectSnapshot(MaxPageSizeTestState.pageId)).toEqual('pageId'); 253 | expect(store.selectSnapshot(MaxPageSizeTestState.pageSize)).toEqual(5000); 254 | 255 | store.dispatch(new GetNextPage('pageId')).subscribe((_) => {}); 256 | tick(1); 257 | 258 | expect(mockFirestoreStream).toHaveBeenCalledTimes(2); 259 | expect(store.selectSnapshot(MaxPageSizeTestState.pageId)).toEqual('pageId'); 260 | expect(store.selectSnapshot(MaxPageSizeTestState.pageSize)).toEqual(10000); 261 | 262 | store.dispatch(new GetNextPage('pageId')).subscribe((_) => {}); 263 | tick(1); 264 | 265 | expect(mockFirestoreStream).toHaveBeenCalledTimes(2); 266 | expect(store.selectSnapshot(MaxPageSizeTestState.pageId)).toEqual('pageId'); 267 | expect(store.selectSnapshot(MaxPageSizeTestState.pageSize)).toEqual(10000); 268 | })); 269 | 270 | test('should support multiple page connections', fakeAsync(() => { 271 | mockCreateId.mockReturnValueOnce('firstId').mockReturnValueOnce('secondId'); 272 | 273 | const stream = new BehaviorSubject(page1); 274 | mockFirestoreStream.mockReturnValue(stream.asObservable()); 275 | 276 | store.dispatch(new TestActionGetPages()).subscribe((_) => {}); 277 | tick(1); 278 | expect(store.selectSnapshot(TestState.pageId)).toEqual('firstId'); 279 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 280 | expect(store.selectSnapshot(AnotherTestState.pageId)).toBeUndefined(); 281 | expect(store.selectSnapshot(AnotherTestState.pageSize)).toBeUndefined(); 282 | 283 | store.dispatch(new AnotherTestActionGetPages()).subscribe((_) => {}); 284 | tick(1); 285 | expect(store.selectSnapshot(TestState.pageId)).toEqual('firstId'); 286 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(5); 287 | expect(store.selectSnapshot(AnotherTestState.pageId)).toEqual('secondId'); 288 | expect(store.selectSnapshot(AnotherTestState.pageSize)).toEqual(5); 289 | 290 | store.dispatch(new GetNextPage('firstId')).subscribe((_) => {}); 291 | tick(1); 292 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(10); 293 | expect(store.selectSnapshot(AnotherTestState.pageSize)).toEqual(5); 294 | 295 | store.dispatch(new GetNextPage('secondId')).subscribe((_) => {}); 296 | tick(1); 297 | expect(store.selectSnapshot(TestState.pageSize)).toEqual(10); 298 | expect(store.selectSnapshot(AnotherTestState.pageSize)).toEqual(10); 299 | })); 300 | }); 301 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore-page.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { defer, Observable } from 'rxjs'; 3 | import { filter, map, startWith, switchMap } from 'rxjs/operators'; 4 | import { Actions, getActionTypeFromInstance, ofActionDispatched } from '@ngxs/store'; 5 | import { FieldPath, Firestore, limit as limitFn, orderBy as orderByFn, query } from '@angular/fire/firestore'; 6 | import { GetNextPage, GetLastPage } from './actions'; 7 | import { createId } from './ngxs-firestore.service'; 8 | import { QueryFn } from './utils'; 9 | import { FirestorePage } from './internal-types'; 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class NgxsFirestorePageIdService { 13 | constructor(private firestore: Firestore) {} 14 | 15 | createId() { 16 | return createId(this.firestore); 17 | } 18 | } 19 | 20 | @Injectable({ providedIn: 'root' }) 21 | export class NgxsFirestorePageService { 22 | constructor(private actions$: Actions, private pageId: NgxsFirestorePageIdService) {} 23 | 24 | create( 25 | queryFn: (pageFn: QueryFn) => Observable, 26 | size: number, 27 | orderBy: { fieldPath: string | FieldPath; directionStr?: 'desc' | 'asc' }[] 28 | ): Observable<{ results: T; pageId: string }> { 29 | return defer(() => { 30 | const pages: FirestorePage[] = []; 31 | 32 | return this.actions$.pipe( 33 | ofActionDispatched(GetNextPage, GetLastPage), 34 | startWith('INIT' as 'INIT'), 35 | map((action: 'INIT' | GetNextPage | GetLastPage) => { 36 | const actionType = <'GetNextPage' | 'GetLastPage'>getActionTypeFromInstance(action); 37 | const payload = action === 'INIT' ? this.pageId.createId() : action.payload; 38 | return { payload, actionType: actionType || 'GetNextPage' }; 39 | }), 40 | filter(({ payload, actionType }) => { 41 | return pages.length === 0 || !!pages.find((page) => page.id === payload); 42 | }), 43 | map(({ payload, actionType }) => { 44 | const thePage = pages.find((page) => page.id === payload); 45 | let limit = thePage?.limit || 0; 46 | 47 | if (actionType === 'GetNextPage') { 48 | limit += size; 49 | } else if (limit - size > 0) { 50 | limit -= size; 51 | } 52 | 53 | // firestore max linit is 10000 54 | const skip = thePage?.limit === limit || limit > 10000; 55 | 56 | if (thePage) { 57 | thePage.limit = limit; 58 | } else { 59 | pages.push({ id: payload, limit }); 60 | } 61 | 62 | return { pageId: payload, limit, skip }; 63 | }), 64 | filter(({ skip }) => { 65 | return !skip; 66 | }), 67 | switchMap(({ pageId, limit }) => { 68 | return queryFn((ref) => { 69 | return orderBy.reduce( 70 | (prev, curr) => query(prev, orderByFn(curr.fieldPath, curr.directionStr || 'asc'), limitFn(limit)), 71 | ref 72 | ); 73 | }).pipe( 74 | map((results) => { 75 | return { results, pageId, pageSize: limit }; 76 | }) 77 | ); 78 | }) 79 | ); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.adapter.ts: -------------------------------------------------------------------------------- 1 | import { Firestore } from '@angular/fire/firestore'; 2 | import { Inject, Injectable, Optional } from '@angular/core'; 3 | import { NgxsFirestoreModuleOptions, NGXS_FIRESTORE_MODULE_OPTIONS } from './tokens'; 4 | import { Store } from '@ngxs/store'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class NgxsFirestoreAdapter { 8 | constructor( 9 | @Inject(Firestore) public firestore: Firestore, 10 | @Inject(Store) public store: Store, 11 | @Optional() @Inject(NGXS_FIRESTORE_MODULE_OPTIONS) public options: NgxsFirestoreModuleOptions 12 | ) {} 13 | } 14 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { NgxsFirestoreModule } from './ngxs-firestore.module'; 3 | 4 | describe('NgxsFirestoreModule', () => { 5 | beforeEach( 6 | waitForAsync(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [NgxsFirestoreModule] 9 | }); 10 | }) 11 | ); 12 | 13 | it('should create', () => { 14 | expect(NgxsFirestoreModule).toBeDefined(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxsFirestoreState } from './ngxs-firestore.state'; 5 | import { NgxsFirestoreModuleOptions, NGXS_FIRESTORE_MODULE_OPTIONS } from './tokens'; 6 | 7 | @NgModule({ 8 | imports: [CommonModule, NgxsModule.forFeature([NgxsFirestoreState])] 9 | }) 10 | export class NgxsFirestoreModule { 11 | public static forRoot(options?: Partial): ModuleWithProviders { 12 | return { 13 | ngModule: NgxsFirestoreModule, 14 | providers: [ 15 | { 16 | provide: NGXS_FIRESTORE_MODULE_OPTIONS, 17 | useValue: { timeoutWriteOperations: false, developmentMode: false, ...options } 18 | } 19 | ] 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgxsFirestore } from './ngxs-firestore.service'; 3 | import { Firestore, doc, DocumentReference, setDoc } from '@angular/fire/firestore'; 4 | import { Store } from '@ngxs/store'; 5 | import { Injectable } from '@angular/core'; 6 | 7 | jest.mock('@angular/fire/firestore'); 8 | 9 | describe('NgxsFirestore', () => { 10 | const createIdMock = jest.fn(); 11 | const mockDoc = jest.mocked(doc); 12 | mockDoc.mockImplementation( 13 | () => (({ id: createIdMock(), withConverter: jest.fn() } as unknown) as DocumentReference) 14 | ); 15 | jest.mocked(setDoc).mockResolvedValue(); 16 | 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | providers: [ 20 | { provide: Firestore, useValue: jest.fn() }, 21 | { provide: Store, useValue: jest.fn() } 22 | ] 23 | }); 24 | }); 25 | 26 | it('cant be directly instantiated', () => { 27 | expect(() => { 28 | TestBed.inject(NgxsFirestore); 29 | }).toThrowError('No provider for NgxsFirestore!'); 30 | }); 31 | 32 | it('can be implemented and instantiated', () => { 33 | @Injectable({ providedIn: 'root' }) 34 | class TestFirestore extends NgxsFirestore<{}> { 35 | protected path = 'test'; 36 | } 37 | 38 | expect(TestBed.inject(TestFirestore)).toBeTruthy(); 39 | }); 40 | 41 | describe('', () => { 42 | @Injectable({ providedIn: 'root' }) 43 | class ImplFirestore extends NgxsFirestore<{}> { 44 | protected path = 'impl'; 45 | } 46 | 47 | describe('create$', () => { 48 | it('should create id if not provided', async () => { 49 | createIdMock.mockReturnValue('newId'); 50 | const service: ImplFirestore = TestBed.inject(ImplFirestore); 51 | const id = await service.create$({}).toPromise(); 52 | expect(id).toEqual('newId'); 53 | }); 54 | 55 | it('should return id when provided', async () => { 56 | const service: ImplFirestore = TestBed.inject(ImplFirestore); 57 | const id = await service.create$({ id: 'someid' }).toPromise(); 58 | expect(id).toEqual('someid'); 59 | }); 60 | }); 61 | 62 | describe('upsert$', () => { 63 | it('should create id if not provided', async () => { 64 | createIdMock.mockReturnValue('newId'); 65 | const service: ImplFirestore = TestBed.inject(ImplFirestore); 66 | const id = await service.upsert$({}).toPromise(); 67 | expect(id).toEqual('newId'); 68 | }); 69 | 70 | it('should return id when provided', async () => { 71 | const service: ImplFirestore = TestBed.inject(ImplFirestore); 72 | const id = await service.upsert$({ id: 'someid' }).toPromise(); 73 | expect(id).toEqual('someid'); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | collection, 3 | collectionSnapshots, 4 | collectionGroup, 5 | deleteDoc, 6 | doc, 7 | docSnapshots, 8 | FirestoreDataConverter, 9 | getDoc, 10 | getDocFromCache, 11 | getDocFromServer, 12 | getDocs, 13 | getDocsFromCache, 14 | getDocsFromServer, 15 | QueryDocumentSnapshot, 16 | setDoc, 17 | SetOptions, 18 | Firestore, 19 | DocumentData 20 | } from '@angular/fire/firestore'; 21 | import { Observable, from, of } from 'rxjs'; 22 | import { Inject, Injectable } from '@angular/core'; 23 | import { map, mapTo, timeoutWith } from 'rxjs/operators'; 24 | import { NgxsFirestoreAdapter } from './ngxs-firestore.adapter'; 25 | import { QueryFn } from './utils'; 26 | 27 | interface GetOptions { 28 | source: 'default' | 'server' | 'cache'; 29 | } 30 | 31 | export function createId(firestore: Firestore) { 32 | // https://github.com/angular/angularfire/discussions/2900#discussioncomment-1343797 33 | return doc(collection(firestore, '_')).id; 34 | } 35 | 36 | @Injectable() 37 | export abstract class NgxsFirestore { 38 | constructor(@Inject(NgxsFirestoreAdapter) protected adapter: NgxsFirestoreAdapter) {} 39 | 40 | protected abstract path: string; 41 | protected idField = 'id'; 42 | protected metadataField: string | false = false; 43 | protected timeoutWriteOperations: number | false = false; 44 | protected optimisticUpdates: boolean = false; 45 | protected converter: FirestoreDataConverter = { 46 | toFirestore: (value) => { 47 | return value as DocumentData; 48 | }, 49 | fromFirestore: (snapshot, options) => { 50 | return { ...(snapshot.data(options)) }; 51 | } 52 | }; 53 | 54 | public createId() { 55 | return createId(this.adapter.firestore); 56 | } 57 | 58 | public doc$(id: string): Observable { 59 | return docSnapshots(this.docRef(id)).pipe( 60 | map((docSnapshot) => { 61 | if (docSnapshot.exists()) { 62 | return this.getDataWithId(docSnapshot); 63 | } else { 64 | return undefined; 65 | } 66 | }) 67 | ); 68 | } 69 | 70 | public docOnce$(id: string, { source }: GetOptions = { source: 'default' }): Observable { 71 | const getDocFn = source === 'cache' ? getDocFromCache : source === 'server' ? getDocFromServer : getDoc; 72 | return from(getDocFn(this.docRef(id))).pipe( 73 | map((docSnapshot) => { 74 | if (docSnapshot.exists()) { 75 | return this.getDataWithId(docSnapshot); 76 | } else { 77 | return undefined; 78 | } 79 | }) 80 | ); 81 | } 82 | 83 | public collection$(queryFn: QueryFn = (ref) => ref): Observable { 84 | return collectionSnapshots(queryFn(this.collectionRef())).pipe( 85 | map((queryDocumentSnapshots) => 86 | queryDocumentSnapshots.map((queryDocumentSnapshot) => this.getDataWithId(queryDocumentSnapshot)) 87 | ) 88 | ); 89 | } 90 | 91 | public collectionGroup$(queryFn: QueryFn = (ref) => ref): Observable { 92 | const collectionGroupQuery = queryFn(collectionGroup(this.adapter.firestore, this.path) as any); 93 | return from(getDocs(collectionGroupQuery)).pipe( 94 | map((querySnapshot) => { 95 | const docSnapshots = querySnapshot.docs; 96 | const items = docSnapshots.map((docSnapshot) => { 97 | return this.getDataWithId(docSnapshot) as T; 98 | }); 99 | return items; 100 | }) 101 | ); 102 | } 103 | 104 | public collectionOnce$( 105 | queryFn: QueryFn = (ref) => ref, 106 | { source }: GetOptions = { source: 'default' } 107 | ): Observable { 108 | const getDocsFn = source === 'cache' ? getDocsFromCache : source === 'server' ? getDocsFromServer : getDocs; 109 | return from(getDocsFn(queryFn(collection(this.adapter.firestore, this.path).withConverter(this.converter)))).pipe( 110 | map((querySnapshot) => { 111 | const docSnapshots = querySnapshot.docs; 112 | const items = docSnapshots.map((docSnapshot) => { 113 | return this.getDataWithId(docSnapshot); 114 | }); 115 | return items; 116 | }) 117 | ); 118 | } 119 | 120 | public update$(id: string, value: Partial, setOptions: SetOptions = { merge: true }): Observable { 121 | return this.docSet(id, value, setOptions); 122 | } 123 | 124 | public delete$(id: string): Observable { 125 | return from(deleteDoc(this.docRef(id))); 126 | } 127 | 128 | public create$(value: Partial): Observable { 129 | return this.upsert$(value); 130 | } 131 | 132 | public upsert$(value: Partial, setOptions: SetOptions = { merge: true }): Observable { 133 | let id; 134 | let newValue; 135 | 136 | if (Object.keys(value).includes(this.idField) && !!(value)[this.idField]) { 137 | id = (value)[this.idField]; 138 | newValue = Object.assign({}, value); 139 | } else { 140 | id = this.createId(); 141 | newValue = Object.assign({}, value, { [this.idField]: id }); 142 | } 143 | 144 | return this.docSet(id, newValue, setOptions); 145 | } 146 | 147 | private getDataWithId(doc: QueryDocumentSnapshot) { 148 | const data = doc.data(); 149 | const id = (data && (data)[this.idField]) || doc.id; 150 | if (this.metadataField) { 151 | return { ...data, [this.idField]: id, [this.metadataField]: doc.metadata }; 152 | } else { 153 | return { ...data, [this.idField]: id }; 154 | } 155 | } 156 | 157 | private docSet(id: string, value: any, setOptions: SetOptions = { merge: true }) { 158 | if (this.metadataField) { 159 | delete value[this.metadataField]; 160 | } 161 | 162 | const optimisticUpdates = this.adapter.options?.optimisticUpdates || this.optimisticUpdates; 163 | if (this.isOffline() || optimisticUpdates) { 164 | setDoc(this.docRef(id), value, {}); 165 | return of(id); 166 | } 167 | 168 | const timeoutWriteOperations = this.adapter.options?.timeoutWriteOperations || this.timeoutWriteOperations; 169 | if (timeoutWriteOperations) { 170 | return from(setDoc(this.docRef(id), value, setOptions)).pipe( 171 | timeoutWith(timeoutWriteOperations, of(id)), 172 | mapTo(id) 173 | ); 174 | } else { 175 | return from(setDoc(this.docRef(id), value, setOptions)).pipe(mapTo(id)); 176 | } 177 | } 178 | 179 | private docRef(id: string) { 180 | return doc(this.adapter.firestore, `${this.path}/${id}`).withConverter(this.converter); 181 | } 182 | 183 | private collectionRef() { 184 | return collection(this.adapter.firestore, `${this.path}`).withConverter(this.converter); 185 | } 186 | 187 | private isOffline() { 188 | return navigator.onLine !== undefined && !navigator.onLine; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.standalone.ts: -------------------------------------------------------------------------------- 1 | import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; 2 | import { NGXS_FIRESTORE_MODULE_OPTIONS, NgxsFirestoreModuleOptions } from './tokens'; 3 | 4 | export function provideNgxsFirestore(options?: Partial): EnvironmentProviders { 5 | return makeEnvironmentProviders([ 6 | { 7 | provide: NGXS_FIRESTORE_MODULE_OPTIONS, 8 | useValue: { timeoutWriteOperations: false, developmentMode: false, ...options } 9 | } 10 | ]); 11 | } 12 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.state.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgxsModule, Store } from '@ngxs/store'; 3 | import { NgxsFirestoreModule } from './ngxs-firestore.module'; 4 | import { ngxsFirestoreConnections } from './ngxs-firestore-connections.selector'; 5 | import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; 6 | import { getFirestore, provideFirestore } from '@angular/fire/firestore'; 7 | 8 | describe('NGXS Firestore State', () => { 9 | let store: Store; 10 | 11 | beforeAll(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [NgxsModule.forRoot([]), NgxsFirestoreModule.forRoot()], 14 | providers: [provideFirebaseApp(() => initializeApp({})), provideFirestore(() => getFirestore())] 15 | }); 16 | 17 | store = TestBed.inject(Store); 18 | }); 19 | 20 | test('State exists', () => { 21 | expect(store.selectSnapshot(ngxsFirestoreConnections)).toBeTruthy(); 22 | expect(store.selectSnapshot(ngxsFirestoreConnections)).toEqual([]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/ngxs-firestore.state.ts: -------------------------------------------------------------------------------- 1 | import { State, StateContext, NgxsOnInit, Action } from '@ngxs/store'; 2 | import { NgxsFirestoreConnectActions } from './ngxs-firestore-connect.actions'; 3 | import { patch, insertItem, removeItem, updateItem } from '@ngxs/store/operators'; 4 | import { Injectable } from '@angular/core'; 5 | 6 | export interface FirestoreConnection { 7 | id: string; 8 | connectedAt: Date; 9 | emmitedAt: Date[]; 10 | } 11 | 12 | export interface NgxsFirestoreStateModel { 13 | connections: FirestoreConnection[]; 14 | } 15 | 16 | @State({ 17 | name: 'ngxs_firestore', 18 | defaults: { 19 | connections: [] 20 | } 21 | }) 22 | @Injectable() 23 | export class NgxsFirestoreState implements NgxsOnInit { 24 | ngxsOnInit(_ctx: StateContext) {} 25 | 26 | @Action([NgxsFirestoreConnectActions.StreamConnected]) 27 | streamConnected( 28 | { setState }: StateContext, 29 | { payload }: NgxsFirestoreConnectActions.StreamConnected 30 | ) { 31 | const conn = { 32 | connectedAt: new Date(), 33 | id: payload 34 | } as FirestoreConnection; 35 | setState(patch({ connections: insertItem(conn) })); 36 | } 37 | 38 | @Action([NgxsFirestoreConnectActions.StreamEmitted]) 39 | streamEmitted( 40 | { setState }: StateContext, 41 | { payload }: NgxsFirestoreConnectActions.StreamEmitted 42 | ) { 43 | const { id } = payload; 44 | setState( 45 | patch({ 46 | connections: updateItem((x) => x.id === id, patch({ emmitedAt: insertItem(new Date()) })) 47 | }) 48 | ); 49 | } 50 | 51 | @Action([NgxsFirestoreConnectActions.StreamDisconnected]) 52 | streamDisconnected( 53 | { setState, getState }: StateContext, 54 | { payload }: NgxsFirestoreConnectActions.StreamDisconnected 55 | ) { 56 | setState( 57 | patch({ connections: removeItem((x) => x.id === payload) }) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export interface NgxsFirestoreModuleOptions { 4 | timeoutWriteOperations: number | false; 5 | developmentMode?: boolean; 6 | optimisticUpdates?: boolean; 7 | } 8 | 9 | export const NGXS_FIRESTORE_MODULE_OPTIONS = new InjectionToken( 10 | 'NGXS_FIRESTORE_MODULE_OPTIONS' 11 | ); 12 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Connected { 2 | action: T; 3 | } 4 | export interface Emitted { 5 | action: T; 6 | payload: U; 7 | } 8 | export interface Disconnected { 9 | action: T; 10 | } 11 | 12 | export interface Errored { 13 | action: T; 14 | error: any; 15 | } 16 | 17 | export interface Page { 18 | results: T[]; 19 | pageId: string; 20 | pageSize: number; 21 | } 22 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { Query, DocumentData } from '@angular/fire/firestore'; 2 | 3 | export type QueryFn = (ref: Query) => Query; 4 | -------------------------------------------------------------------------------- /packages/firestore-plugin/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/ngxs-firestore.module'; 2 | export * from './lib/ngxs-firestore.standalone'; 3 | export * from './lib/ngxs-firestore.service'; 4 | export * from './lib/ngxs-firestore-page.service'; 5 | export * from './lib/ngxs-firestore-connect.service'; 6 | export * from './lib/ngxs-firestore-connections.selector'; 7 | export * from './lib/ngxs-firestore.adapter'; 8 | export * from './lib/action-decorator-helpers'; 9 | export * from './lib/actions'; 10 | export * from './lib/types'; 11 | -------------------------------------------------------------------------------- /packages/firestore-plugin/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": ["dom", "es2018"] 10 | }, 11 | "angularCompilerOptions": { 12 | "skipTemplateCodegen": true, 13 | "strictMetadataEmit": true, 14 | "fullTemplateTypeCheck": true, 15 | "strictInjectionParameters": true, 16 | "enableResourceInlining": true 17 | }, 18 | "exclude": ["**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/firestore-plugin/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /setupJest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /tools/copy-readme.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { existsSync, createReadStream, createWriteStream } from 'fs'; 3 | import { name } from '../package.json'; 4 | 5 | function copyReadmeAfterSuccessfulBuild(): void { 6 | const path = join(__dirname, '../README.md'); 7 | const noReadme = !existsSync(path); 8 | 9 | if (noReadme) { 10 | return console.log(`README.md doesn't exist on the root level!`); 11 | } 12 | 13 | createReadStream(path) 14 | .pipe(createWriteStream(join(__dirname, `../dist/${name}/README.md`))) 15 | .on('finish', () => { 16 | console.log(`Successfully copied README.md into dist/${name} folder!`); 17 | }); 18 | } 19 | 20 | copyReadmeAfterSuccessfulBuild(); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "noUnusedLocals": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "target": "ES2022", 13 | "lib": ["es2017", "dom"], 14 | "module": "es2020", 15 | "typeRoots": ["node_modules/@types"], 16 | "paths": { 17 | "@ngxs-labs/firestore-plugin": ["packages/firestore-plugin/src/public-api.ts"], 18 | "@ngxs-labs/firestore-plugin/compat": ["packages/firestore-plugin/compat/src/public-api.ts"] 19 | }, 20 | "forceConsistentCasingInFileNames": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "strict": true, 23 | "noImplicitReturns": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "useDefineForClassFields": false 26 | }, 27 | "angularCompilerOptions": { 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "module": "CommonJs", 6 | "types": ["jest"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "allowJs": true, 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /yarn.lock.readme.md: -------------------------------------------------------------------------------- 1 | All of our npm dependencies are locked via the `yarn.lock` file for the following reasons: 2 | 3 | - our project has lots of dependencies which update at unpredictable times, so it's important that 4 | we update them explicitly once in a while rather than implicitly when any of us runs `yarn install` 5 | - locked dependencies allow us to reuse yarn cache on travis, significantly speeding up our builds 6 | (by 5 minutes or more) 7 | - locked dependencies allow us to detect when node_modules folder is out of date after a branch switch 8 | which allows us to build the project with the correct dependencies every time 9 | 10 | Before changing a dependency, do the following: 11 | 12 | - make sure you are in sync with `upstream/master`: `git fetch upstream && git rebase upstream/master` 13 | - ensure that your `node_modules` directory is not stale by running `yarn install` 14 | 15 | 16 | To add a new dependency do the following: `yarn add --dev` 17 | 18 | To update an existing dependency do the following: run `yarn upgrade @ --dev` 19 | or `yarn upgrade --dev` to update to the latest version that matches version constraint 20 | in `package.json` 21 | 22 | To Remove an existing dependency do the following: run `yarn remove ` 23 | 24 | 25 | Once you've changed the dependency, commit the changes to `package.json` & `yarn.lock`, and you are done. 26 | --------------------------------------------------------------------------------