├── .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 |
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 |
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 |
--------------------------------------------------------------------------------