From 319b9ce1ccce6501249b3e99b5f00c282ad4e431 Mon Sep 17 00:00:00 2001 From: Randall Meeker Date: Sat, 25 Sep 2021 17:30:31 -0500 Subject: [PATCH] prettier-all-the-things-redux --- .vscode/launch.json | 2 +- BREAKING_CHANGES.md | 37 +- CHANGELOG.md | 23 +- angular.json | 18 +- doc/authentication/api.md | 57 ++- doc/collection-group/api.md | 10 +- doc/collection/getting-started.md | 19 +- doc/collection/guard/api.md | 51 +-- doc/collection/guard/config.md | 17 +- doc/collection/service/api.md | 124 ++++--- doc/collection/service/config.md | 14 +- doc/cookbook/callable-functions.md | 8 +- doc/cookbook/dynamic-stores.md | 24 +- doc/cookbook/subcollection.md | 24 +- doc/real-time-db/real-time-db.md | 4 +- doc/utils.md | 51 ++- firebase.json | 2 +- karma.conf.js | 8 +- projects/akita-ng-fire/karma.conf.js | 8 +- projects/akita-ng-fire/package.json | 10 +- projects/akita-ng-fire/rtdb/package.json | 10 +- projects/akita-ng-fire/rtdb/src/public-api.ts | 4 +- .../rtdb/src/real-time.config.ts | 3 +- .../rtdb/src/real-time.service.ts | 92 +++-- .../akita-ng-fire/src/lib/auth/auth.model.ts | 2 +- .../src/lib/auth/auth.service.ts | 142 ++++++-- .../collection-group.service.ts | 57 ++- .../src/lib/collection/collection.config.ts | 7 +- .../src/lib/collection/collection.guard.ts | 78 ++-- .../src/lib/collection/collection.service.ts | 343 ++++++++++++------ .../src/lib/utils/cancellation.ts | 11 +- .../src/lib/utils/has-path-getter.ts | 17 +- .../src/lib/utils/httpsCallable.ts | 18 +- .../akita-ng-fire/src/lib/utils/id-or-path.ts | 9 +- .../src/lib/utils/path-with-params.ts | 34 +- .../src/lib/utils/query/await-sync-query.ts | 76 ++-- .../src/lib/utils/query/sync-query.ts | 175 ++++++--- .../src/lib/utils/query/types.ts | 4 +- .../src/lib/utils/query/utils.ts | 13 +- .../src/lib/utils/redirect-if-empty.ts | 4 +- projects/akita-ng-fire/src/lib/utils/roles.ts | 38 +- .../src/lib/utils/share-delay.ts | 12 +- .../src/lib/utils/store-options.ts | 9 +- .../src/lib/utils/sync-from-action.ts | 68 +++- .../src/lib/utils/sync-with-router.ts | 22 +- projects/akita-ng-fire/src/lib/utils/types.ts | 18 +- projects/akita-ng-fire/src/public-api.ts | 2 +- projects/akita-ng-fire/src/test.ts | 2 +- projects/akita-ng-fire/tsconfig.lib.json | 10 +- projects/akita-ng-fire/tsconfig.lib.prod.json | 2 +- projects/akita-ng-fire/tslint.json | 14 +- schematics/README.md | 2 +- .../src/ng-g/collection-service/index.ts | 16 +- .../src/ng-g/collection-service/index_spec.ts | 51 ++- schematics/tsconfig.json | 18 +- tsconfig.app.json | 9 +- tsconfig.base.json | 5 +- tsconfig.json | 2 +- tslint.json | 79 ++-- 59 files changed, 1290 insertions(+), 699 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index eba84f6a..25420055 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,6 @@ "name": "Launch Chrome against localhost", "url": "http://localhost:4200", "webRoot": "${workspaceFolder}" - } + } ] } diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index b17a1b6b..55f5a0c9 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,18 +1,21 @@ # Breaking Changes & Deprecations ## 5.0.0 + Typescript version 4 and akita version 6 are no required. ## 3.1.7 + With version 3.1.7, you need to update your firebase version >= 8 and your @angular/fire >= 6.0.4, firebase 8 now uses EMS instead of CommonJs. ## 3.0.0 -With version v3, you need to update your akita version >=5.1.1, since there where breaking changes in the event-based-API from akita. +With version v3, you need to update your akita version >=5.1.1, since there where breaking changes in the event-based-API from akita. ## 2.0.0 **DEPRECATION**: + - `syncManyDocs` won't support Observable argument anymore & will not cleanup previous ids when there is a change: Use `switchMap` & `store.reset()` instead : @@ -22,19 +25,23 @@ const ids = new BehaviorSubject(['1']); // Don't syncManyDocs(ids.asObservable()).subscribe(); // DO -ids.asObservable().pipe( - tap(_ => this.store.reset()), // Remove old ids from the store before sync - switchMap(ids => syncManyDocs(ids)) -).subscribe(); +ids + .asObservable() + .pipe( + tap((_) => this.store.reset()), // Remove old ids from the store before sync + switchMap((ids) => syncManyDocs(ids)) + ) + .subscribe(); ``` - `CollectionGroupService` is deprecated. Use `CollectionService` and `syncGroupCollection` ```typescript -articleService.syncGroupCollection('article').subscribe() +articleService.syncGroupCollection('article').subscribe(); ``` - `SubcollectionService` is **removed**. Use new `syncWithRouter` util method: + ```typescript export class MovieService extends CollectionService { constructor(store: MovieStore, protected routerQuery: RouterQuery) { @@ -45,6 +52,7 @@ export class MovieService extends CollectionService { ``` And in case your Collection path contains parameters, make sure to also override `currentPath` getter: + ```typescript @CollectionConfig({ path: 'organization/:id/movies' }) export class MovieService extends CollectionService { @@ -63,10 +71,16 @@ export class MovieService extends CollectionService { Alternatively you can use `CollectionService` and `syncCollection({ params: { movieId }})`, cause now you can provide a `SyncOptions` value inside each `sync` method + ```typescript -movieQuery.selectActiveId().pipe( - switchMap(movieId => stakeholderService.syncCollection({ params: { movieId }})) -).subscribe() +movieQuery + .selectActiveId() + .pipe( + switchMap((movieId) => + stakeholderService.syncCollection({ params: { movieId } }) + ) + ) + .subscribe(); ``` > Checkout the [Cookbook](./doc/cookbook/subcollection.md) for more details @@ -74,13 +88,14 @@ movieQuery.selectActiveId().pipe( - `[CollectionService].preFormat` is deprecated. Use `formatToFirestore` instead of `preFormat` We improve the semantic of the hook method to make it more understandable. + ```typescript class MovieService extends CollectionService { formatToFirestore(movie: Partial): DBMovie { return { updatedOn: new Date(), - ...movie - } + ...movie, + }; } } ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index ab09d337..30c7cc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,62 +1,81 @@ # Change Log ## 6.0.0 + Supporting Angular 12. ## 5.0.2 + Fixed typing in syncWithRouter function. Thanks to @randallmeeker for the bug report. ## 5.0.1 + CollectionService exposes `createId` function from AngularFire. ## 4.3.3 + Add `useMemorization` on `CollectionService` to support multicasting. ## 4.3.1 + Added `await` in the update callback function. ## 4.3 + Created a second entry point for the `real time database service` => `akita-ng-fire/rtdb` ## 4.0.1 + Thanks to @randallmeeker for reporting a bug on how we reset the store. ## 4.0.0 + Update peerDependencies firebase to version >= 8 and @angular/fire to version >= 6.0.4, firebase 8 now uses EMS instead of CommonJs. -Thanks to @avelow +Thanks to @avelow ### 3.1.6 + Removed export from public api ### 3.1.5 + Removed if statement in `upsert` method. It was causing an issue when we provide an object with the id of the document. `service.upsert({ id, ...updates })` ### 3.1.4 + Fixed wrong hook call in auth service. ### 3.1.3 + Fixed scope issue in signin function. ### 3.1.2 + If signin is working with a not new user, we update the store from the corresponding document from firestore. ### 3.1.1 + Fixed issue with profile object getting updated in the signin method from the auth service. ### 3.1.0 -The auth store gets now updated properly when a user signs in after he signed up. + +The auth store gets now updated properly when a user signs in after he signed up. Also the function `fireAuth` that was deprecated for a long time has been removed. ### 3.0.5 + Bug fix for `getValue`. Now the idKey is merged into the value object. Thanks to @randallmeeker for the bug report. ### 3.0.4 + `callFunction` is now part of the public API. Thanks to @wSedlacek ### 3.0.3 + Bug fix from @TimVanMourik . Entity stores didn't get properly updated. ### 3.0.2 + Added `resetOnUpdate` config to `CollectionService`. With this config you can choose whether you totally want to remove and entity and add a new one with the new state, or, and this is default, when set to false, you let akita handle how they update their stores by design. Meaning if your new state updating the store, the keys that maybe got removed in the new state are still present in the akita store. So if you want the new state to be the only source of truth, set this config to true. diff --git a/angular.json b/angular.json index b95ad39a..3b65057c 100644 --- a/angular.json +++ b/angular.json @@ -22,10 +22,7 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", - "assets": [ - "src/favicon.ico", - "src/assets" - ], + "assets": ["src/favicon.ico", "src/assets"], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" @@ -91,10 +88,7 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", - "assets": [ - "src/favicon.ico", - "src/assets" - ], + "assets": ["src/favicon.ico", "src/assets"], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "src/styles.scss" @@ -110,9 +104,7 @@ "tsconfig.spec.json", "e2e/tsconfig.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } }, "e2e": { @@ -162,9 +154,7 @@ "projects/akita-ng-fire/tsconfig.lib.json", "projects/akita-ng-fire/tsconfig.spec.json" ], - "exclude": [ - "**/node_modules/**" - ] + "exclude": ["**/node_modules/**"] } } } diff --git a/doc/authentication/api.md b/doc/authentication/api.md index d747a899..7f585287 100644 --- a/doc/authentication/api.md +++ b/doc/authentication/api.md @@ -1,7 +1,9 @@ # Authentication + The `FireAuthService` makes it easy to connect Firebase Authentication and Firestore. Create an authentication store with the type of the user's profile : + ```typescript export interface Profile { displayName: string; @@ -20,27 +22,27 @@ export class AuthStore extends Store { ``` Then extend your service with `FireAuthService`: + ```typescript @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'users' }) export class AuthService extends FireAuthService { - constructor(store: AuthStore) { super(store); } - } ``` The authentication data will be managed by `AngularFireAuth`, while the profile data will be managed by `AngularFirestore`. In this case, the collection `users` will be used to store the profile. -In your `Query` you might want to expose the `profile` key : +In your `Query` you might want to expose the `profile` key : + ```typescript @Injectable({ providedIn: 'root' }) export class AuthQuery extends Query { profile$ = this.select('profile'); - roles$ = this.select('roles'); // check section "roles" below + roles$ = this.select('roles'); // check section "roles" below constructor(protected store: AuthStore) { super(store); @@ -53,7 +55,9 @@ export class AuthQuery extends Query { ### Create #### Providers Name -`FireAuthService` provides a single method to signin with a provider. Just type the provider you want to use as first parameter : + +`FireAuthService` provides a single method to signin with a provider. Just type the provider you want to use as first parameter : + ```typescript service.signin('apple'); ``` @@ -61,7 +65,9 @@ service.signin('apple'); > When user authenticates for the first time, a document in the collection will be created #### Provider Object -If you want to add custom options on your provider, you can use the `setCustomParameters` function : + +If you want to add custom options on your provider, you can use the `setCustomParameters` function : + ```typescript import { getAuthProvider } from 'akita-ng-fire'; @@ -71,7 +77,9 @@ service.signin(provider); ``` #### Email & Password -When using email & password, you first need to signup, then signin : + +When using email & password, you first need to signup, then signin : + ```typescript service.signup(email, password); ... @@ -79,12 +87,15 @@ service.signin(email, password); ``` #### Signout + ```typescript service.signout(); ``` #### Create profile -By default `FireAuthService` will create a document with `photoURL` and `displayName`. You can override this : + +By default `FireAuthService` will create a document with `photoURL` and `displayName`. You can override this : + ```typescript export class AuthService extends FireAuthService { createProfile(user: User): AuthState['profile'] { @@ -94,25 +105,33 @@ export class AuthService extends FireAuthService { ``` ### Read -The process to sync the store with Firestore is similar to `CollectionService` : + +The process to sync the store with Firestore is similar to `CollectionService` : + ```typescript service.sync().subscribe(); ``` ### Update -When you use `update` you're updating the `profile` in Firestore : + +When you use `update` you're updating the `profile` in Firestore : + ```typescript service.update({ age }); ``` ### Delete + ⚠️ When you use `delete` you're removing the user from Firebase Authentication & Firestore. Only the authenticated user can delete their account. + ```typescript service.delete(); ``` ## User -For more advance operation on the account, you can use the user property: + +For more advance operation on the account, you can use the user property: + ```typescript service.user.sendEmailVerification(); service.user.updatePassword(newPassword); @@ -121,6 +140,7 @@ service.user.reload(); ``` ## Hooks + Similar to `CollectionService`, the `FireAuthService` provides hooks for atomic updates. ```typescript @@ -132,12 +152,15 @@ onSignin(credentials) {} ``` ## Roles -The `FireAuthService` helps you manage roles for your user. + +The `FireAuthService` helps you manage roles for your user. ### Custom User Claims + For roles specific to the user you might want to update the `CustomUserClaims` : -First update your state : +First update your state : + ```typescript export interface Roles { admin: boolean; @@ -147,22 +170,26 @@ export interface Roles { export interface AuthState extends FireAuthState, RoleState {} ``` -In a Cloud function using `Firebase Admin SDK`: +In a Cloud function using `Firebase Admin SDK`: + ```typescript admin.auth().setCustomUserClaims(uid, { admin: false, contributor: true }); ``` Then inside the `FireAuthService` : + ```typescript export class AuthService extends FireAuthService { selectRoles(user: User): Promise { - return getCustomClaims(user, ['admin', 'contributor']); // Fetch keys "admin" & "contributor" of the claims in the token + return getCustomClaims(user, ['admin', 'contributor']); // Fetch keys "admin" & "contributor" of the claims in the token } } ``` ### Collection Roles + To store roles somewhere else you can override the method `selectRoles` and implement a `updateRole` method: + ```typescript export class AuthService extends FireAuthService { selectRoles(user: User) { diff --git a/doc/collection-group/api.md b/doc/collection-group/api.md index b5608b6a..d3ae1d24 100644 --- a/doc/collection-group/api.md +++ b/doc/collection-group/api.md @@ -1,5 +1,6 @@ # Collection Group Service - Getting Started -The `CollectionGroupService` is a simple service to sync a store with a collection group. + +The `CollectionGroupService` is a simple service to sync a store with a collection group. ```typescript @Injectable({ providedIn: 'root' }) @@ -9,7 +10,6 @@ export class MovieService extends CollectionGroupService { constructor(store: MovieStore) { super(store); } - } ``` @@ -18,17 +18,21 @@ export class MovieService extends CollectionGroupService { ## Properties ```typescript -collectionId: string +collectionId: string; ``` + The id of the collection group you want to sync with. This value is **mandatory**. ## Methods + ```typescript syncCollection(query?: QueryGroupFn); ``` + Sync the collection group query with the store. ```typescript getValue(query?: QueryGroupFn): Promise ``` + Get a snapshot of the collection group query. diff --git a/doc/collection/getting-started.md b/doc/collection/getting-started.md index 7480f4b0..23bd7f86 100644 --- a/doc/collection/getting-started.md +++ b/doc/collection/getting-started.md @@ -1,17 +1,20 @@ # Collection - Getting Started ## Collection Service + The `CollectionService` provides all the CRUD methods needed to interact with Firestore. It simplifies the communication between your Akita and Firestore for a specific Collection. -Let's see how we can use it to connect a `MovieStore` with Firestore : +Let's see how we can use it to connect a `MovieStore` with Firestore : In your **movie.store.ts**, extend the `MovieState` with `CollectionState` : + ```typescript export interface MovieState extends CollectionState {} ``` Then in your **movie.service.ts** : + ```typescript import { Injectable } from '@angular/core'; import { MovieStore, MovieState } from './movie.store'; @@ -21,11 +24,9 @@ import { CollectionConfig, CollectionService } from 'akita-ng-fire'; @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) export class MovieService extends CollectionService { - constructor(store: MovieStore) { super(store); } - } ``` @@ -36,10 +37,10 @@ Let's see what happen here : 3. We provide a `MovieState` in `CollectionService`'s generic. **`MovieState` as to extend the `CollectionState` interface**. 4. We pass the dependancies to `AngularFirestore` and `MovieStore` though `super()`. - ## Component -In your component you can now start listening on Firebase : +In your component you can now start listening on Firebase : + ```typescript @Component({ selector: 'app-root', @@ -48,7 +49,7 @@ In your component you can now start listening on Firebase :
  • {{ movie.title }}
  • - ` + `, }) export class AppComponent implements OnInit, OnDestroy { private subscription: Subscription; @@ -76,9 +77,10 @@ export class AppComponent implements OnInit, OnDestroy { ## Guard (alternative to `ngOnDestroy`) -Alternatively you can use a Guard to manage your subscriptions/unsubscriptions : +Alternatively you can use a Guard to manage your subscriptions/unsubscriptions : + +First create a new `movie.guard.ts`: -First create a new `movie.guard.ts`: ```typescript @Injectable({ providedIn: 'root' }) export class MovieGuard extends CollectionGuard { @@ -89,6 +91,7 @@ export class MovieGuard extends CollectionGuard { ``` In your `movie.module.ts` + ```typescript @NgModule({ declarations: [HomeComponent, MovieListComponent] diff --git a/doc/collection/guard/api.md b/doc/collection/guard/api.md index 5ceb1441..5e480bd0 100644 --- a/doc/collection/guard/api.md +++ b/doc/collection/guard/api.md @@ -1,4 +1,5 @@ # Collection Guard - API + `CollectionGuard` provides you an elegant way to subscribe and unsubscribe to a collection. ```typescript @@ -12,37 +13,38 @@ export class MovieListGuard extends CollectionGuard { This Guard is going to subscribe to `syncCollection` of `MovieService` when entering the route, and unsubscribe when leaving. -In your `RouterModule` you would have : +In your `RouterModule` you would have : + ```typescript const routes: Route[] = [ { path: 'movies', component: MovieListGuard, canActivate: [MovieListGuard], - canDeactivate: [MovieListGuard] - } -] + canDeactivate: [MovieListGuard], + }, +]; ``` --- ## Subscription Strategy + `MovieGuard` provides two strategy to deal with subscription. ### Loading Strategy -By default the `MovieGuard` doesn't wait for the Firestore to send the first data. It makes routing **fast** but the **data might not be available** when component appears. Then you want to leverage the `loading` key of Akita : + +By default the `MovieGuard` doesn't wait for the Firestore to send the first data. It makes routing **fast** but the **data might not be available** when component appears. Then you want to leverage the `loading` key of Akita : ```typescript @Component({ selector: 'app-movies', template: ` - - Loading - + Loading - ` + `, }) export class MovieListComponent implements OnInit { public loading$: Observable; @@ -56,9 +58,10 @@ export class MovieListComponent implements OnInit { ``` ### AwaitSync Strategy + You can specify the Guard to wait for Firestore first push. It might make routing **slow**, but you're sure that the **data is available** when component appears. -For that you can use the `CollectionGuardConfig` decorator: +For that you can use the `CollectionGuardConfig` decorator: ```typescript @Injectable({ providedIn: 'root' }) @@ -73,6 +76,7 @@ export class MovieListGuard extends CollectionGuard { --- ## Custom Sync Function + By default `CollectionGuard` is going to run `syncCollection()` of your service. You can override the behavior with the `sync` getter. ```typescript @@ -80,6 +84,7 @@ sync(next: ActivatedRouteSnapshot): Observable ``` The `sync` getter should return an `Observable` of `string`, `boolean` or `any`. + - If `boolean`: `canActivate` returns the value. - If `string`: `canActivate` returns the `UrlTree` representation of the string. **Useful for redirection**. - Else `canActivate` always returns `true`. @@ -87,11 +92,12 @@ The `sync` getter should return an `Observable` of `string`, `boolean` or `any`. > **IMPORTANT** : The return value will only be evaluated if using the **Await Strategy**. ### Example: Sync and Activate a Document + To sync and activate a document when you enter a route, you can do : + ```typescript @Injectable({ providedIn: 'root' }) export class ActiveMovieGuard extends CollectionGuard { - constructor(service: MovieService) { super(service); } @@ -105,20 +111,22 @@ export class ActiveMovieGuard extends CollectionGuard { > Note: In this case we use the **Loading Strategy** because we don't need to wait for `syncActive`. -And in the router : +And in the router : + ```typescript const routes: Route[] = [ { path: 'movie/:id', component: MovieViewComponent, canActivate: [ActiveMovieGuard], - canDeactivate: [ActiveMovieGuard] - } -] + canDeactivate: [ActiveMovieGuard], + }, +]; ``` ### Example: Sync and Redirect if Empty -As very common feature is to redirect to a specific page if there is no document in the collection. You can do that very easily with `CollectionGuard` : + +As very common feature is to redirect to a specific page if there is no document in the collection. You can do that very easily with `CollectionGuard` : ```typescript @Injectable({ providedIn: 'root' }) @@ -131,8 +139,8 @@ export class MovieListGuard extends CollectionGuard { // Sync to collection. If empty redirecto to 'movies/create' sync() { return this.service.syncCollection().pipe( - map(_ => this.query.getCount()), - map(count => count === 0 ? '/movies/create' : true) + map((_) => this.query.getCount()), + map((count) => (count === 0 ? '/movies/create' : true)) ); } } @@ -140,7 +148,8 @@ export class MovieListGuard extends CollectionGuard { > Note: In this case we use the **AwaitSync Strategy** because we need `canActivate` to evaluate the value returned by Firestore. -And in the router : +And in the router : + ```typescript const routes: Route[] = [ { path: 'create', component: MovieCreateComponent }, @@ -148,7 +157,7 @@ const routes: Route[] = [ path: 'list', canActivate: [MovieListGuard], canDeactivate: [MovieListGuard], - component: MovieListComponent + component: MovieListComponent, }, -] +]; ``` diff --git a/doc/collection/guard/config.md b/doc/collection/guard/config.md index d045e757..cb77237b 100644 --- a/doc/collection/guard/config.md +++ b/doc/collection/guard/config.md @@ -1,4 +1,5 @@ # Collection Guard - Configuration + The Guard can be configure with a `CollectionRouteData` object : ```typescript @@ -13,10 +14,11 @@ interface CollectionRouteData { - **redirect**: The route to redirect to if subscription failed (only for **AwaitSync Strategy**) - **awaitSync**: Use **AwaitSync Strategy** if true. -You can set this configuration in three different places : +You can set this configuration in three different places : ## CollectionGuardConfig -You can use the `CollectionGuardConfig` decorator : + +You can use the `CollectionGuardConfig` decorator : ```typescript @Injectable({ providedIn: 'root' }) @@ -32,8 +34,8 @@ export class MovieListGuard extends CollectionGuard { } ``` - ## Router Data + You can set the `CollectionRouteData` directly in the route : ```typescript @@ -43,16 +45,17 @@ const routes: Route[] = [ component: MovieListGuard, canActivate: [MovieListGuard], canDeactivate: [MovieListGuard], - data : { + data: { queryFn: (ref) => ref.limit(10), redirect: '/404', awaitSync: true, - } - } -] + }, + }, +]; ``` ## Getter parameters + For finer configuration you'll want to use the getters inside `CollectionGuard` : ```typescript diff --git a/doc/collection/service/api.md b/doc/collection/service/api.md index c0a2ec4b..dc2f1ac9 100644 --- a/doc/collection/service/api.md +++ b/doc/collection/service/api.md @@ -1,47 +1,58 @@ # Collection Service - API ## Sync + Collection Service provides methods to subscribe on changes from Firestore. ### syncCollection + `syncCollection` will subscribe to your collection, and update the store accordingly. + ``` syncCollection(path: string | Observable | QueryFn, queryFn?: QueryFn) ``` -It takes a `path` and/or a firestore `queryFn`. By default `CollectionService`'s `path` will be used. +It takes a `path` and/or a firestore `queryFn`. By default `CollectionService`'s `path` will be used. ```typescript this.service.syncCollection().subscribe(); ``` With a `QueryFn` + ```typescript -this.service.syncCollection(ref => ref.limit(10)).subscribe(); +this.service.syncCollection((ref) => ref.limit(10)).subscribe(); ``` For subcollections : + ```typescript -const queryFn = ref => ref.orderBy('age'); -this.parentQuery.selectActiveId().pipe( - map(id => `parent/${id}/subcollection`), - switchMap(path => this.service.syncCollection(path, queryFn)) -).subscribe(); +const queryFn = (ref) => ref.orderBy('age'); +this.parentQuery + .selectActiveId() + .pipe( + map((id) => `parent/${id}/subcollection`), + switchMap((path) => this.service.syncCollection(path, queryFn)) + ) + .subscribe(); ``` ### syncCollectionGroup + `syncCollectionGroup` will subscribe to a firebase collection group and sync the store with the result. ` + ```typescript syncCollectionGroup(queryGroupFn?: QueryGroupFn); syncCollectionGroup(collectionId?: string, queryGroupFn?: QueryGroupFn); ``` + If not provided, the method will use the `currentPath` as collectionId. ⚠️ If the `path` is a subcollection, `syncCollectionGroup` will take **the last part of the path** (eg: if path is `movies/{movieId}/stakeholders` then the collectionId will be `stakeholders`). - ### syncDoc + `syncDoc` will subscribe to a specific document, and update the store accordingly. ```typescript @@ -51,48 +62,55 @@ syncDoc(options: { id: string } | { path: string }); It takes either an `{ id: string }` object **OR** a `{ path: string }`. ```typescript -this.route.params.pipe( - switchMap(({ id }) => this.service.syncDoc({ id })) -).subscribe(); +this.route.params + .pipe(switchMap(({ id }) => this.service.syncDoc({ id }))) + .subscribe(); ``` ### syncManyDocs + `syncManyDocs` subscribes to a list of documents and update the store accordingly + ```typescript syncManyDocs(ids: string[]); ``` Here is an example that sync all movie from the active user : + ```typescript -userQuery.selectActive().pipe( - pluck('movieIds'), - distinctUntilChanges((x, y) => x.length === y.length), // trigger only when amount of movieIds changes - tap(_ => movieStore.reset()), // Remove old ids from the store before sync - switchMap(movieIds => movieService.syncManyDocs(movieIds)) -).subscribe(); +userQuery + .selectActive() + .pipe( + pluck('movieIds'), + distinctUntilChanges((x, y) => x.length === y.length), // trigger only when amount of movieIds changes + tap((_) => movieStore.reset()), // Remove old ids from the store before sync + switchMap((movieIds) => movieService.syncManyDocs(movieIds)) + ) + .subscribe(); ``` ### syncActive + `syncActive` is an helper that run `syncDoc({id})` or `syncManyDocs(ids)` and `setActive(id)`. ```typescript this.service.syncActive(`movies/${movieId}`); // ActiveState -this.service.syncActive(['1', '2', '3']); // ManyActiveState +this.service.syncActive(['1', '2', '3']); // ManyActiveState ``` ## Read ```typescript -path: string +path: string; ``` -The `path` is the path of your Firestore collection. It can be override, which can be useful for [subcollections](../../cookbook/subcollection.md#override-path-with-getter). +The `path` is the path of your Firestore collection. It can be override, which can be useful for [subcollections](../../cookbook/subcollection.md#override-path-with-getter). ```typescript collection: AngularFirestoreCollection ``` -The `collection` is a snapshot of the collection. It's mostly used for writing operations (`add`, `remove`, `update`). +The `collection` is a snapshot of the collection. It's mostly used for writing operations (`add`, `remove`, `update`). ### getValue @@ -103,12 +121,12 @@ getValue(ids: string[], options?: Partial): Promise getValue(queryFn: QueryFn, options?: Partial): Promise ``` -Returns a snapshot of the collection or a document in the collection. If no parameters are provided, will fetch the whole collection : +Returns a snapshot of the collection or a document in the collection. If no parameters are provided, will fetch the whole collection : ```typescript const movie = await movieService.getValue('star_wars'); const movies = await movieServie.getValue(userQuery.getActive().movieIds); // all movies of current user -const movies = await movieService.getValue(ref => ref.limitTo(10)); +const movies = await movieService.getValue((ref) => ref.limitTo(10)); const stakeholders = await stakeholderService.getValue({ params: { movieId } }); // all sub-collection ``` @@ -126,6 +144,7 @@ getRef(id?: string, options?: Partial): Promise): Observable ``` #### useMemorization -You can multicast observables on the docs queries by id by specifying the flat `useMemorization`: + +You can multicast observables on the docs queries by id by specifying the flat `useMemorization`: + ```typescript @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) @@ -148,8 +169,8 @@ export class MovieService extends CollectionService { } ``` - ## Write + `CollectionService` provides three methods to update Firestore. This library encourages you to sync your Akita store with Firestore (see above), so you **shouldn't update the store yourself** after `add`, `remove` or `update` succeed. ### Atomic Write @@ -159,7 +180,7 @@ batch(): firestore.WriteBatch runTransaction((tx: firestore.Transaction) => Promise) ``` -Create a batch object or run a transaction. Those methods are just alias for `firstore.batch()` & `firestore.runTransaction()`. +Create a batch object or run a transaction. Those methods are just alias for `firstore.batch()` & `firestore.runTransaction()`. > This is god practice to use AtomicWrite when you operate several interdependant write operations. @@ -168,15 +189,20 @@ Create a batch object or run a transaction. Those methods are just alias for `f ```typescript add(entities: E[] | E, options?: WriteOptions): Promise ``` + Add one or several documents in your collection. And return the id(s). > `add` will create an id on the client-side if not provided. This example shows how to add a movie and a stakeholder for this movie with a batch : + ```typescript const write = await movieService.batch(); const movieId = await movieService.add({ name: 'Star Wars' }, { write }); -await stakeholderService.add({ name: 'Walt Disney' }, { write, params: { movieId } }); +await stakeholderService.add( + { name: 'Walt Disney' }, + { write, params: { movieId } } +); write.commit(); ``` @@ -185,6 +211,7 @@ write.commit(); ```typescript remove(ids: string | string[], options?: WriteOptions) ``` + Remove one or several documents from the collection. > To avoid wrong manipulatoin, `remove()` will not remove all document in a collection. Use `removeAll` for that. @@ -213,6 +240,7 @@ update(entity: Partial | Partial[], options?: WriteOptions) update(id: string | string[], newState: Partial, options?: WriteOptions) update(id: string | string[] | predicateFn, newStateFn: ((entity: Readonly, tx: firestore.Transaction) => Partial), options?: WriteOptions) ``` + Update one or several documents in the collection. > When using a newStateFn, akita-ng-fire will use a transaction so it cannot be combine with a batch : @@ -223,10 +251,10 @@ This example remove a movie from a user, and update the stakeholders of the movi const user = userQuery.getActive(); const movieId = movieQuery.getActiveId(); await userService.update(uid, async (user, tx) => { - const movieIds = user.movieId.filter(id => id !== movieId); - await stakeholderService.remove(uid, { params: { movieId } }); // Remove user from stakeholders of movie - return { movieIds }; // Update user movieIds -}) + const movieIds = user.movieId.filter((id) => id !== movieId); + await stakeholderService.remove(uid, { params: { movieId } }); // Remove user from stakeholders of movie + return { movieIds }; // Update user movieIds +}); ``` ### Upsert @@ -234,15 +262,17 @@ await userService.update(uid, async (user, tx) => { ```typescript upsert(entities: E[] | E, options?: WriteOptions): Promise ``` -Create or update one or a list of document. -If an array is provided, `upsert` will check for every element if it exists. In this case, it's highly recommended to provide a transaction in the option parameter : +Create or update one or a list of document. + +If an array is provided, `upsert` will check for every element if it exists. In this case, it's highly recommended to provide a transaction in the option parameter : ```typescript -service.runTransaction(write => service.upsert(manyDocs, { write })); +service.runTransaction((write) => service.upsert(manyDocs, { write })); ``` ## Hooks + You can hook every write operation and chain them with atomic operations: ```typescript @@ -250,15 +280,19 @@ onCreate(entity: E, options: { write: AtomicWrite, ctx?: any }) onUpdate(entity: E, options: { write: AtomicWrite, ctx?: any }) onDelete(id: string, options: { write: AtomicWrite, ctx?: any }) ``` + The `options` parameter is used to pass atomic writer and optional contextual data. The `write` parameter is either a `batch` or a `transaction` used to group several write operations. The `ctx` parameter is used to send contextual data for cascading writings. For example, you can remove all stakeholders of a movie on deletion: + ```typescript class MovieService extends CollectionService { - - constructor(store: MovieStore, private stakeholderService: StakeholderService) { + constructor( + store: MovieStore, + private stakeholderService: StakeholderService + ) { super(store); } @@ -269,32 +303,38 @@ class MovieService extends CollectionService { ``` You can also chain the atomic write: + ```typescript class OrganizationService extends CollectionService { - constructor( store: OrganizationStore, private userService: UserService, - private userQuery: UserQuery, + private userQuery: UserQuery ) { super(store); } onCreate(organization: Organization, options: WriteOptions) { const uid = this.userQuery.getActiveId(); - return this.userService.update(uid, (user) => { - return { orgIds: [...user.orgIds, organization.id] } - }, options); // We pass the "options" parameter as 3rd argument of the update to do everything in one batch + return this.userService.update( + uid, + (user) => { + return { orgIds: [...user.orgIds, organization.id] }; + }, + options + ); // We pass the "options" parameter as 3rd argument of the update to do everything in one batch } } ``` ## Formatters + You can format your data when it comes from Firestore with a custom function. To do so you have to override the function `formatFromFirestore`. + ```typescript formatFromFirestore(stakeholder: Readonly) { const alteredStakeholder = { ...stakeholder, name: `The original name was ${stakeholder.name}, but now its formatFromFirestore` } return alteredStakeholder; } -``` \ No newline at end of file +``` diff --git a/doc/collection/service/config.md b/doc/collection/service/config.md index ab56a635..c6c8e265 100644 --- a/doc/collection/service/config.md +++ b/doc/collection/service/config.md @@ -1,10 +1,13 @@ # Collection Service - Configuration -There is two ways to configure your `CollectionService` : + +There is two ways to configure your `CollectionService` : + - With the `CollectionConfig` decorator. - By overriding default `getter` of `CollectionService`. ## CollectionConfig -`CollectionConfig` gives you an elegant way to define the configuration of your service : + +`CollectionConfig` gives you an elegant way to define the configuration of your service : ```typescript @CollectionConfig({ @@ -13,10 +16,12 @@ There is two ways to configure your `CollectionService` : resetOnUpdate: true }) ``` + The `resetOnUpdate` config you can choose whether you totally want to remove and entity and add a new one with the new state, or, and this is default, when set to false, you let akita handle how they update their stores by design. Meaning if your new state updating the store, the keys that maybe got removed in the new state are still present in the akita store. So if you want the new state to be the only source of truth, set this config to true. -`CollectionConfig` accept a `Partial` object as parameter that looks like that : +`CollectionConfig` accept a `Partial` object as parameter that looks like that : + ```typescript export interface CollectionOptions { path: string; // The path of the collection in Firestore @@ -24,8 +29,8 @@ export interface CollectionOptions { } ``` - ## Path Getter + Sometime the path is dynamic. If your service is targeting a sub collection, the path needs to know what is the id of the parent document. In this case you'll need to override the `path` getter inside the class. Let's see how to do that with a `Stakeholder` of a specific movie : @@ -43,6 +48,7 @@ export class StakeholderService extends CollectionService { } } ``` + 1. We do not need the `CollectionConfig` here. 2. We inject `MovieQuery`, the query of the parent collection. 3. We override the `path` getter by getting the active movie Id. diff --git a/doc/cookbook/callable-functions.md b/doc/cookbook/callable-functions.md index 605d8479..e5ffc67b 100644 --- a/doc/cookbook/callable-functions.md +++ b/doc/cookbook/callable-functions.md @@ -3,8 +3,7 @@ If you want to instantiate a firebase cloud function in your service, you can bind the `callFunction` in method of akita-ng-fire like so: -``` typescript - +```typescript import { Injectable } from '@angular/core'; import { MovieStore, MovieState } from './movie.store'; import { AngularFirestore } from '@angular/fire/firestore'; @@ -14,12 +13,11 @@ import { AngularFireFunctions } from '@angular/fire/functions'; @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) export class MovieService extends CollectionService { - private callable = callFunction.bind(this.functions); constructor(store: MovieStore, private functions: AngularFireFunctions) { - super(store); - } + super(store); + } } ``` diff --git a/doc/cookbook/dynamic-stores.md b/doc/cookbook/dynamic-stores.md index 80bd945c..a891a3ec 100644 --- a/doc/cookbook/dynamic-stores.md +++ b/doc/cookbook/dynamic-stores.md @@ -1,7 +1,9 @@ # Dynamic Stores + If you want to sync one collection with several stores you can use [Dynamic Stores](https://datorama.github.io/akita/docs/angular/local-state/#dynamic-stores) from Akita. First create a Store with unique id : + ```typescript export class MovieStore extends EntityStore { constructor() { @@ -10,9 +12,11 @@ export class MovieStore extends EntityStore { } } ``` + > Note that you don't need to add `@Injectable()` here. -Then the Query: +Then the Query: + ```typescript export class MovieQuery extends QueryEntity { constructor(protected store: MovieStore) { @@ -22,6 +26,7 @@ export class MovieQuery extends QueryEntity { ``` The `CollectionService` : + ```typescript @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) @@ -31,18 +36,19 @@ export class MovieService extends CollectionService { } } ``` + > **IMPORTANT**: We don't provide the store here because we want this service to work with several stores. -And the component : +And the component : + ```typescript @Component({ selector: '[genre] movie-filter', templateUrl: './movie-filter.component.html', styleUrls: ['./movie-filter.component.css'], - providers: [MovieStore, MovieQuery] + providers: [MovieStore, MovieQuery], }) export class MovieFilterComponent implements OnInit, OnDestroy { - @Input() genre: string; private sub: Subscription; public movies$: Observable; @@ -50,14 +56,14 @@ export class MovieFilterComponent implements OnInit, OnDestroy { constructor( private service: MovieService, private query: MovieQuery, - private store: MovieeStore, - ) { } + private store: MovieeStore + ) {} ngOnInit() { // Get the store name of the local store const storeName = this.store.storeName; // Define a query specific to this component - const queryFn = ref => ref.where('genre', 'array-contains', this.genre); + const queryFn = (ref) => ref.where('genre', 'array-contains', this.genre); // Specify the storeName as StoreOption this.sub = this.service.syncCollection(queryFn, { storeName }).subscribe(); this.movies$ = this.query.selectAll(); @@ -69,9 +75,11 @@ export class MovieFilterComponent implements OnInit, OnDestroy { } ``` -Now you can do : +Now you can do : + ```html ``` + This will generate 2 stores with their own query. diff --git a/doc/cookbook/subcollection.md b/doc/cookbook/subcollection.md index 60c89372..67592e2e 100644 --- a/doc/cookbook/subcollection.md +++ b/doc/cookbook/subcollection.md @@ -12,7 +12,6 @@ A common use case is to `setActive` the id of the parent document in Akita's Ent By overriding the `path` all methods will work directly on the right subcollection. - ```typescript import { CollectionService, CollectionConfig } from 'akita-ng-fire'; @@ -25,12 +24,13 @@ export class StakeholderService extends CollectionService { get path() { const parentId = this.movieQuery.getActiveId(); - return `movies/${parentId}/stakeholders` + return `movies/${parentId}/stakeholders`; } } ``` Now you can use your service as if it were a normal collection: + ```typescript ngOnInit() { this.sub = stakeholderService.syncCollection().subscribe(); @@ -41,12 +41,13 @@ ngOnDestroy() { ``` Pros: + - All methods will work with it. - Easy to use. Cons: -- You have to be sure that the **parent ID doesn't change** during your subscription time. +- You have to be sure that the **parent ID doesn't change** during your subscription time. ### Use selectActiveId @@ -64,9 +65,11 @@ export class StakeholderService extends CollectionService { sync(queryFn: QueryFn) { return this.movieQuery.selectActiveId().pipe( - tap(_ => this.store.reset()), // Optional, but highly recommended - switchMap(movieId => this.syncCollection(queryFn, { params: { movieId }})) - ) + tap((_) => this.store.reset()), // Optional, but highly recommended + switchMap((movieId) => + this.syncCollection(queryFn, { params: { movieId } }) + ) + ); } } ``` @@ -74,11 +77,12 @@ export class StakeholderService extends CollectionService { > Whenever the parent active ID change you might want to reset the store to make sure you're store only have the right sub collection. Pros: + - Subscribe on the change of the parent active ID. Cons: -- You have to do it for every methods you want to use. +- You have to do it for every methods you want to use. ## With Router params @@ -87,7 +91,11 @@ When working with sub collection, a common use case is to pass the id of the par In this case the `RouterQuery` from akita comes handy in combination with the `syncWithRouter` utils : ```typescript -import { syncWithRouter, CollectionService, CollectionConfig } from 'akita-ng-fire'; +import { + syncWithRouter, + CollectionService, + CollectionConfig, +} from 'akita-ng-fire'; @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies/:movieId/stakeholders' }) diff --git a/doc/real-time-db/real-time-db.md b/doc/real-time-db/real-time-db.md index ee133b2a..6886176b 100644 --- a/doc/real-time-db/real-time-db.md +++ b/doc/real-time-db/real-time-db.md @@ -1,4 +1,5 @@ # Real Time Database + ## Real Time Service If you want to sync your store with your real time database, you can do that now with the `RealTimeService`. @@ -8,14 +9,13 @@ If you want to initialize this service you can do so like the example below: ```typescript import { RealTimeConfig } from 'akita-ng-fire/rtdb'; -@Injectable({provideIn: 'root'}) +@Injectable({ provideIn: 'root' }) @RealTimeConfig({ nodeName: 'vehicles' }) class VehicleService extends RealTimeService { constructor(store: VehicleStore, db: AngularFireDatabase) { super(store, 'vehicles', db); } } - ``` - The first parameter is the akita store you want to link to this service. diff --git a/doc/utils.md b/doc/utils.md index 87186551..c83d504d 100644 --- a/doc/utils.md +++ b/doc/utils.md @@ -1,4 +1,5 @@ # Utils + akita-ng-fire provide some utils method to improve your experience with Akita and Firebase. ## Utils for Service @@ -13,7 +14,11 @@ This is useful to keep the impact for `akita-ng-fire` on your bundle size as low It synchronizes your stor with a subcollection which document parent's ID is provided as router params. ```typescript -import { syncWithRouter, CollectionService, CollectionConfig } from 'akita-ng-fire'; +import { + syncWithRouter, + CollectionService, + CollectionConfig, +} from 'akita-ng-fire'; @Injectable({ providedIn: 'root' }) @CollectionConfig({ path: 'movies/:movieId/stakeholders' }) @@ -36,6 +41,7 @@ awaitSyncQuery(query: Query): Observble ``` The `Query` object has at least a `path` and an optional `queryFn`. Every other keys will set the value of the object. Thus is a simplified definition of `Query` + ```typescript type Query = { path: string; @@ -45,7 +51,12 @@ type Query = { ``` ```typescript -import { Query, CollectionConfig, CollectionService, awaitSyncQuery } from 'akita-ng-fire'; +import { + Query, + CollectionConfig, + CollectionService, + awaitSyncQuery, +} from 'akita-ng-fire'; interface MovieWithStakehodlers extends Movie { stakehoders: Stakeholders[]; } @@ -55,10 +66,10 @@ const syncMovieWithStakehodlers: Query = { path: 'movies', stakehoders: (movie: Movie) => ({ path: `movies/${movie.id}/stakeholders`, - queryFn: ref => ref.limitTo(10), - movieId: movie.id // Set the movie ID - }) -} + queryFn: (ref) => ref.limitTo(10), + movieId: movie.id, // Set the movie ID + }), +}; @Injectabe({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) @@ -69,12 +80,13 @@ class MovieService extends CollectionService { This method can be compared with `syncQuery`. Let's see pro & con for `awaitSyncQuery` : -Pro : +Pro : + - Query is recursive and can be **as deep as required**. -Con : -- The query will await ALL documents to be fetched before returning the entity. It can be quite long depending on the amount of documents. +Con : +- The query will await ALL documents to be fetched before returning the entity. It can be quite long depending on the amount of documents. ## syncQuery @@ -83,7 +95,12 @@ Combines **two** collections/subcollection from firestore into one entity store It works exactly like `awaitSyncQuery` but can be only one level deep and subentities will be added to the store directly when they are fetched (it will not wait for ALL of them to be fetched). ```typescript -import { Query, CollectionConfig, CollectionService, syncQuery } from 'akita-ng-fire'; +import { + Query, + CollectionConfig, + CollectionService, + syncQuery, +} from 'akita-ng-fire'; interface MovieWithStakehodlers extends Movie { stakehoders: Stakeholders[]; } @@ -93,9 +110,9 @@ const syncMovieWithStakehodlers: Query = { path: 'movies', stakehoders: (movie: Movie) => ({ path: `movies/${movie.id}/stakeholders`, - queryFn: ref => ref.limitTo(10), - }) -} + queryFn: (ref) => ref.limitTo(10), + }), +}; @Injectabe({ providedIn: 'root' }) @CollectionConfig({ path: 'movies' }) @@ -106,8 +123,10 @@ class MovieService extends CollectionService { This method can be compared with `awaitSyncQuery`. Let's see pro & con for `syncQuery` : -Pro : +Pro : + - Doesn't wait for all documents to be loaded, so you can display documents as soon as they arrive. -Con : -- The query is only two level deep. \ No newline at end of file +Con : + +- The query is only two level deep. diff --git a/firebase.json b/firebase.json index f8b0b2ef..24b135a4 100644 --- a/firebase.json +++ b/firebase.json @@ -14,4 +14,4 @@ "port": "9000" } } -} \ No newline at end of file +} diff --git a/karma.conf.js b/karma.conf.js index db0b948c..46ba0fa4 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,15 +10,15 @@ module.exports = function (config) { require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma'), ], client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/akita-ng-fire'), reports: ['html', 'lcovonly', 'text-summary'], - fixWebpackSourcePaths: true + fixWebpackSourcePaths: true, }, reporters: ['progress', 'kjhtml'], port: 9876, @@ -27,6 +27,6 @@ module.exports = function (config) { autoWatch: true, browsers: ['Chrome'], singleRun: false, - restartOnFileChange: true + restartOnFileChange: true, }); }; diff --git a/projects/akita-ng-fire/karma.conf.js b/projects/akita-ng-fire/karma.conf.js index aadc1009..5a1281d2 100644 --- a/projects/akita-ng-fire/karma.conf.js +++ b/projects/akita-ng-fire/karma.conf.js @@ -11,15 +11,15 @@ module.exports = function (config) { require('karma-mocha-reporter'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma'), ], client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { dir: require('path').join(__dirname, '../../coverage/akita-ng-fire'), reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true + fixWebpackSourcePaths: true, }, reporters: ['progress', 'kjhtml'], port: 9876, @@ -29,6 +29,6 @@ module.exports = function (config) { browsers: ['Chrome'], singleRun: false, restartOnFileChange: true, - reporters: ['mocha', 'kjhtml'] + reporters: ['mocha', 'kjhtml'], }); }; diff --git a/projects/akita-ng-fire/package.json b/projects/akita-ng-fire/package.json index 83493c2e..4bb3c3ed 100644 --- a/projects/akita-ng-fire/package.json +++ b/projects/akita-ng-fire/package.json @@ -11,9 +11,15 @@ "typescript": ">= 4.0.0" }, "author": "GrandSchtroumpf", - "contributors": ["fritzschoff"], + "contributors": [ + "fritzschoff" + ], "description": "A service to connect Akita and Angular Firestore", - "keywords": ["akita", "firebase", "angular"], + "keywords": [ + "akita", + "firebase", + "angular" + ], "license": "MIT", "repository": { "type": "git", diff --git a/projects/akita-ng-fire/rtdb/package.json b/projects/akita-ng-fire/rtdb/package.json index c075bb7c..b0817c73 100644 --- a/projects/akita-ng-fire/rtdb/package.json +++ b/projects/akita-ng-fire/rtdb/package.json @@ -1,7 +1,7 @@ { - "ngPackage": { - "lib": { - "entryFile": "./src/public-api.ts" - } + "ngPackage": { + "lib": { + "entryFile": "./src/public-api.ts" } - } \ No newline at end of file + } +} diff --git a/projects/akita-ng-fire/rtdb/src/public-api.ts b/projects/akita-ng-fire/rtdb/src/public-api.ts index 357bd497..fe96a6f0 100644 --- a/projects/akita-ng-fire/rtdb/src/public-api.ts +++ b/projects/akita-ng-fire/rtdb/src/public-api.ts @@ -1,2 +1,2 @@ -export * from './real-time.config' -export * from './real-time.service'; \ No newline at end of file +export * from './real-time.config'; +export * from './real-time.service'; diff --git a/projects/akita-ng-fire/rtdb/src/real-time.config.ts b/projects/akita-ng-fire/rtdb/src/real-time.config.ts index a7957533..807cfbac 100644 --- a/projects/akita-ng-fire/rtdb/src/real-time.config.ts +++ b/projects/akita-ng-fire/rtdb/src/real-time.config.ts @@ -1,4 +1,3 @@ - export interface RealTimeOptions { /** The name of the node in the database */ nodeName: string; @@ -7,6 +6,6 @@ export interface RealTimeOptions { /** Set the configuration for the collection service */ export function RealTimeConfig(options: Partial = {}) { return (constructor) => { - Object.keys(options).forEach(key => constructor[key] = options[key]); + Object.keys(options).forEach((key) => (constructor[key] = options[key])); }; } diff --git a/projects/akita-ng-fire/rtdb/src/real-time.service.ts b/projects/akita-ng-fire/rtdb/src/real-time.service.ts index b5c4977b..82867571 100644 --- a/projects/akita-ng-fire/rtdb/src/real-time.service.ts +++ b/projects/akita-ng-fire/rtdb/src/real-time.service.ts @@ -4,13 +4,15 @@ import { inject } from '@angular/core'; import { removeStoreEntity, upsertStoreEntity } from 'akita-ng-fire'; import { map, tap } from 'rxjs/operators'; -export class RealTimeService, EntityType = getEntityType> { - +export class RealTimeService< + S extends EntityState, + EntityType = getEntityType +> { protected rtdb: AngularFireDatabase; private nodePath: string; - private listRef: AngularFireList<(Partial | Partial[])>; + private listRef: AngularFireList | Partial[]>; constructor( protected store?: EntityStore, @@ -51,31 +53,43 @@ export class RealTimeService, EntityTy } /** - * @description just sync with node you specified when initialized the service class without updating the store + * @description just sync with node you specified when initialized the service class without updating the store */ syncNode() { - return this.listRef.valueChanges().pipe(map(value => this.formatFromDatabase(value))); + return this.listRef + .valueChanges() + .pipe(map((value) => this.formatFromDatabase(value))); } /** * @description sync the node with the store. `formatFromDatabase` will be called every time there is data incoming. */ syncNodeWithStore() { - return this.listRef.stateChanges().pipe(tap(data => { - switch (data.type) { - case 'child_added': { - upsertStoreEntity(this.store.storeName, this.formatFromDatabase(data.payload.toJSON()), data.key); - break; - } - case 'child_removed': { - removeStoreEntity(this.store.storeName, data.key); - break; - } - case 'child_changed': { - upsertStoreEntity(this.store.storeName, this.formatFromDatabase(data.payload.toJSON()), data.key); + return this.listRef.stateChanges().pipe( + tap((data) => { + switch (data.type) { + case 'child_added': { + upsertStoreEntity( + this.store.storeName, + this.formatFromDatabase(data.payload.toJSON()), + data.key + ); + break; + } + case 'child_removed': { + removeStoreEntity(this.store.storeName, data.key); + break; + } + case 'child_changed': { + upsertStoreEntity( + this.store.storeName, + this.formatFromDatabase(data.payload.toJSON()), + data.key + ); + } } - } - })); + }) + ); } /** @@ -84,7 +98,8 @@ export class RealTimeService, EntityTy */ add(entity: Partial): Promise { if (entity[this.idKey]) { - return this.rtdb.database.ref(this.nodePath + '/' + entity[this.idKey]) + return this.rtdb.database + .ref(this.nodePath + '/' + entity[this.idKey]) .set(this.formatToDatabase(entity as Partial), (error) => { if (error) { throw error; @@ -93,50 +108,65 @@ export class RealTimeService, EntityTy } if (Array.isArray(entity)) { const ids: string[] = []; - const promises = entity.map(e => { + const promises = entity.map((e) => { const id = this.rtdb.createPushId(); ids.push(id); - return this.listRef.set(id, { ...e, [this.idKey]: id }) + return this.listRef.set(id, { ...e, [this.idKey]: id }); }); return Promise.all(promises).then(() => ids); } else { const id = this.rtdb.createPushId(); - return this.listRef.set(id, this.formatToDatabase({ ...entity, [this.idKey]: id })).then(() => id); + return this.listRef + .set(id, this.formatToDatabase({ ...entity, [this.idKey]: id })) + .then(() => id); } } /** - * + * * @param id id of the entity * @param entity to update. */ update(id: string, entity: Partial | Partial[]); update(entity?: Partial | Partial[]); - update(idOrEntity?: string | Partial | Partial[], entity?: Partial | Partial[]) - : Promise | Promise | Error { + update( + idOrEntity?: string | Partial | Partial[], + entity?: Partial | Partial[] + ): Promise | Promise | Error { if (Array.isArray(idOrEntity)) { - return Promise.all(idOrEntity.map(e => this.listRef.update(e[this.idKey], this.formatToDatabase(e)))); + return Promise.all( + idOrEntity.map((e) => + this.listRef.update(e[this.idKey], this.formatToDatabase(e)) + ) + ); } else { if (typeof idOrEntity === 'string') { - return this.listRef.update(idOrEntity, this.formatToDatabase(entity as Partial)); + return this.listRef.update( + idOrEntity, + this.formatToDatabase(entity as Partial) + ); } else if (typeof idOrEntity === 'object') { const id = idOrEntity[this.idKey]; return this.listRef.update(id, this.formatToDatabase(idOrEntity)); } else { - return new Error(`Couldn\'t find corresponding entity/ies: ${idOrEntity}, ${entity}`); + return new Error( + `Couldn\'t find corresponding entity/ies: ${idOrEntity}, ${entity}` + ); } } } /** - * + * * @param id of the entity to remove */ remove(id: string) { try { return this.listRef.remove(id); } catch (error) { - return new Error(`Error while removing entity with this id: ${id}. ${error}`); + return new Error( + `Error while removing entity with this id: ${id}. ${error}` + ); } } diff --git a/projects/akita-ng-fire/src/lib/auth/auth.model.ts b/projects/akita-ng-fire/src/lib/auth/auth.model.ts index 40e38623..3b0324de 100644 --- a/projects/akita-ng-fire/src/lib/auth/auth.model.ts +++ b/projects/akita-ng-fire/src/lib/auth/auth.model.ts @@ -14,5 +14,5 @@ export const initialAuthState: FireAuthState = { uid: null, emailVerified: undefined, profile: null, - loading: false + loading: false, }; diff --git a/projects/akita-ng-fire/src/lib/auth/auth.service.ts b/projects/akita-ng-fire/src/lib/auth/auth.service.ts index f4006975..3a246659 100644 --- a/projects/akita-ng-fire/src/lib/auth/auth.service.ts +++ b/projects/akita-ng-fire/src/lib/auth/auth.service.ts @@ -1,6 +1,9 @@ import { inject } from '@angular/core'; import { AngularFireAuth } from '@angular/fire/auth'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'; +import { + AngularFirestore, + AngularFirestoreCollection, +} from '@angular/fire/firestore'; import firebase from 'firebase/app'; import { switchMap, tap, map } from 'rxjs/operators'; import { Observable, of, combineLatest } from 'rxjs'; @@ -8,15 +11,25 @@ import { Store } from '@datorama/akita'; import { FireAuthState, initialAuthState } from './auth.model'; import { WriteOptions, UpdateCallback } from '../utils/types'; -export const authProviders = ['github', 'google', 'microsoft', 'facebook', 'twitter', 'email', 'apple'] as const; +export const authProviders = [ + 'github', + 'google', + 'microsoft', + 'facebook', + 'twitter', + 'email', + 'apple', +] as const; -export type FireProvider = (typeof authProviders)[number]; +export type FireProvider = typeof authProviders[number]; type UserCredential = firebase.auth.UserCredential; type AuthProvider = firebase.auth.AuthProvider; /** Verify if provider is part of the list of Authentication provider provided by Firebase Auth */ export function isFireAuthProvider(provider: any): provider is FireProvider { - return typeof provider === 'string' && authProviders.includes(provider as any); + return ( + typeof provider === 'string' && authProviders.includes(provider as any) + ); } /** @@ -24,14 +37,17 @@ export function isFireAuthProvider(provider: any): provider is FireProvider { * @param user The user object returned by Firebase Auth * @param roles Keys of the custom claims inside the claim objet */ -export async function getCustomClaims(user: firebase.User, roles?: string | string[]): Promise> { +export async function getCustomClaims( + user: firebase.User, + roles?: string | string[] +): Promise> { const { claims } = await user.getIdTokenResult(); if (!roles) { return claims; } const keys = Array.isArray(roles) ? roles : [roles]; return Object.keys(claims) - .filter(key => keys.includes(key)) + .filter((key) => keys.includes(key)) .reduce((acc, key) => { acc[key] = claims[key]; return acc; @@ -44,18 +60,24 @@ export async function getCustomClaims(user: firebase.User, roles?: string | stri */ export function getAuthProvider(provider: FireProvider) { switch (provider) { - case 'email': return new firebase.auth.EmailAuthProvider(); - case 'facebook': return new firebase.auth.FacebookAuthProvider(); - case 'github': return new firebase.auth.GithubAuthProvider(); - case 'google': return new firebase.auth.GoogleAuthProvider(); - case 'microsoft': return new firebase.auth.OAuthProvider('microsoft.com'); - case 'twitter': return new firebase.auth.TwitterAuthProvider(); - case 'apple': return new firebase.auth.OAuthProvider('apple'); + case 'email': + return new firebase.auth.EmailAuthProvider(); + case 'facebook': + return new firebase.auth.FacebookAuthProvider(); + case 'github': + return new firebase.auth.GithubAuthProvider(); + case 'google': + return new firebase.auth.GoogleAuthProvider(); + case 'microsoft': + return new firebase.auth.OAuthProvider('microsoft.com'); + case 'twitter': + return new firebase.auth.TwitterAuthProvider(); + case 'apple': + return new firebase.auth.OAuthProvider('apple'); } } export class FireAuthService { - private collection: AngularFirestoreCollection; protected collectionPath = 'users'; protected db: AngularFirestore; @@ -97,7 +119,9 @@ export class FireAuthService { * @see getCustomClaims to get the custom claims out of the user * @note Can be overwritten */ - protected selectRoles(user: firebase.User): Promise | Observable { + protected selectRoles( + user: firebase.User + ): Promise | Observable { return of(null); } @@ -123,7 +147,10 @@ export class FireAuthService { * @param ctx The context given on signup * @note Should be override */ - protected createProfile(user: firebase.User, ctx?: any): Promise> | Partial { + protected createProfile( + user: firebase.User, + ctx?: any + ): Promise> | Partial { return { photoURL: user.photoURL, displayName: user.displayName, @@ -147,21 +174,26 @@ export class FireAuthService { return this.constructor['path'] || this.collectionPath; } - /** Start listening on User */ sync() { return this.auth.authState.pipe( - switchMap((user) => user ? combineLatest([ - of(user), - this.selectProfile(user), - this.selectRoles(user), - ]) : of([undefined, undefined, undefined])), + switchMap((user) => + user + ? combineLatest([ + of(user), + this.selectProfile(user), + this.selectRoles(user), + ]) + : of([undefined, undefined, undefined]) + ), tap(([user = {}, userProfile, roles]) => { const profile = this.formatFromFirestore(userProfile); const { uid, emailVerified } = user; this.store.update({ uid, emailVerified, profile, roles } as any); }), - map(([user, userProfile, roles]) => user ? [user, this.formatFromFirestore(userProfile), roles] : null), + map(([user, userProfile, roles]) => + user ? [user, this.formatFromFirestore(userProfile), roles] : null + ) ); } @@ -197,10 +229,16 @@ export class FireAuthService { } const { ref } = this.collection.doc(user.uid); if (typeof profile === 'function') { - return this.db.firestore.runTransaction(async tx => { + return this.db.firestore.runTransaction(async (tx) => { const snapshot = await tx.get(ref); - const doc = Object.freeze({ ...snapshot.data(), [this.idKey]: snapshot.id }); - const data = (profile as UpdateCallback)(this.formatToFirestore(doc), tx); + const doc = Object.freeze({ + ...snapshot.data(), + [this.idKey]: snapshot.id, + }); + const data = (profile as UpdateCallback)( + this.formatToFirestore(doc), + tx + ); tx.update(ref, data); if (this.onUpdate) { await this.onUpdate(data, { write: tx, ctx: options.ctx }); @@ -221,15 +259,25 @@ export class FireAuthService { } /** Create a user based on email and password */ - async signup(email: string, password: string, options: WriteOptions = {}): Promise { - const cred = await this.auth.createUserWithEmailAndPassword(email, password); + async signup( + email: string, + password: string, + options: WriteOptions = {} + ): Promise { + const cred = await this.auth.createUserWithEmailAndPassword( + email, + password + ); const { write = this.db.firestore.batch(), ctx } = options; if (this.onSignup) { await this.onSignup(cred, { write, ctx }); } const profile = await this.createProfile(cred.user, ctx); const { ref } = this.collection.doc(cred.user.uid); - (write as firebase.firestore.WriteBatch).set(ref, this.formatToFirestore(profile)); + (write as firebase.firestore.WriteBatch).set( + ref, + this.formatToFirestore(profile) + ); if (this.onCreate) { await this.onCreate(profile, { write, ctx }); } @@ -241,12 +289,25 @@ export class FireAuthService { /** Signin with email & password, provider name, provider objet or custom token */ // tslint:disable-next-line: unified-signatures - signin(email: string, password: string, options?: WriteOptions): Promise; - signin(authProvider: AuthProvider, options?: WriteOptions): Promise; - signin(provider?: FireProvider, options?: WriteOptions): Promise; + signin( + email: string, + password: string, + options?: WriteOptions + ): Promise; + signin( + authProvider: AuthProvider, + options?: WriteOptions + ): Promise; + signin( + provider?: FireProvider, + options?: WriteOptions + ): Promise; // tslint:disable-next-line: unified-signatures signin(token: string, options?: WriteOptions): Promise; - async signin(provider?: FireProvider | AuthProvider | string, passwordOrOptions?: string | WriteOptions): Promise { + async signin( + provider?: FireProvider | AuthProvider | string, + passwordOrOptions?: string | WriteOptions + ): Promise { this.store.setLoading(true); let profile; try { @@ -254,8 +315,15 @@ export class FireAuthService { const write = this.db.firestore.batch(); if (!provider) { cred = await this.auth.signInAnonymously(); - } else if (passwordOrOptions && typeof provider === 'string' && typeof passwordOrOptions === 'string') { - cred = await this.auth.signInWithEmailAndPassword(provider, passwordOrOptions); + } else if ( + passwordOrOptions && + typeof provider === 'string' && + typeof passwordOrOptions === 'string' + ) { + cred = await this.auth.signInWithEmailAndPassword( + provider, + passwordOrOptions + ); } else if (typeof provider === 'object') { cred = await this.auth.signInWithPopup(provider); } else if (isFireAuthProvider(provider)) { @@ -305,7 +373,9 @@ export class FireAuthService { } catch (err) { this.store.setLoading(false); if (err.code === 'auth/operation-not-allowed') { - console.warn('You tried to connect with a disabled auth provider. Enable it in Firebase console'); + console.warn( + 'You tried to connect with a disabled auth provider. Enable it in Firebase console' + ); } throw err; } diff --git a/projects/akita-ng-fire/src/lib/collection-group/collection-group.service.ts b/projects/akita-ng-fire/src/lib/collection-group/collection-group.service.ts index d340730a..21613701 100644 --- a/projects/akita-ng-fire/src/lib/collection-group/collection-group.service.ts +++ b/projects/akita-ng-fire/src/lib/collection-group/collection-group.service.ts @@ -1,7 +1,16 @@ import { inject } from '@angular/core'; -import { EntityStore, EntityState, withTransaction, getEntityType } from '@datorama/akita'; +import { + EntityStore, + EntityState, + withTransaction, + getEntityType, +} from '@datorama/akita'; import { AngularFirestore, QueryGroupFn } from '@angular/fire/firestore'; -import { setLoading, syncStoreFromDocAction, resetStore } from '../utils/sync-from-action'; +import { + setLoading, + syncStoreFromDocAction, + resetStore, +} from '../utils/sync-from-action'; import { getStoreName } from '../utils/store-options'; import { Observable } from 'rxjs'; import { SyncOptions } from '../utils/types'; @@ -20,9 +29,9 @@ export abstract class CollectionGroupService { } /** - * Function triggered when getting data from firestore - * @note should be overrided - */ + * Function triggered when getting data from firestore + * @note should be overrided + */ protected formatFromFirestore(entity: any): getEntityType { return entity; } @@ -43,7 +52,10 @@ export abstract class CollectionGroupService { /** Sync the collection group with the store */ public syncCollection(queryGroupFn?: QueryGroupFn | Partial); - public syncCollection(queryGroupFn: QueryGroupFn, storeOptions?: Partial); + public syncCollection( + queryGroupFn: QueryGroupFn, + storeOptions?: Partial + ); public syncCollection( queryOrOptions?: QueryGroupFn | Partial, storeOptions: Partial = { loading: true } @@ -66,19 +78,34 @@ export abstract class CollectionGroupService { setLoading(storeName, true); } - return this.db.collectionGroup(this.collectionId, query).stateChanges().pipe( - withTransaction(actions => syncStoreFromDocAction(storeName, actions, this.idKey, this.resetOnUpdate, - this.mergeRef, - (entity) => this.formatFromFirestore(entity))) - ); + return this.db + .collectionGroup(this.collectionId, query) + .stateChanges() + .pipe( + withTransaction((actions) => + syncStoreFromDocAction( + storeName, + actions, + this.idKey, + this.resetOnUpdate, + this.mergeRef, + (entity) => this.formatFromFirestore(entity) + ) + ) + ); } /** Return a snapshot of the collection group */ - public async getValue(queryGroupFn?: QueryGroupFn): Promise[]> { - const snapshot = await this.db.collectionGroup(this.collectionId, queryGroupFn).get().toPromise(); - return snapshot.docs.map(doc => { + public async getValue( + queryGroupFn?: QueryGroupFn + ): Promise[]> { + const snapshot = await this.db + .collectionGroup(this.collectionId, queryGroupFn) + .get() + .toPromise(); + return snapshot.docs.map((doc) => { const entity = doc.data() as getEntityType; return this.formatFromFirestore(entity); - }) + }); } } diff --git a/projects/akita-ng-fire/src/lib/collection/collection.config.ts b/projects/akita-ng-fire/src/lib/collection/collection.config.ts index 5e1c3e60..f7c6e6a2 100644 --- a/projects/akita-ng-fire/src/lib/collection/collection.config.ts +++ b/projects/akita-ng-fire/src/lib/collection/collection.config.ts @@ -1,11 +1,10 @@ - export interface CollectionOptions { /** The path of the collection in Firestore */ path: string; /** The key to use as an id for the document in Firestore. Default is store.idKey */ idKey?: string; - /** - * If true we will remove the entity from the store + /** + * If true we will remove the entity from the store * and add a new one in other to make sure * to get the rid of the any old keys that maybe still persist. */ @@ -21,6 +20,6 @@ export interface CollectionOptions { /** Set the configuration for the collection service */ export function CollectionConfig(options: Partial = {}) { return (constructor) => { - Object.keys(options).forEach(key => constructor[key] = options[key]); + Object.keys(options).forEach((key) => (constructor[key] = options[key])); }; } diff --git a/projects/akita-ng-fire/src/lib/collection/collection.guard.ts b/projects/akita-ng-fire/src/lib/collection/collection.guard.ts index 994f0b08..bddf6e57 100644 --- a/projects/akita-ng-fire/src/lib/collection/collection.guard.ts +++ b/projects/akita-ng-fire/src/lib/collection/collection.guard.ts @@ -5,7 +5,7 @@ import { Router, UrlTree, ActivatedRouteSnapshot, - RouterStateSnapshot + RouterStateSnapshot, } from '@angular/router'; import { Subscription, Observable, Subject } from 'rxjs'; import { CollectionService } from './collection.service'; @@ -22,17 +22,21 @@ export interface CollectionRouteData { } export function CollectionGuardConfig(data: Partial) { - return constructor => { - Object.keys(data).forEach(key => (constructor[key] = data[key])); + return (constructor) => { + Object.keys(data).forEach((key) => (constructor[key] = data[key])); }; } -type GuardService | FireAuthState> = S extends FireAuthState - ? FireAuthService : S extends EntityState - ? CollectionService : never; +type GuardService | FireAuthState> = + S extends FireAuthState + ? FireAuthService + : S extends EntityState + ? CollectionService + : never; export class CollectionGuard | FireAuthState = any> - implements CanActivate, CanDeactivate { + implements CanActivate, CanDeactivate +{ private subscription: Subscription; protected router: Router; @@ -63,7 +67,10 @@ export class CollectionGuard | FireAuthState = any> // Can be override by the extended class /** The method to subscribe to while route is active */ - protected sync(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + protected sync( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable { const { queryFn = this.queryFn } = next.data as CollectionRouteData; if (this.service instanceof FireAuthService) { return this.service.sync(); @@ -72,37 +79,38 @@ export class CollectionGuard | FireAuthState = any> } } - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - const { - redirect = this.redirect, - awaitSync = this.awaitSync - } = next.data as CollectionRouteData; + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Promise { + const { redirect = this.redirect, awaitSync = this.awaitSync } = + next.data as CollectionRouteData; return new Promise((res, rej) => { if (awaitSync) { const unsubscribe = new Subject(); - this.subscription = this.sync(next, state).pipe( - takeUntil(unsubscribe), - ).subscribe({ - next: (result) => { - if (result instanceof UrlTree) { - return res(result); - } - switch (typeof result) { - case 'string': - unsubscribe.next(); - unsubscribe.complete(); - return res(this.router.parseUrl(result)); - case 'boolean': + this.subscription = this.sync(next, state) + .pipe(takeUntil(unsubscribe)) + .subscribe({ + next: (result) => { + if (result instanceof UrlTree) { return res(result); - default: - return res(true); - } - }, - error: (err) => { - res(this.router.parseUrl(redirect || '')); - throw new Error(err); - } - }); + } + switch (typeof result) { + case 'string': + unsubscribe.next(); + unsubscribe.complete(); + return res(this.router.parseUrl(result)); + case 'boolean': + return res(result); + default: + return res(true); + } + }, + error: (err) => { + res(this.router.parseUrl(redirect || '')); + throw new Error(err); + }, + }); } else { this.subscription = this.sync(next, state).subscribe(); res(true); diff --git a/projects/akita-ng-fire/src/lib/collection/collection.service.ts b/projects/akita-ng-fire/src/lib/collection/collection.service.ts index b19f4b65..c400d3a9 100644 --- a/projects/akita-ng-fire/src/lib/collection/collection.service.ts +++ b/projects/akita-ng-fire/src/lib/collection/collection.service.ts @@ -5,7 +5,7 @@ import { DocumentChangeAction, QueryFn, QueryGroupFn, - Query + Query, } from '@angular/fire/firestore'; import { EntityStore, @@ -23,9 +23,15 @@ import { upsertStoreEntity, removeStoreEntity, setActive, - resetStore + resetStore, } from '../utils/sync-from-action'; -import { WriteOptions, SyncOptions, PathParams, UpdateCallback, AtomicWrite } from '../utils/types'; +import { + WriteOptions, + SyncOptions, + PathParams, + UpdateCallback, + AtomicWrite, +} from '../utils/types'; import { Observable, isObservable, of, combineLatest } from 'rxjs'; import { tap, map, switchMap } from 'rxjs/operators'; import { getStoreName } from '../utils/store-options'; @@ -33,13 +39,15 @@ import { pathWithParams } from '../utils/path-with-params'; import { hasChildGetter } from '../utils/has-path-getter'; import { shareWithDelay } from '../utils/share-delay'; -export type CollectionState = EntityState & ActiveState; +export type CollectionState = EntityState & + ActiveState; export type DocOptions = { path: string } | { id: string }; -export type GetRefs = - idOrQuery extends (infer I)[] ? firebase.firestore.DocumentReference[] - : idOrQuery extends string ? firebase.firestore.DocumentReference +export type GetRefs = idOrQuery extends (infer I)[] + ? firebase.firestore.DocumentReference[] + : idOrQuery extends string + ? firebase.firestore.DocumentReference : firebase.firestore.CollectionReference; function isArray(entityOrArray: E | E[]): entityOrArray is E[] { @@ -47,11 +55,16 @@ function isArray(entityOrArray: E | E[]): entityOrArray is E[] { } /** check is an Atomic write is a transaction */ -export function isTransaction(write: AtomicWrite): write is firebase.firestore.Transaction { +export function isTransaction( + write: AtomicWrite +): write is firebase.firestore.Transaction { return write && !!write['get']; } -export class CollectionService, EntityType = getEntityType> { +export class CollectionService< + S extends EntityState, + EntityType = getEntityType +> { // keep memory of the current ids to listen to (for syncManyDocs) private idsToListen: Record = {}; private memoPath: Record> = {}; @@ -74,7 +87,11 @@ export class CollectionService, Entity private collectionPath?: string, db?: AngularFirestore ) { - if (!hasChildGetter(this, CollectionService, 'path') && !this.constructor['path'] && !this.collectionPath) { + if ( + !hasChildGetter(this, CollectionService, 'path') && + !this.constructor['path'] && + !this.collectionPath + ) { throw new Error('You should provide a path to the collection'); } try { @@ -84,7 +101,10 @@ export class CollectionService, Entity } } - private fromMemo(key: string | Query, cb: () => Observable): Observable { + private fromMemo( + key: string | Query, + cb: () => Observable + ): Observable { if (!this.useMemorization) return cb(); if (typeof key === 'string') { if (!this.memoPath[key]) { @@ -103,14 +123,13 @@ export class CollectionService, Entity } protected getPath(options: PathParams) { - return (options && options.params) + return options && options.params ? pathWithParams(this.path, options.params) : this.currentPath; } get idKey() { - return this.constructor['idKey'] - || this.store ? this.store.idKey : 'id'; + return this.constructor['idKey'] || this.store ? this.store.idKey : 'id'; } /** The path to the collection in Firestore */ @@ -121,7 +140,9 @@ export class CollectionService, Entity /** A snapshot of the path */ get currentPath(): string { if (isObservable(this.path)) { - throw new Error('Cannot get a snapshot of the path if it is an Observable'); + throw new Error( + 'Cannot get a snapshot of the path if it is an Observable' + ); } return this.path; } @@ -200,7 +221,6 @@ export class CollectionService, Entity queryOrOptions?: QueryFn | Partial, syncOptions: Partial = { loading: true } ): Observable[]> { - let path: string; let queryFn: QueryFn; // check type of pathOrQuery @@ -234,11 +254,21 @@ export class CollectionService, Entity setLoading(storeName, true); } // Start Listening - return this.db.collection(path, queryFn).stateChanges().pipe( - withTransaction(actions => - syncStoreFromDocAction(storeName, actions, this.idKey, this.resetOnUpdate, this.mergeRef, - (entity) => this.formatFromFirestore(entity))) - ); + return this.db + .collection(path, queryFn) + .stateChanges() + .pipe( + withTransaction((actions) => + syncStoreFromDocAction( + storeName, + actions, + this.idKey, + this.resetOnUpdate, + this.mergeRef, + (entity) => this.formatFromFirestore(entity) + ) + ) + ); } /** @@ -279,7 +309,9 @@ export class CollectionService, Entity path = this.currentPath; syncOptions = idOrQuery; } else { - throw new Error('1ier parameter if either a string, a queryFn or a StoreOption'); + throw new Error( + '1ier parameter if either a string, a queryFn or a StoreOption' + ); } if (typeof queryOrOption === 'function') { @@ -300,11 +332,21 @@ export class CollectionService, Entity } const collectionId = path.split('/').pop(); - return this.db.collectionGroup(collectionId, query).stateChanges().pipe( - withTransaction(actions => - syncStoreFromDocAction(storeName, actions, this.idKey, this.resetOnUpdate, - this.mergeRef, (entity) => this.formatFromFirestore(entity))) - ); + return this.db + .collectionGroup(collectionId, query) + .stateChanges() + .pipe( + withTransaction((actions) => + syncStoreFromDocAction( + storeName, + actions, + this.idKey, + this.resetOnUpdate, + this.mergeRef, + (entity) => this.formatFromFirestore(entity) + ) + ) + ); } /** @@ -334,11 +376,11 @@ export class CollectionService, Entity setLoading(storeName, true); } return ids$.pipe( - switchMap((ids => { + switchMap((ids) => { // Remove previous ids that have changed const previousIds = this.idsToListen[storeName]; if (previousIds) { - const idsToRemove = previousIds.filter(id => !ids.includes(id)); + const idsToRemove = previousIds.filter((id) => !ids.includes(id)); removeStoreEntity(storeName, idsToRemove); } this.idsToListen[storeName] = ids; @@ -347,16 +389,24 @@ export class CollectionService, Entity return of([]); } // Sync all docs - const syncs = ids.map(id => { + const syncs = ids.map((id) => { const path = `${this.getPath(syncOptions)}/${id}`; return this.db.doc(path).snapshotChanges(); }); return combineLatest(syncs).pipe( - tap((actions) => actions.map(action => { - syncStoreFromDocActionSnapshot(storeName, action, this.idKey, this.mergeRef, (entity) => this.formatFromFirestore(entity)); - })) + tap((actions) => + actions.map((action) => { + syncStoreFromDocActionSnapshot( + storeName, + action, + this.idKey, + this.mergeRef, + (entity) => this.formatFromFirestore(entity) + ); + }) + ) ); - })) + }) ); } @@ -382,19 +432,25 @@ export class CollectionService, Entity if (syncOptions.loading) { setLoading(storeName, true); } - return this.db.doc(path).valueChanges().pipe( - map(entity => { - if (!entity) { + return this.db + .doc(path) + .valueChanges() + .pipe( + map((entity) => { + if (!entity) { + setLoading(storeName, false); + // note: We don't removeEntity as it would result in weird behavior + return undefined; + } + const data = this.formatFromFirestore({ + [this.idKey]: id, + ...entity, + }); + upsertStoreEntity(storeName, data, id); setLoading(storeName, false); - // note: We don't removeEntity as it would result in weird behavior - return undefined; - } - const data = this.formatFromFirestore({ [this.idKey]: id, ...entity }); - upsertStoreEntity(storeName, data, id); - setLoading(storeName, false); - return data; - }) - ); + return data; + }) + ); } /** @@ -406,7 +462,9 @@ export class CollectionService, Entity syncActive( options: S['active'] extends any[] ? string[] : DocOptions, syncOptions?: Partial - ): S['active'] extends any[] ? Observable : Observable; + ): S['active'] extends any[] + ? Observable + : Observable; syncActive( options: string[] | DocOptions, syncOptions?: Partial @@ -414,11 +472,13 @@ export class CollectionService, Entity const storeName = getStoreName(this.store, syncOptions); if (Array.isArray(options)) { return this.syncManyDocs(options, syncOptions).pipe( - tap(_ => setActive(storeName, options)) + tap((_) => setActive(storeName, options)) ); } else { return this.syncDoc(options, syncOptions).pipe( - tap(entity => entity ? setActive(storeName, entity[this.idKey]) : null) + tap((entity) => + entity ? setActive(storeName, entity[this.idKey]) : null + ) ); } } @@ -428,20 +488,30 @@ export class CollectionService, Entity /////////////// /** Return the reference of the document(s) or collection */ - public getRef(options?: Partial): firebase.firestore.CollectionReference; - public getRef(ids?: string[], options?: Partial): firebase.firestore.DocumentReference[]; - public getRef(id?: string, options?: Partial): firebase.firestore.DocumentReference; + public getRef( + options?: Partial + ): firebase.firestore.CollectionReference; + public getRef( + ids?: string[], + options?: Partial + ): firebase.firestore.DocumentReference[]; + public getRef( + id?: string, + options?: Partial + ): firebase.firestore.DocumentReference; public getRef( idOrQuery?: string | string[] | Partial, options: Partial = {} - ): GetRefs<(typeof idOrQuery)> { + ): GetRefs { const path = this.getPath(options); // If path targets a collection ( odd number of segments after the split ) if (typeof idOrQuery === 'string') { return this.db.doc(`${path}/${idOrQuery}`).ref; } if (Array.isArray(idOrQuery)) { - return idOrQuery.map(id => this.db.doc(`${path}/${id}`).ref); + return idOrQuery.map( + (id) => this.db.doc(`${path}/${id}`).ref + ); } else if (typeof idOrQuery === 'object') { const subpath = this.getPath(idOrQuery); return this.db.collection(subpath).ref; @@ -450,13 +520,21 @@ export class CollectionService, Entity } } - /** Return the current value of the path from Firestore */ public async getValue(options?: Partial): Promise; - public async getValue(ids?: string[], options?: Partial): Promise; + public async getValue( + ids?: string[], + options?: Partial + ): Promise; // tslint:disable-next-line: unified-signatures - public async getValue(query?: QueryFn, options?: Partial): Promise; - public async getValue(id: string, options?: Partial): Promise; + public async getValue( + query?: QueryFn, + options?: Partial + ): Promise; + public async getValue( + id: string, + options?: Partial + ): Promise; public async getValue( idOrQuery?: string | string[] | QueryFn | Partial, options: Partial = {} @@ -464,16 +542,23 @@ export class CollectionService, Entity const path = this.getPath(options); // If path targets a collection ( odd number of segments after the split ) if (typeof idOrQuery === 'string') { - const snapshot = await this.db.doc(`${path}/${idOrQuery}`).ref.get(); + const snapshot = await this.db + .doc(`${path}/${idOrQuery}`) + .ref.get(); return snapshot.exists - ? this.formatFromFirestore({ ...snapshot.data(), [this.idKey]: snapshot.id }) + ? this.formatFromFirestore({ + ...snapshot.data(), + [this.idKey]: snapshot.id, + }) : null; } let docs: firebase.firestore.QueryDocumentSnapshot[]; if (Array.isArray(idOrQuery)) { - docs = await Promise.all(idOrQuery.map(id => { - return this.db.doc(`${path}/${id}`).ref.get(); - })); + docs = await Promise.all( + idOrQuery.map((id) => { + return this.db.doc(`${path}/${id}`).ref.get(); + }) + ); } else if (typeof idOrQuery === 'function') { const { ref } = this.db.collection(path); const snaphot = await idOrQuery(ref).get(); @@ -486,20 +571,29 @@ export class CollectionService, Entity const snapshot = await this.db.collection(path, idOrQuery).ref.get(); docs = snapshot.docs; } - return docs.filter(doc => doc.exists) - .map(doc => { + return docs + .filter((doc) => doc.exists) + .map((doc) => { return { ...doc.data(), [this.idKey]: doc.id }; }) - .map(doc => this.formatFromFirestore(doc)); + .map((doc) => this.formatFromFirestore(doc)); } - /** Listen to the change of values of the path from Firestore */ public valueChanges(options?: Partial): Observable; - public valueChanges(ids?: string[], options?: Partial): Observable; + public valueChanges( + ids?: string[], + options?: Partial + ): Observable; // tslint:disable-next-line: unified-signatures - public valueChanges(query?: QueryFn, options?: Partial): Observable; - public valueChanges(id: string, options?: Partial): Observable; + public valueChanges( + query?: QueryFn, + options?: Partial + ): Observable; + public valueChanges( + id: string, + options?: Partial + ): Observable; public valueChanges( idOrQuery?: string | string[] | QueryFn | Partial, options: Partial = {} @@ -510,13 +604,13 @@ export class CollectionService, Entity const key = `${path}/${idOrQuery}`; const query = () => this.db.doc(key).valueChanges(); return this.fromMemo(key, query).pipe( - map(doc => this.formatFromFirestore(doc)) + map((doc) => this.formatFromFirestore(doc)) ); } let docs$: Observable; if (Array.isArray(idOrQuery)) { if (!idOrQuery.length) return of([]); - const queries = idOrQuery.map(id => { + const queries = idOrQuery.map((id) => { const key = `${path}/${id}`; const query = () => this.db.doc(key).valueChanges(); return this.fromMemo(key, query) as Observable; @@ -524,7 +618,8 @@ export class CollectionService, Entity docs$ = combineLatest(queries); } else if (typeof idOrQuery === 'function') { const query = idOrQuery(this.db.collection(path).ref); - const cb = () => this.db.collection(path, idOrQuery).valueChanges(); + const cb = () => + this.db.collection(path, idOrQuery).valueChanges(); docs$ = this.fromMemo(query, cb); } else if (typeof idOrQuery === 'object') { const subpath = this.getPath(idOrQuery); @@ -533,11 +628,12 @@ export class CollectionService, Entity docs$ = this.fromMemo(query, cb); } else { const query = this.db.collection(path).ref; - const cb = () => this.db.collection(path, idOrQuery).valueChanges(); + const cb = () => + this.db.collection(path, idOrQuery).valueChanges(); docs$ = this.fromMemo(query, cb); } return docs$.pipe( - map(docs => docs.map(doc => this.formatFromFirestore(doc))) + map((docs) => docs.map((doc) => this.formatFromFirestore(doc))) ); } @@ -557,8 +653,10 @@ export class CollectionService, Entity * Run a transaction * @note alias for `angularFirestore.firestore.runTransaction()` */ - runTransaction(cb: Parameters[0]) { - return this.db.firestore.runTransaction(tx => cb(tx)); + runTransaction( + cb: Parameters[0] + ) { + return this.db.firestore.runTransaction((tx) => cb(tx)); } /** @@ -566,31 +664,33 @@ export class CollectionService, Entity * @param documents One or many documents * @param options options to write the document on firestore */ - async upsert | Partial[])>( + async upsert | Partial[]>( documents: D, options: WriteOptions = {} ): Promise { const doesExist = async (doc: Partial) => { const ref = this.getRef(doc[this.idKey] as string); - const { exists } = await (isTransaction(options.write) ? options.write.get(ref) : ref.get()); + const { exists } = await (isTransaction(options.write) + ? options.write.get(ref) + : ref.get()); return exists; }; if (!isArray(documents)) { return (await doesExist(documents as Partial)) - ? this.update(documents, options).then(_ => documents[this.idKey]) + ? this.update(documents, options).then((_) => documents[this.idKey]) : this.add(documents, options); } const toAdd = []; const toUpdate = []; for (const doc of documents) { - (await doesExist(doc)) - ? toUpdate.push(doc) - : toAdd.push(doc); + (await doesExist(doc)) ? toUpdate.push(doc) : toAdd.push(doc); } return Promise.all([ this.add(toAdd, options), - this.update(toUpdate, options).then(_ => toUpdate.map(doc => doc[this.idKey] as string)) + this.update(toUpdate, options).then((_) => + toUpdate.map((doc) => doc[this.idKey] as string) + ), ]).then(([added, updated]) => added.concat(updated) as any); } @@ -599,18 +699,20 @@ export class CollectionService, Entity * @param docs A document or a list of document * @param options options to write the document on firestore */ - async add | Partial[])>( + async add | Partial[]>( documents: D, options: WriteOptions = {} ): Promise { - const docs: Partial[] = (Array.isArray(documents) ? documents : [documents]) as any; + const docs: Partial[] = ( + Array.isArray(documents) ? documents : [documents] + ) as any; const { write = this.batch(), ctx } = options; const path = this.getPath(options); - const operations = docs.map(async doc => { + const operations = docs.map(async (doc) => { const id = doc[this.idKey] || this.db.createId(); const data = this.formatToFirestore({ ...doc, [this.idKey]: id }); const { ref } = this.db.doc(`${path}/${id}`); - (write as firebase.firestore.WriteBatch).set(ref, (data)); + (write as firebase.firestore.WriteBatch).set(ref, data); if (this.onCreate) { await this.onCreate(data, { write, ctx }); } @@ -634,7 +736,7 @@ export class CollectionService, Entity const path = this.getPath(options); const ids: string[] = Array.isArray(id) ? id : [id]; - const operations = ids.map(async docId => { + const operations = ids.map(async (docId) => { const { ref } = this.db.doc(`${path}/${docId}`); write.delete(ref); if (this.onDelete) { @@ -652,23 +754,39 @@ export class CollectionService, Entity async removeAll(options: WriteOptions = {}) { const path = this.getPath(options); const snapshot = await this.db.collection(path).ref.get(); - const ids = snapshot.docs.map(doc => doc.id); + const ids = snapshot.docs.map((doc) => doc.id); return this.remove(ids, options); } /** * Update one or several document in Firestore */ - update(entity: Partial | Partial[], options?: WriteOptions): Promise; - update(id: string | string[], entityChanges: Partial, options?: WriteOptions): Promise; - update(ids: string | string[], stateFunction: UpdateCallback, options?: WriteOptions) - : Promise; + update( + entity: Partial | Partial[], + options?: WriteOptions + ): Promise; + update( + id: string | string[], + entityChanges: Partial, + options?: WriteOptions + ): Promise; + update( + ids: string | string[], + stateFunction: UpdateCallback, + options?: WriteOptions + ): Promise; async update( - idsOrEntity: Partial | Partial[] | string | string[], - stateFnOrWrite?: UpdateCallback | Partial | WriteOptions, + idsOrEntity: + | Partial + | Partial[] + | string + | string[], + stateFnOrWrite?: + | UpdateCallback + | Partial + | WriteOptions, options: WriteOptions = {} ): Promise { - let ids: string[] = []; let stateFunction: UpdateCallback; let getData: (docId: string) => Partial; @@ -677,18 +795,20 @@ export class CollectionService, Entity return typeof value === 'object' && value[this.idKey]; }; const isEntityArray = (values): values is Partial[] => { - return Array.isArray(values) && values.every(value => isEntity(value)); + return Array.isArray(values) && values.every((value) => isEntity(value)); }; if (isEntity(idsOrEntity)) { ids = [idsOrEntity[this.idKey]]; getData = () => idsOrEntity; - options = stateFnOrWrite as WriteOptions || {}; + options = (stateFnOrWrite as WriteOptions) || {}; } else if (isEntityArray(idsOrEntity)) { - const entityMap = new Map(idsOrEntity.map(entity => [entity[this.idKey] as string, entity])); + const entityMap = new Map( + idsOrEntity.map((entity) => [entity[this.idKey] as string, entity]) + ); ids = Array.from(entityMap.keys()); - getData = docId => entityMap.get(docId); - options = stateFnOrWrite as WriteOptions || {}; + getData = (docId) => entityMap.get(docId); + options = (stateFnOrWrite as WriteOptions) || {}; } else if (typeof stateFnOrWrite === 'function') { ids = Array.isArray(idsOrEntity) ? idsOrEntity : [idsOrEntity]; stateFunction = stateFnOrWrite as UpdateCallback; @@ -696,7 +816,9 @@ export class CollectionService, Entity ids = Array.isArray(idsOrEntity) ? idsOrEntity : [idsOrEntity]; getData = () => stateFnOrWrite as Partial; } else { - throw new Error('Passed parameters match none of the function signatures.'); + throw new Error( + 'Passed parameters match none of the function signatures.' + ); } const { ctx } = options; @@ -708,11 +830,14 @@ export class CollectionService, Entity // If update depends on the entity, use transaction if (stateFunction) { - return this.db.firestore.runTransaction(async tx => { - const operations = ids.map(async id => { + return this.db.firestore.runTransaction(async (tx) => { + const operations = ids.map(async (id) => { const { ref } = this.db.doc(`${path}/${id}`); const snapshot = await tx.get(ref); - const doc = Object.freeze({ ...snapshot.data(), [this.idKey]: id } as EntityType); + const doc = Object.freeze({ + ...snapshot.data(), + [this.idKey]: id, + } as EntityType); const data = await stateFunction(doc, tx); tx.update(ref, this.formatToFirestore(data)); if (this.onUpdate) { @@ -724,10 +849,12 @@ export class CollectionService, Entity }); } else { const { write = this.batch() } = options; - const operations = ids.map(async docId => { + const operations = ids.map(async (docId) => { const doc = Object.freeze(getData(docId)); if (!docId) { - throw new Error(`Document should have an unique id to be updated, but none was found in ${doc}`); + throw new Error( + `Document should have an unique id to be updated, but none was found in ${doc}` + ); } const { ref } = this.db.doc(`${path}/${docId}`); write.update(ref, this.formatToFirestore(doc)); diff --git a/projects/akita-ng-fire/src/lib/utils/cancellation.ts b/projects/akita-ng-fire/src/lib/utils/cancellation.ts index 2542a6ba..b0c56920 100644 --- a/projects/akita-ng-fire/src/lib/utils/cancellation.ts +++ b/projects/akita-ng-fire/src/lib/utils/cancellation.ts @@ -14,19 +14,22 @@ export interface WaitForCancelOptions { } export function shouldCancel({ validate, cancel }: ShouldCancelOptions) { - return race([validate.pipe(map(_ => false)), cancel.pipe(map(_ => true))]); + return race([ + validate.pipe(map((_) => false)), + cancel.pipe(map((_) => true)), + ]); } export async function waitForCancel({ startWith, endWith, shouldValidate, - shouldCancel + shouldCancel, }: WaitForCancelOptions) { startWith(); const cancelled = await race([ - shouldValidate.pipe(map(_ => false)), - shouldCancel.pipe(map(_ => true)) + shouldValidate.pipe(map((_) => false)), + shouldCancel.pipe(map((_) => true)), ]).toPromise(); endWith(cancelled); } diff --git a/projects/akita-ng-fire/src/lib/utils/has-path-getter.ts b/projects/akita-ng-fire/src/lib/utils/has-path-getter.ts index e589dcff..6768148d 100644 --- a/projects/akita-ng-fire/src/lib/utils/has-path-getter.ts +++ b/projects/akita-ng-fire/src/lib/utils/has-path-getter.ts @@ -5,10 +5,17 @@ * @param currentClass Checks prototype chain until this class * @example getPropertyDescriptor(this, 'path', CollectionService) */ -export function getPropertyDescriptor(instance: any, property: string, currentClass: any = Object): PropertyDescriptor { +export function getPropertyDescriptor( + instance: any, + property: string, + currentClass: any = Object +): PropertyDescriptor { const prototype = Object.getPrototypeOf(instance); if (!prototype || !(prototype instanceof currentClass)) return; - return Object.getOwnPropertyDescriptor(prototype, property) || getPropertyDescriptor(prototype, property, currentClass); + return ( + Object.getOwnPropertyDescriptor(prototype, property) || + getPropertyDescriptor(prototype, property, currentClass) + ); } /** @@ -18,7 +25,11 @@ export function getPropertyDescriptor(instance: any, property: string, currentCl * @param property Property name to check * @example hasChildGetter(this, CollectionService, 'path') */ -export function hasChildGetter(instance: any, parentClass: any, property: string): boolean { +export function hasChildGetter( + instance: any, + parentClass: any, + property: string +): boolean { const descriptor = getPropertyDescriptor(instance, property, parentClass); return descriptor && descriptor.get && true; } diff --git a/projects/akita-ng-fire/src/lib/utils/httpsCallable.ts b/projects/akita-ng-fire/src/lib/utils/httpsCallable.ts index fdd81a04..6d876223 100644 --- a/projects/akita-ng-fire/src/lib/utils/httpsCallable.ts +++ b/projects/akita-ng-fire/src/lib/utils/httpsCallable.ts @@ -5,12 +5,14 @@ import { AngularFireFunctions } from '@angular/fire/functions'; * @param name of the cloud function * @param params you want to set */ -export async function callFunction any>, - N extends Extract>( - functions: AngularFireFunctions, - name: N, - params?: Parameters - ): Promise> { - const callable = functions.httpsCallable(name); - return callable(params).toPromise(); +export async function callFunction< + C extends Record any>, + N extends Extract +>( + functions: AngularFireFunctions, + name: N, + params?: Parameters +): Promise> { + const callable = functions.httpsCallable(name); + return callable(params).toPromise(); } diff --git a/projects/akita-ng-fire/src/lib/utils/id-or-path.ts b/projects/akita-ng-fire/src/lib/utils/id-or-path.ts index c87f84ad..5d1b74c7 100644 --- a/projects/akita-ng-fire/src/lib/utils/id-or-path.ts +++ b/projects/akita-ng-fire/src/lib/utils/id-or-path.ts @@ -1,7 +1,10 @@ import { DocOptions } from '../collection/collection.service'; // Helper to retrieve the id and path of a document in the collection -export function getIdAndPath(options: DocOptions, collectionPath?: string): { id: string, path: string } { +export function getIdAndPath( + options: DocOptions, + collectionPath?: string +): { id: string; path: string } { let path = ''; let id = ''; if (options['id']) { @@ -14,7 +17,9 @@ export function getIdAndPath(options: DocOptions, collectionPath?: string): { id path = options['path']; const part = path.split('/'); if (part.length % 2 !== 0) { - throw new Error(`Path ${path} doesn't look like a Firestore's document path`); + throw new Error( + `Path ${path} doesn't look like a Firestore's document path` + ); } id = part[part.length - 1]; } else { diff --git a/projects/akita-ng-fire/src/lib/utils/path-with-params.ts b/projects/akita-ng-fire/src/lib/utils/path-with-params.ts index 17b1c702..5bd08502 100644 --- a/projects/akita-ng-fire/src/lib/utils/path-with-params.ts +++ b/projects/akita-ng-fire/src/lib/utils/path-with-params.ts @@ -2,9 +2,10 @@ import { HashMap } from '@datorama/akita'; /** Get the params from a path */ export function getPathParams(path: string) { - return path.split('/') - .filter(segment => segment.charAt(0) === ':') - .map(segment => segment.substr(1)); + return path + .split('/') + .filter((segment) => segment.charAt(0) === ':') + .map((segment) => segment.substr(1)); } /** @@ -14,15 +15,22 @@ export function getPathParams(path: string) { * @example pathWithParams('movies/:movieId/stakeholder/:shId', { movieId, shId }) */ export function pathWithParams(path: string, params: HashMap): string { - return path.split('/').map(segment => { - if (segment.charAt(0) === ':') { - const key = segment.substr(1); - if (!params[key]) { - throw new Error(`Required parameter ${key} from ${path} doesn't exist in params ${JSON.stringify(params)}`); + return path + .split('/') + .map((segment) => { + if (segment.charAt(0) === ':') { + const key = segment.substr(1); + if (!params[key]) { + throw new Error( + `Required parameter ${key} from ${path} doesn't exist in params ${JSON.stringify( + params + )}` + ); + } + return params[key]; + } else { + return segment; } - return params[key]; - } else { - return segment; - } - }).join('/'); + }) + .join('/'); } diff --git a/projects/akita-ng-fire/src/lib/utils/query/await-sync-query.ts b/projects/akita-ng-fire/src/lib/utils/query/await-sync-query.ts index 93077ad2..49dafdec 100644 --- a/projects/akita-ng-fire/src/lib/utils/query/await-sync-query.ts +++ b/projects/akita-ng-fire/src/lib/utils/query/await-sync-query.ts @@ -1,15 +1,18 @@ import { isDocPath, getSubQueryKeys } from './utils'; -import { CollectionService, CollectionState } from '../../collection/collection.service'; +import { + CollectionService, + CollectionState, +} from '../../collection/collection.service'; import { getIdAndPath } from '../id-or-path'; import { Observable, combineLatest, throwError, of } from 'rxjs'; import { Query } from './types'; import { isQuery, hasSubQueries } from './utils'; import { switchMap, map, tap } from 'rxjs/operators'; -export function awaitSyncQuery>, E>( - this: Service, - query: Query -): Observable { +export function awaitSyncQuery< + Service extends CollectionService>, + E +>(this: Service, query: Query): Observable { return queryChanges.call(this, query).pipe( tap((entities: E | E[]) => { Array.isArray(entities) @@ -23,24 +26,23 @@ export function awaitSyncQuery>, E>( - this: Service, - query: Query -): Observable { +export function queryChanges< + Service extends CollectionService>, + E +>(this: Service, query: Query): Observable { return awaitQuery.call(this, query).pipe( map((entities: E | E[]) => { return Array.isArray(entities) - ? entities.map(e => this.formatFromFirestore(e)) + ? entities.map((e) => this.formatFromFirestore(e)) : this.formatFromFirestore(entities); }) ); } -export function awaitQuery>, E>( - this: Service, - query: Query -): Observable { - +export function awaitQuery< + Service extends CollectionService>, + E +>(this: Service, query: Query): Observable { // If single query if (typeof query === 'string') { return isDocPath(query) @@ -50,7 +52,9 @@ export function awaitQuery> if (Array.isArray(query)) { return !!query.length - ? combineLatest(query.map(oneQuery => awaitSyncQuery.call(this, oneQuery))) + ? combineLatest( + query.map((oneQuery) => awaitSyncQuery.call(this, oneQuery)) + ) : of(query); } @@ -58,13 +62,15 @@ export function awaitQuery> return of(query); } - /** * Get the entity of one subquery * @param subQuery The subquery function or value * @param entity The parent entity */ - const syncSubQuery = (subQueryFn: ((e: T) => Query) | any, entity: T): Observable => { + const syncSubQuery = ( + subQueryFn: ((e: T) => Query) | any, + entity: T + ): Observable => { if (!subQueryFn) { return throwError(`Query failed`); } @@ -79,7 +85,10 @@ export function awaitQuery> * @param parentQuery The parent Query * @param entity The parent Entity */ - const getAllSubQueries = (parentQuery: Query, entity: T): Observable => { + const getAllSubQueries = ( + parentQuery: Query, + entity: T + ): Observable => { if (!entity) { // Nothing found at path return of(undefined); @@ -91,31 +100,38 @@ export function awaitQuery> // Get all subquery keys const subQueryKeys = getSubQueryKeys(query); // For each key get the subquery - const subQueries$ = subQueryKeys.map(key => { - return syncSubQuery(parentQuery[key], entity).pipe(tap(subentity => entity[key] = subentity)); + const subQueries$ = subQueryKeys.map((key) => { + return syncSubQuery(parentQuery[key], entity).pipe( + tap((subentity) => (entity[key] = subentity)) + ); }); return !!subQueries$.length ? combineLatest(subQueries$).pipe(map(() => entity)) : of(entity); }; - // IF DOCUMENT const { path, queryFn } = query; if (isDocPath(path)) { const { id } = getIdAndPath({ path }); - return this['db'].doc(path).valueChanges().pipe( - switchMap(entity => getAllSubQueries(query, entity)), - map(entity => entity ? ({ id, ...entity }) : undefined) - ); + return this['db'] + .doc(path) + .valueChanges() + .pipe( + switchMap((entity) => getAllSubQueries(query, entity)), + map((entity) => (entity ? { id, ...entity } : undefined)) + ); } // IF COLLECTION - return this['db'].collection(path, queryFn) + return this['db'] + .collection(path, queryFn) .valueChanges({ idField: this.idKey }) .pipe( - switchMap(entities => { - const entities$ = entities.map(entity => getAllSubQueries(query, entity)); + switchMap((entities) => { + const entities$ = entities.map((entity) => + getAllSubQueries(query, entity) + ); return entities$.length ? combineLatest(entities$) : of([]); - }), + }) ); } diff --git a/projects/akita-ng-fire/src/lib/utils/query/sync-query.ts b/projects/akita-ng-fire/src/lib/utils/query/sync-query.ts index cd3808cf..543e08c8 100644 --- a/projects/akita-ng-fire/src/lib/utils/query/sync-query.ts +++ b/projects/akita-ng-fire/src/lib/utils/query/sync-query.ts @@ -1,15 +1,21 @@ import { DocumentChangeAction } from '@angular/fire/firestore'; -import { arrayUpdate, arrayAdd, arrayRemove, withTransaction, arrayUpsert } from '@datorama/akita'; -import { CollectionService, CollectionState } from '../../collection/collection.service'; +import { + arrayUpdate, + arrayAdd, + arrayRemove, + withTransaction, + arrayUpsert, +} from '@datorama/akita'; +import { + CollectionService, + CollectionState, +} from '../../collection/collection.service'; import { getIdAndPath } from '../id-or-path'; import { Query, SubQueries, CollectionChild, SubscriptionMap } from './types'; import { isDocPath, isQuery, getSubQuery } from './utils'; import { Observable, combineLatest, Subscription, of } from 'rxjs'; import { tap, finalize } from 'rxjs/operators'; - - - /** * Sync the collection * @param this Uses this function with bind on a Collection Service @@ -19,7 +25,6 @@ export function syncQuery( this: CollectionService>, query: Query ): Observable { - // If single query if (typeof query === 'string') { return isDocPath(query) @@ -28,20 +33,26 @@ export function syncQuery( } if (Array.isArray(query)) { - return combineLatest(query.map(oneQuery => syncQuery.call(this, oneQuery))); + return combineLatest( + query.map((oneQuery) => syncQuery.call(this, oneQuery)) + ); } if (!isQuery(query)) { - throw new Error('Query should be either a path, a Query object or an array of Queries'); + throw new Error( + 'Query should be either a path, a Query object or an array of Queries' + ); } - //////////////// // SUBQUERIES // //////////////// /** Listen on Child actions */ - const fromChildAction = (actions: DocumentChangeAction[], child: CollectionChild) => { + const fromChildAction = ( + actions: DocumentChangeAction[], + child: CollectionChild + ) => { const idKey = 'id'; // TODO: Improve how to const { parentId, key } = child; for (const action of actions) { @@ -50,21 +61,33 @@ export function syncQuery( switch (action.type) { case 'added': { - this['store'].update(parentId as any, (entity) => ({ - [key]: arrayUpsert(entity[key] as any || [], id, data, idKey) - }) as any); + this['store'].update( + parentId as any, + (entity) => + ({ + [key]: arrayUpsert((entity[key] as any) || [], id, data, idKey), + } as any) + ); break; } case 'removed': { - this['store'].update(parentId as any, (entity) => ({ - [key]: arrayRemove(entity[key] as any, id, idKey) - }) as any); + this['store'].update( + parentId as any, + (entity) => + ({ + [key]: arrayRemove(entity[key] as any, id, idKey), + } as any) + ); break; } case 'modified': { - this['store'].update(parentId as any, (entity) => ({ - [key]: arrayUpdate(entity[key] as any, id, data, idKey) - }) as any); + this['store'].update( + parentId as any, + (entity) => + ({ + [key]: arrayUpdate(entity[key] as any, id, data, idKey), + } as any) + ); } } } @@ -83,21 +106,31 @@ export function syncQuery( // If it's a static value // TODO : Check if subQuery is a string ??? if (!isQuery(subQuery)) { - const update = this['store'].update(parentId as any, {[key]: subQuery} as any); + const update = this['store'].update( + parentId as any, + { [key]: subQuery } as any + ); return of(update); } if (Array.isArray(subQuery)) { - const syncQueries = subQuery.map(oneQuery => { + const syncQueries = subQuery.map((oneQuery) => { if (isQuery(subQuery)) { const id = getIdAndPath({ path: subQuery.path }); - return this['db'].doc(subQuery.path).valueChanges().pipe( - tap((childDoc: E[K]) => { - this['store'].update(parentId as any, (entity) => ({ - [key]: arrayAdd(entity[key] as any, id, childDoc) - }) as any); - }) - ); + return this['db'] + .doc(subQuery.path) + .valueChanges() + .pipe( + tap((childDoc: E[K]) => { + this['store'].update( + parentId as any, + (entity) => + ({ + [key]: arrayAdd(entity[key] as any, id, childDoc), + } as any) + ); + }) + ); } return syncSubQuery(oneQuery, child); }); @@ -105,24 +138,33 @@ export function syncQuery( } if (typeof subQuery !== 'object') { - throw new Error('Query should be either a path, a Query object or an array of Queries'); + throw new Error( + 'Query should be either a path, a Query object or an array of Queries' + ); } // Sync subquery if (isDocPath(subQuery.path)) { - return this['db'].doc(subQuery.path).valueChanges().pipe( - tap((children: E[K]) => this['store'].update(parentId as any, { [key]: children } as any)) - ); + return this['db'] + .doc(subQuery.path) + .valueChanges() + .pipe( + tap((children: E[K]) => + this['store'].update(parentId as any, { [key]: children } as any) + ) + ); } else { - return this['db'].collection(subQuery.path, subQuery.queryFn) + return this['db'] + .collection(subQuery.path, subQuery.queryFn) .stateChanges() .pipe( - withTransaction(actions => fromChildAction(actions as DocumentChangeAction[], child)) + withTransaction((actions) => + fromChildAction(actions as DocumentChangeAction[], child) + ) ) as Observable; } }; - /** * Get in sync with all the subqueries * @param subQueries A map of all the subqueries @@ -130,7 +172,7 @@ export function syncQuery( */ const syncAllSubQueries = (subQueries: SubQueries, parent: E) => { const obs = Object.keys(subQueries) - .filter(key => key !== 'path' && key !== 'queryFn') + .filter((key) => key !== 'path' && key !== 'queryFn') .map((key: Extract, string>) => { const queryLike = getSubQuery(subQueries[key], parent); const child = { key, parentId: parent[this.idKey] }; @@ -139,7 +181,6 @@ export function syncQuery( return combineLatest(obs); }; - //////////////// // MAIN QUERY // //////////////// @@ -148,17 +189,23 @@ export function syncQuery( const fromActionWithChild = ( actions: DocumentChangeAction[], mainQuery: Query, - subscriptions: SubscriptionMap) => { - + subscriptions: SubscriptionMap + ) => { for (const action of actions) { const id = action.payload.doc.id; const data = action.payload.doc.data(); switch (action.type) { case 'added': { - const entity = this.formatFromFirestore({ [this.idKey]: id, ...data }); + const entity = this.formatFromFirestore({ + [this.idKey]: id, + ...data, + }); this['store'].upsert(id, entity); - subscriptions[id] = syncAllSubQueries(mainQuery as SubQueries, entity).subscribe(); + subscriptions[id] = syncAllSubQueries( + mainQuery as SubQueries, + entity + ).subscribe(); break; } case 'removed': { @@ -176,29 +223,41 @@ export function syncQuery( const { path, queryFn } = query; if (isDocPath(path)) { - const { id } = getIdAndPath({path}); + const { id } = getIdAndPath({ path }); let subscription: Subscription; - return this['db'].doc(path).valueChanges().pipe( - tap(entity => { - this['store'].upsert(id, this.formatFromFirestore({id, ...entity})); - if (!subscription) { // Subscribe only the first time - subscription = syncAllSubQueries(query as SubQueries, entity).subscribe(); - } - }), - // Stop subscription - finalize(() => subscription.unsubscribe()) - ); + return this['db'] + .doc(path) + .valueChanges() + .pipe( + tap((entity) => { + this['store'].upsert(id, this.formatFromFirestore({ id, ...entity })); + if (!subscription) { + // Subscribe only the first time + subscription = syncAllSubQueries( + query as SubQueries, + entity + ).subscribe(); + } + }), + // Stop subscription + finalize(() => subscription.unsubscribe()) + ); } else { const subscriptions: SubscriptionMap = {}; - return this['db'].collection(path, queryFn) + return this['db'] + .collection(path, queryFn) .stateChanges() .pipe( - withTransaction(actions => fromActionWithChild(actions, query, subscriptions)), + withTransaction((actions) => + fromActionWithChild(actions, query, subscriptions) + ), // Stop all subscriptions - finalize(() => Object.keys(subscriptions).forEach(id => { - subscriptions[id].unsubscribe(); - delete subscriptions[id]; - })) + finalize(() => + Object.keys(subscriptions).forEach((id) => { + subscriptions[id].unsubscribe(); + delete subscriptions[id]; + }) + ) ) as Observable; } } diff --git a/projects/akita-ng-fire/src/lib/utils/query/types.ts b/projects/akita-ng-fire/src/lib/utils/query/types.ts index a69d13e9..3e8f9483 100644 --- a/projects/akita-ng-fire/src/lib/utils/query/types.ts +++ b/projects/akita-ng-fire/src/lib/utils/query/types.ts @@ -10,7 +10,9 @@ export type Query = { export type QueryLike = Query | Query[] | string; export type SubQueries = { - [K in keyof Partial]: (QueryLike | T[K]) | ((entity: T) => QueryLike) + [K in keyof Partial]: + | (QueryLike | T[K]) + | ((entity: T) => QueryLike); }; export interface CollectionChild { diff --git a/projects/akita-ng-fire/src/lib/utils/query/utils.ts b/projects/akita-ng-fire/src/lib/utils/query/utils.ts index f5d31cb4..12173940 100644 --- a/projects/akita-ng-fire/src/lib/utils/query/utils.ts +++ b/projects/akita-ng-fire/src/lib/utils/query/utils.ts @@ -5,27 +5,32 @@ export const queryKeys = ['path', 'queryFn']; export function getSubQueryKeys(query: Query) { const keys = Object.keys(query); - return keys.filter(key => !queryKeys.includes(key)); + return keys.filter((key) => !queryKeys.includes(key)); } export function hasSubQueries(query: Query) { return getSubQueryKeys(query).length > 0; } -export function getSubQuery(query: SubQueries[K], parent: E): SubQueries[K] { +export function getSubQuery( + query: SubQueries[K], + parent: E +): SubQueries[K] { if (typeof query !== 'function') { return query; } return query(parent); } - export function isDocPath(path: string) { return path.split('/').length % 2 === 0; } /** Transform a path into a collection Query */ -export function collection(path: string, queryFn?: QueryFn): Query { +export function collection( + path: string, + queryFn?: QueryFn +): Query { return { path, queryFn } as Query; } diff --git a/projects/akita-ng-fire/src/lib/utils/redirect-if-empty.ts b/projects/akita-ng-fire/src/lib/utils/redirect-if-empty.ts index ce9834fe..52edf895 100644 --- a/projects/akita-ng-fire/src/lib/utils/redirect-if-empty.ts +++ b/projects/akita-ng-fire/src/lib/utils/redirect-if-empty.ts @@ -6,5 +6,7 @@ import { DocumentChangeAction } from '@angular/fire/firestore'; * @param redirectTo Route path to redirecto if collection is empty */ export function redirectIfEmpty(redirectTo: string) { - return map((actions: DocumentChangeAction[]) => actions.length === 0 ? redirectTo : true); + return map((actions: DocumentChangeAction[]) => + actions.length === 0 ? redirectTo : true + ); } diff --git a/projects/akita-ng-fire/src/lib/utils/roles.ts b/projects/akita-ng-fire/src/lib/utils/roles.ts index 9349bc8b..132047af 100644 --- a/projects/akita-ng-fire/src/lib/utils/roles.ts +++ b/projects/akita-ng-fire/src/lib/utils/roles.ts @@ -1,23 +1,37 @@ -export type Role = 'read' | 'get' | 'list' | 'write' | 'create' | 'update' | 'delete'; +export type Role = + | 'read' + | 'get' + | 'list' + | 'write' + | 'create' + | 'update' + | 'delete'; export type RolesState = Record; export function canWrite(write: 'create' | 'update' | 'delete', roles: Role[]) { - return roles.some(role => role === write || role === 'write'); + return roles.some((role) => role === write || role === 'write'); } export function canRead(read: 'get' | 'list', roles: Role[]) { - return roles.some(role => role === read || role === 'write'); + return roles.some((role) => role === read || role === 'write'); } - export function hasRole(role: Role, roles: Role[]) { switch (role) { - case 'write': return roles.includes('write'); - case 'create': return canWrite('create', roles); - case 'delete': return canWrite('delete', roles); - case 'update': return canWrite('update', roles); - case 'read': return roles.includes('read'); - case 'get': return canRead('get', roles); - case 'list': return canRead('list', roles); - default: return false; + case 'write': + return roles.includes('write'); + case 'create': + return canWrite('create', roles); + case 'delete': + return canWrite('delete', roles); + case 'update': + return canWrite('update', roles); + case 'read': + return roles.includes('read'); + case 'get': + return canRead('get', roles); + case 'list': + return canRead('list', roles); + default: + return false; } } diff --git a/projects/akita-ng-fire/src/lib/utils/share-delay.ts b/projects/akita-ng-fire/src/lib/utils/share-delay.ts index db9745d2..fbe1e096 100644 --- a/projects/akita-ng-fire/src/lib/utils/share-delay.ts +++ b/projects/akita-ng-fire/src/lib/utils/share-delay.ts @@ -3,7 +3,7 @@ import { Observable, ReplaySubject, Subscriber, - Subscription + Subscription, } from 'rxjs'; /** @@ -13,7 +13,9 @@ import { * @note code based on shareReplay of rxjs v6.6.7: https://github.com/ReactiveX/rxjs/blob/6.6.7/src/internal/operators/shareReplay.ts * @param delay Delay in ms to wait before unsubscribing */ -export function shareWithDelay(delay: number = 100): MonoTypeOperatorFunction { +export function shareWithDelay( + delay: number = 100 +): MonoTypeOperatorFunction { let subject: ReplaySubject | undefined; let subscription: Subscription | undefined; let refCount = 0; @@ -41,7 +43,7 @@ export function shareWithDelay(delay: number = 100): MonoTypeOperatorFunction isComplete = true; subscription = undefined; subject?.complete(); - } + }, }); // Here we need to check to see if the source synchronously completed. Although @@ -59,7 +61,7 @@ export function shareWithDelay(delay: number = 100): MonoTypeOperatorFunction refCount--; innerSub?.unsubscribe(); innerSub = undefined; - + // await some ms before unsubscribing setTimeout(() => { if (subscription && !isComplete && refCount === 0) { @@ -72,4 +74,4 @@ export function shareWithDelay(delay: number = 100): MonoTypeOperatorFunction } return (source: Observable) => source.lift(operation); -} \ No newline at end of file +} diff --git a/projects/akita-ng-fire/src/lib/utils/store-options.ts b/projects/akita-ng-fire/src/lib/utils/store-options.ts index b66b5d5e..60d13063 100644 --- a/projects/akita-ng-fire/src/lib/utils/store-options.ts +++ b/projects/akita-ng-fire/src/lib/utils/store-options.ts @@ -4,9 +4,14 @@ import { SyncOptions } from './types'; /** * Get the store name of a store to be synced */ -export function getStoreName(store: EntityStore, storeOptions: Partial = {}) { +export function getStoreName( + store: EntityStore, + storeOptions: Partial = {} +) { if (!store && !storeOptions.storeName) { - throw new Error('You should either provide a store name or inject a store instance in constructor'); + throw new Error( + 'You should either provide a store name or inject a store instance in constructor' + ); } return storeOptions.storeName || store.storeName; } diff --git a/projects/akita-ng-fire/src/lib/utils/sync-from-action.ts b/projects/akita-ng-fire/src/lib/utils/sync-from-action.ts index 56b8b8bb..ab75dd01 100644 --- a/projects/akita-ng-fire/src/lib/utils/sync-from-action.ts +++ b/projects/akita-ng-fire/src/lib/utils/sync-from-action.ts @@ -1,4 +1,8 @@ -import { DocumentChangeAction, DocumentSnapshot, Action } from '@angular/fire/firestore'; +import { + DocumentChangeAction, + DocumentSnapshot, + Action, +} from '@angular/fire/firestore'; import { getEntityType, StoreAction, @@ -6,44 +10,66 @@ import { EntityStoreAction, runStoreAction, applyTransaction, - getStoreByName + getStoreByName, } from '@datorama/akita'; import firebase from 'firebase'; /** Set the loading parameter of a specific store */ export function setLoading(storeName: string, loading: boolean) { - runStoreAction(storeName, StoreAction.Update, update => update({ loading })) -}; + runStoreAction(storeName, StoreAction.Update, (update) => + update({ loading }) + ); +} /** Reset the store to an empty array */ export function resetStore(storeName: string) { getStoreByName(storeName).reset(); -}; +} /** Set a entity as active */ export function setActive(storeName: string, active: string | string[]) { - runStoreAction(storeName, StoreAction.Update, update => update({ active })) -}; + runStoreAction(storeName, StoreAction.Update, (update) => update({ active })); +} /** Create or update one or several entities in the store */ -export function upsertStoreEntity(storeName: string, data: any, id: string | string[]) { - runEntityStoreAction(storeName, EntityStoreAction.UpsertEntities, upsert => upsert(id, data)); +export function upsertStoreEntity( + storeName: string, + data: any, + id: string | string[] +) { + runEntityStoreAction(storeName, EntityStoreAction.UpsertEntities, (upsert) => + upsert(id, data) + ); } /** Remove one or several entities in the store */ -export function removeStoreEntity(storeName: string, entityIds: string | string[]) { - runEntityStoreAction(storeName, EntityStoreAction.RemoveEntities, remove => remove(entityIds)); +export function removeStoreEntity( + storeName: string, + entityIds: string | string[] +) { + runEntityStoreAction(storeName, EntityStoreAction.RemoveEntities, (remove) => + remove(entityIds) + ); } /** Update one or several entities in the store */ -export function updateStoreEntity(removeAndAdd: boolean, storeName: string, entityIds: string | string[], data: any) { +export function updateStoreEntity( + removeAndAdd: boolean, + storeName: string, + entityIds: string | string[], + data: any +) { if (removeAndAdd) { applyTransaction(() => { removeStoreEntity(storeName, entityIds); - upsertStoreEntity(storeName, data, entityIds) - }) + upsertStoreEntity(storeName, data, entityIds); + }); } else { - runEntityStoreAction(storeName, EntityStoreAction.UpdateEntities, update => update(entityIds, data)) + runEntityStoreAction( + storeName, + EntityStoreAction.UpdateEntities, + (update) => update(entityIds, data) + ); } } @@ -72,7 +98,11 @@ export async function syncStoreFromDocAction( switch (action.type) { case 'added': { - upsertStoreEntity(storeName, { [idKey]: id, ...(formattedEntity as object) }, id); + upsertStoreEntity( + storeName, + { [idKey]: id, ...(formattedEntity as object) }, + id + ); break; } case 'removed': { @@ -107,7 +137,11 @@ export async function syncStoreFromDocActionSnapshot( if (!action.payload.exists) { removeStoreEntity(storeName, id); } else { - upsertStoreEntity(storeName, { [idKey]: id, ...(formattedEntity as object) }, id); + upsertStoreEntity( + storeName, + { [idKey]: id, ...(formattedEntity as object) }, + id + ); } } diff --git a/projects/akita-ng-fire/src/lib/utils/sync-with-router.ts b/projects/akita-ng-fire/src/lib/utils/sync-with-router.ts index 5e1e6cf6..33d89594 100644 --- a/projects/akita-ng-fire/src/lib/utils/sync-with-router.ts +++ b/projects/akita-ng-fire/src/lib/utils/sync-with-router.ts @@ -1,23 +1,33 @@ import { getPathParams } from './path-with-params'; -import { CollectionService, CollectionState } from '../collection/collection.service'; +import { + CollectionService, + CollectionState, +} from '../collection/collection.service'; import { distinctUntilChanged, filter, switchMap, share } from 'rxjs/operators'; import { RouterQuery } from '@datorama/akita-ng-router-store'; import { Observable } from 'rxjs'; import { DocumentChangeAction } from '@angular/fire/firestore'; -export function syncWithRouter>, E>( +export function syncWithRouter< + Service extends CollectionService>, + E +>( this: Service, routerQuery: RouterQuery ): Observable[]> { if (!this['store'].resettable) { - throw new Error(`Store ${this['store'].storeName} is required to be resettable for syncWithRouter to work.`); + throw new Error( + `Store ${this['store'].storeName} is required to be resettable for syncWithRouter to work.` + ); } const pathParams = getPathParams(this.path); return routerQuery.selectParams().pipe( // Don't trigger change if params needed (and only them) haven't changed distinctUntilChanged((old, next) => { - const paramsHaveChanged = !!pathParams.find(param => old[param] !== next[param]); + const paramsHaveChanged = !!pathParams.find( + (param) => old[param] !== next[param] + ); // reset store on every parameter change if (paramsHaveChanged) { this['store'].reset(); @@ -25,8 +35,8 @@ export function syncWithRouter pathParams.every(param => !!params[param])), - switchMap(params => this.syncCollection({ params: { ...params } })), + filter((params) => pathParams.every((param) => !!params[param])), + switchMap((params) => this.syncCollection({ params: { ...params } })), share() ); } diff --git a/projects/akita-ng-fire/src/lib/utils/types.ts b/projects/akita-ng-fire/src/lib/utils/types.ts index a35de792..dfd4fe1d 100644 --- a/projects/akita-ng-fire/src/lib/utils/types.ts +++ b/projects/akita-ng-fire/src/lib/utils/types.ts @@ -1,4 +1,7 @@ -import { AngularFirestore, DocumentChangeAction } from '@angular/fire/firestore'; +import { + AngularFirestore, + DocumentChangeAction, +} from '@angular/fire/firestore'; import { EntityStore, EntityState, getEntityType } from '@datorama/akita'; import { Observable } from 'rxjs'; import firebase from 'firebase/app'; @@ -9,11 +12,15 @@ export interface FirestoreService = any> { db: AngularFirestore; store: EntityStore; idKey: string; - syncCollection(query?: any): Observable>[]>; + syncCollection( + query?: any + ): Observable>[]>; getValue(query?: any): Promise | getEntityType[]>; } -export type AtomicWrite = firebase.firestore.Transaction | firebase.firestore.WriteBatch; +export type AtomicWrite = + | firebase.firestore.Transaction + | firebase.firestore.WriteBatch; /** * @param params params for the collection path @@ -39,4 +46,7 @@ export interface SyncOptions extends PathParams { } /** Function used to update an entity within a transaction */ -export type UpdateCallback = (state: Readonly, tx?: firebase.firestore.Transaction) => OrPromise>; +export type UpdateCallback = ( + state: Readonly, + tx?: firebase.firestore.Transaction +) => OrPromise>; diff --git a/projects/akita-ng-fire/src/public-api.ts b/projects/akita-ng-fire/src/public-api.ts index a6ba552a..51354b37 100644 --- a/projects/akita-ng-fire/src/public-api.ts +++ b/projects/akita-ng-fire/src/public-api.ts @@ -24,4 +24,4 @@ export * from './lib/utils/query/sync-query'; export * from './lib/utils/query/await-sync-query'; export * from './lib/utils/query/types'; export * from './lib/utils/query/utils'; -export * from './lib/utils/httpsCallable'; \ No newline at end of file +export * from './lib/utils/httpsCallable'; diff --git a/projects/akita-ng-fire/src/test.ts b/projects/akita-ng-fire/src/test.ts index ee9915d4..4c6f5ca7 100644 --- a/projects/akita-ng-fire/src/test.ts +++ b/projects/akita-ng-fire/src/test.ts @@ -5,7 +5,7 @@ import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, - platformBrowserDynamicTesting + platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; declare const require: any; diff --git a/projects/akita-ng-fire/tsconfig.lib.json b/projects/akita-ng-fire/tsconfig.lib.json index a24eb932..c75d1515 100644 --- a/projects/akita-ng-fire/tsconfig.lib.json +++ b/projects/akita-ng-fire/tsconfig.lib.json @@ -7,10 +7,7 @@ "declaration": true, "inlineSources": true, "types": [], - "lib": [ - "dom", - "es2018" - ] + "lib": ["dom", "es2018"] }, "angularCompilerOptions": { "skipTemplateCodegen": true, @@ -19,8 +16,5 @@ "strictInjectionParameters": true, "enableResourceInlining": true }, - "exclude": [ - "src/test.ts", - "**/*.spec.ts" - ] + "exclude": ["src/test.ts", "**/*.spec.ts"] } diff --git a/projects/akita-ng-fire/tsconfig.lib.prod.json b/projects/akita-ng-fire/tsconfig.lib.prod.json index 2617a83e..04c0e662 100644 --- a/projects/akita-ng-fire/tsconfig.lib.prod.json +++ b/projects/akita-ng-fire/tsconfig.lib.prod.json @@ -6,4 +6,4 @@ "angularCompilerOptions": { "enableIvy": false } -} \ No newline at end of file +} diff --git a/projects/akita-ng-fire/tslint.json b/projects/akita-ng-fire/tslint.json index 124133f8..205aedaa 100644 --- a/projects/akita-ng-fire/tslint.json +++ b/projects/akita-ng-fire/tslint.json @@ -1,17 +1,7 @@ { "extends": "../../tslint.json", "rules": { - "directive-selector": [ - true, - "attribute", - "lib", - "camelCase" - ], - "component-selector": [ - true, - "element", - "lib", - "kebab-case" - ] + "directive-selector": [true, "attribute", "lib", "camelCase"], + "component-selector": [true, "element", "lib", "kebab-case"] } } diff --git a/schematics/README.md b/schematics/README.md index 7854bc97..1784a817 100644 --- a/schematics/README.md +++ b/schematics/README.md @@ -7,6 +7,7 @@ This repository is a basic Schematic implementation that serves as a starting po To test locally, install `@angular-devkit/schematics-cli` globally and use the `schematics` command line tool. That tool acts the same as the `generate` command of the Angular CLI, but also has a debug mode. Check the documentation with + ```bash schematics --help ``` @@ -25,4 +26,3 @@ npm publish ``` That's it! - \ No newline at end of file diff --git a/schematics/src/ng-g/collection-service/index.ts b/schematics/src/ng-g/collection-service/index.ts index 96a5464c..c3f94a05 100644 --- a/schematics/src/ng-g/collection-service/index.ts +++ b/schematics/src/ng-g/collection-service/index.ts @@ -9,7 +9,7 @@ import { SchematicsException, move, noop, - filter + filter, } from '@angular-devkit/schematics'; import { strings } from '@angular-devkit/core'; import { buildDefaultPath } from '@schematics/angular/utility/project'; @@ -22,13 +22,17 @@ import { Schema } from './schema'; function getProjectPath(tree: Tree, options: Schema): Location { const workspaceBuffer = tree.read('angular.json'); if (!workspaceBuffer) { - throw new SchematicsException('No angular CLI workspace (angular.json) found.'); + throw new SchematicsException( + 'No angular CLI workspace (angular.json) found.' + ); } const workspace: WorkspaceSchema = JSON.parse(workspaceBuffer.toString()); const projectName = options.project || workspace.defaultProject; if (!projectName) { - throw new SchematicsException('Project name not found. Please provide the name of the projet.'); + throw new SchematicsException( + 'Project name not found. Please provide the name of the projet.' + ); } const project = workspace.projects[projectName]; const path = buildDefaultPath(project); @@ -36,16 +40,16 @@ function getProjectPath(tree: Tree, options: Schema): Location { } /** Generate the CollectionService */ -export default function(options: Schema): Rule { +export default function (options: Schema): Rule { return (tree: Tree, _context: SchematicContext) => { const { name, path } = getProjectPath(tree, options); const templateSource = apply(url('./files'), [ options.spec ? noop() - : filter(filePath => !filePath.endsWith('.spec.ts')), + : filter((filePath) => !filePath.endsWith('.spec.ts')), template({ ...strings, ...options, name }), - move(path) + move(path), ]); return mergeWith(templateSource); }; diff --git a/schematics/src/ng-g/collection-service/index_spec.ts b/schematics/src/ng-g/collection-service/index_spec.ts index 2c93b206..583c66be 100644 --- a/schematics/src/ng-g/collection-service/index_spec.ts +++ b/schematics/src/ng-g/collection-service/index_spec.ts @@ -1,13 +1,19 @@ -import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; -import { Schema as ApplicationOptions, Style } from '@schematics/angular/application/schema'; +import { + Schema as ApplicationOptions, + Style, +} from '@schematics/angular/application/schema'; import * as path from 'path'; import { Schema } from './schema'; const workspaceOptions: WorkspaceOptions = { name: 'workspace', newProjectRoot: 'projects', - version: '8.0.0' + version: '8.0.0', }; const appOptions: ApplicationOptions = { @@ -17,7 +23,7 @@ const appOptions: ApplicationOptions = { routing: false, style: Style.Css, skipTests: false, - skipPackageJson: false + skipPackageJson: false, }; const collectionPath = path.join(__dirname, '../../collection.json'); @@ -29,26 +35,39 @@ let appTree: UnitTestTree; function createNgApp(tree: UnitTestTree, name: string) { const options = { ...appOptions, name }; return runner - .runExternalSchematicAsync('@schematics/angular', 'application', options, tree) + .runExternalSchematicAsync( + '@schematics/angular', + 'application', + options, + tree + ) .toPromise(); } function createCollectionService(tree: UnitTestTree, options: Schema) { - return runner.runSchematicAsync('collection-service', options, tree).toPromise(); + return runner + .runSchematicAsync('collection-service', options, tree) + .toPromise(); } - describe('CollectionService', () => { - beforeEach(async () => { - appTree = await runner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise(); + appTree = await runner + .runExternalSchematicAsync( + '@schematics/angular', + 'workspace', + workspaceOptions + ) + .toPromise(); appTree = await createNgApp(appTree, 'foo'); }); it('Should create a CollectionService', async () => { const options: Schema = { name: 'movie' }; const tree = await createCollectionService(appTree, options); - expect(tree.files.includes('/projects/foo/src/app/movie.service.ts')).toBeTruthy(); + expect( + tree.files.includes('/projects/foo/src/app/movie.service.ts') + ).toBeTruthy(); }); it('Should create a CollectionService in another project', async () => { @@ -57,18 +76,24 @@ describe('CollectionService', () => { // Add ServiceCollection const options: Schema = { name: 'movie', project: 'bar' }; const tree = await createCollectionService(appTree, options); - expect(tree.files.includes('/projects/bar/src/app/movie.service.ts')).toBeTruthy(); + expect( + tree.files.includes('/projects/bar/src/app/movie.service.ts') + ).toBeTruthy(); }); it('Should have NO test file by default', async () => { const options: Schema = { name: 'movie' }; const tree = await createCollectionService(appTree, options); - expect(tree.files.includes('/projects/foo/src/app/movie.service_spec.ts')).toBeFalsy(); + expect( + tree.files.includes('/projects/foo/src/app/movie.service_spec.ts') + ).toBeFalsy(); }); it('Should have test file with option', async () => { const options: Schema = { name: 'movie', spec: true }; const tree = await createCollectionService(appTree, options); - expect(tree.files.includes('/projects/foo/src/app/movie.service.spec.ts')).toBeTruthy(); + expect( + tree.files.includes('/projects/foo/src/app/movie.service.spec.ts') + ).toBeTruthy(); }); }); diff --git a/schematics/tsconfig.json b/schematics/tsconfig.json index ec6103da..fc362b4e 100644 --- a/schematics/tsconfig.json +++ b/schematics/tsconfig.json @@ -1,10 +1,7 @@ { "compilerOptions": { "baseUrl": "tsconfig", - "lib": [ - "es2018", - "dom" - ], + "lib": ["es2018", "dom"], "declaration": true, "module": "commonjs", "moduleResolution": "node", @@ -20,15 +17,8 @@ "sourceMap": true, "strictNullChecks": true, "target": "es6", - "types": [ - "jasmine", - "node" - ] + "types": ["jasmine", "node"] }, - "include": [ - "src/**/*" - ], - "exclude": [ - "src/**/*/files/**/*" - ] + "include": ["src/**/*"], + "exclude": ["src/**/*/files/**/*"] } diff --git a/tsconfig.app.json b/tsconfig.app.json index 44795bd5..81e534ed 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,11 +4,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts", - "src/polyfills.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts", "src/polyfills.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/tsconfig.base.json b/tsconfig.base.json index 709c2d46..066ac033 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,7 +14,10 @@ "typeRoots": ["node_modules/@types"], "lib": ["es2018", "dom"], "paths": { - "akita-ng-fire": ["projects/akita-ng-fire/src/public-api", "dist/akita-ng-fire"], + "akita-ng-fire": [ + "projects/akita-ng-fire/src/public-api", + "dist/akita-ng-fire" + ], "akita-ng-fire/*": ["dist/akita-ng-fire/*"] } }, diff --git a/tsconfig.json b/tsconfig.json index b27e19ae..55d80326 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,4 +20,4 @@ "path": "./projects/akita-ng-fire/tsconfig.spec.json" } ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index 0fef6070..f7426b61 100644 --- a/tslint.json +++ b/tslint.json @@ -2,10 +2,7 @@ "extends": "tslint:recommended", "rules": { "align": { - "options": [ - "parameters", - "statements" - ] + "options": ["parameters", "statements"] }, "array-type": false, "arrow-parens": false, @@ -20,32 +17,42 @@ "directive-selector": [ true, "attribute", - ["app", "movie", "stakeholder", "catalog", "company", "auth", "marketplace", "organization"], + [ + "app", + "movie", + "stakeholder", + "catalog", + "company", + "auth", + "marketplace", + "organization" + ], "camelCase" ], "component-selector": [ true, "element", - ["app", "movie", "stakeholder", "catalog", "company", "auth", "marketplace", "organization"], + [ + "app", + "movie", + "stakeholder", + "catalog", + "company", + "auth", + "marketplace", + "organization" + ], "kebab-case" ], "eofline": true, - "import-blacklist": [ - true, - "rxjs/Rx" - ], + "import-blacklist": [true, "rxjs/Rx"], "import-spacing": true, "indent": { - "options": [ - "spaces" - ] + "options": ["spaces"] }, "interface-name": false, "max-classes-per-file": false, - "max-line-length": [ - true, - 140 - ], + "max-line-length": [true, 140], "member-access": false, "member-ordering": [ true, @@ -59,38 +66,20 @@ } ], "no-consecutive-blank-lines": false, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], + "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-empty": false, - "no-inferrable-types": [ - true, - "ignore-params" - ], + "no-inferrable-types": [true, "ignore-params"], "no-non-null-assertion": true, "no-redundant-jsdoc": true, "no-switch-case-fall-through": true, "no-string-literal": false, "no-var-requires": false, - "object-literal-key-quotes": [ - true, - "as-needed" - ], + "object-literal-key-quotes": [true, "as-needed"], "object-literal-sort-keys": false, "ordered-imports": false, - "quotemark": [ - true, - "single" - ], + "quotemark": [true, "single"], "semicolon": { - "options": [ - "always" - ] + "options": ["always"] }, "space-before-function-paren": { "options": { @@ -132,8 +121,8 @@ }, "use-lifecycle-interface": true, "use-pipe-transform-interface": true, - "variable-name": false - , "whitespace": { + "variable-name": false, + "whitespace": { "options": [ "check-branch", "check-decl", @@ -143,8 +132,6 @@ "check-typecast" ] } -}, - "rulesDirectory": [ - "codelyzer" - ] + }, + "rulesDirectory": ["codelyzer"] }