From 3be59322db10bea64d5cabf2d37c61d4c8483344 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 25 Apr 2022 15:44:03 +0200 Subject: [PATCH] WIP ngrx --- angular/package-lock.json | 43 ++++++++++++++++ angular/package.json | 2 + angular/src/components/groupings.component.ts | 14 ++++- angular/src/services/jslib-services.module.ts | 7 ++- common/src/models/view/folderDecrypted.ts | 5 +- common/src/services/sync.service.ts | 11 ++++ common/src/state/collection.reducer.ts | 51 +++++++++++++++++++ common/src/state/folder.ts | 7 +++ common/src/state/folders.actions.ts | 17 +++++++ common/src/state/folders.reducer.ts | 20 ++++++++ common/src/state/index.ts | 47 +++++++++++++++++ 11 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 common/src/state/collection.reducer.ts create mode 100644 common/src/state/folder.ts create mode 100644 common/src/state/folders.actions.ts create mode 100644 common/src/state/folders.reducer.ts create mode 100644 common/src/state/index.ts diff --git a/angular/package-lock.json b/angular/package-lock.json index 7b2465bf..627f1e44 100644 --- a/angular/package-lock.json +++ b/angular/package-lock.json @@ -19,6 +19,8 @@ "@angular/platform-browser-dynamic": "^12.2.13", "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", + "@ngrx/entity": "^12.5.1", + "@ngrx/store": "^12.5.1", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "rxjs": "^7.4.0", "tldjs": "^2.3.1", @@ -204,6 +206,31 @@ "resolved": "../common", "link": true }, + "node_modules/@ngrx/entity": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-12.5.1.tgz", + "integrity": "sha512-bjRMMe83onhrhxu5rJo+FhaS0gFY4HbMulSjUpuh0/LJckd0Z3QUDs+UcqYW/tjG/2o2rbNDxkws6w1D0c5ksA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^12.0.0", + "@ngrx/store": "12.5.1", + "rxjs": "^6.5.3 || ^7.0.0" + } + }, + "node_modules/@ngrx/store": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-12.5.1.tgz", + "integrity": "sha512-NLVkHLVeZc7IboXSDZlFoq1QrupmwYTYKRHS6se7ZasAv/lrIjHWsVVdICKSVRBsHZYu3+dmCXmu+YgulP7iHw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^12.0.0", + "rxjs": "^6.5.3 || ^7.0.0" + } + }, "node_modules/@types/duo_web_sdk": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@types/duo_web_sdk/-/duo_web_sdk-2.7.1.tgz", @@ -490,6 +517,22 @@ "zxcvbn": "^4.4.2" } }, + "@ngrx/entity": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-12.5.1.tgz", + "integrity": "sha512-bjRMMe83onhrhxu5rJo+FhaS0gFY4HbMulSjUpuh0/LJckd0Z3QUDs+UcqYW/tjG/2o2rbNDxkws6w1D0c5ksA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-12.5.1.tgz", + "integrity": "sha512-NLVkHLVeZc7IboXSDZlFoq1QrupmwYTYKRHS6se7ZasAv/lrIjHWsVVdICKSVRBsHZYu3+dmCXmu+YgulP7iHw==", + "requires": { + "tslib": "^2.0.0" + } + }, "@types/duo_web_sdk": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@types/duo_web_sdk/-/duo_web_sdk-2.7.1.tgz", diff --git a/angular/package.json b/angular/package.json index 47cfa4a4..981fe9df 100644 --- a/angular/package.json +++ b/angular/package.json @@ -33,6 +33,8 @@ "@angular/platform-browser-dynamic": "^12.2.13", "@angular/router": "^12.2.13", "@bitwarden/jslib-common": "file:../common", + "@ngrx/entity": "^12.5.1", + "@ngrx/store": "^12.5.1", "duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git", "rxjs": "^7.4.0", "tldjs": "^2.3.1", diff --git a/angular/src/components/groupings.component.ts b/angular/src/components/groupings.component.ts index d225b44c..a8e14f31 100644 --- a/angular/src/components/groupings.component.ts +++ b/angular/src/components/groupings.component.ts @@ -1,12 +1,16 @@ import { Directive, EventEmitter, Input, Output } from "@angular/core"; +import { Store } from "@ngrx/store"; +import { Observable } from "rxjs"; import { CollectionService } from "jslib-common/abstractions/collection.service"; import { FolderService } from "jslib-common/abstractions/folder.service"; import { StateService } from "jslib-common/abstractions/state.service"; import { CipherType } from "jslib-common/enums/cipherType"; +import { Folder } from "jslib-common/models/domain/folder"; import { TreeNode } from "jslib-common/models/domain/treeNode"; import { CollectionView } from "jslib-common/models/view/collectionView"; import { FolderDecrypted } from "jslib-common/models/view/folderDecrypted"; +import * as fromFolders from "jslib-common/state"; @Directive() export class GroupingsComponent { @@ -24,6 +28,8 @@ export class GroupingsComponent { @Output() onEditFolder = new EventEmitter(); @Output() onCollectionClicked = new EventEmitter(); + folders$: Observable; + folders: FolderDecrypted[]; nestedFolders: TreeNode[]; collections: CollectionView[]; @@ -43,8 +49,12 @@ export class GroupingsComponent { constructor( protected collectionService: CollectionService, protected folderService: FolderService, - protected stateService: StateService - ) {} + protected stateService: StateService, + store: Store + ) { + this.folders$ = store.select(fromFolders.selectFolderCollection); + console.log(this.folders$); + } async load(setLoaded = true) { const collapsedGroupings = await this.stateService.getCollapsedGroupings(); diff --git a/angular/src/services/jslib-services.module.ts b/angular/src/services/jslib-services.module.ts index 8d8aa85b..79ecf83a 100644 --- a/angular/src/services/jslib-services.module.ts +++ b/angular/src/services/jslib-services.module.ts @@ -1,4 +1,5 @@ import { Injector, LOCALE_ID, NgModule } from "@angular/core"; +import { Store } from "@ngrx/store"; import { ApiService as ApiServiceAbstraction } from "jslib-common/abstractions/api.service"; import { AppIdService as AppIdServiceAbstraction } from "jslib-common/abstractions/appId.service"; @@ -73,6 +74,7 @@ import { UserVerificationService } from "jslib-common/services/userVerification. import { UsernameGenerationService } from "jslib-common/services/usernameGeneration.service"; import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service"; import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service"; +import { State } from "jslib-common/state"; import { AuthGuardService } from "./auth-guard.service"; import { BroadcasterService } from "./broadcaster.service"; @@ -250,7 +252,8 @@ import { ValidationService } from "./validation.service"; keyConnectorService: KeyConnectorServiceAbstraction, stateService: StateServiceAbstraction, organizationService: OrganizationServiceAbstraction, - providerService: ProviderServiceAbstraction + providerService: ProviderServiceAbstraction, + store: Store ) => new SyncService( apiService, @@ -267,6 +270,7 @@ import { ValidationService } from "./validation.service"; stateService, organizationService, providerService, + store, async (expired: boolean) => messagingService.send("logout", { expired: expired }) ), deps: [ @@ -284,6 +288,7 @@ import { ValidationService } from "./validation.service"; StateServiceAbstraction, OrganizationServiceAbstraction, ProviderServiceAbstraction, + Store, ], }, { provide: BroadcasterServiceAbstraction, useClass: BroadcasterService }, diff --git a/common/src/models/view/folderDecrypted.ts b/common/src/models/view/folderDecrypted.ts index d236c32b..e18a98d1 100644 --- a/common/src/models/view/folderDecrypted.ts +++ b/common/src/models/view/folderDecrypted.ts @@ -1,6 +1,7 @@ import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { Folder } from "../domain/folder"; +import { SymmetricCryptoKey } from "../domain/symmetricCryptoKey"; import { ITreeNodeObject } from "../domain/treeNode"; export class FolderDecrypted implements ITreeNodeObject { @@ -8,10 +9,10 @@ export class FolderDecrypted implements ITreeNodeObject { name: string = null; revisionDate: Date = null; - async encrypt(cryptoService: CryptoService): Promise { + async encrypt(cryptoService: CryptoService, key?: SymmetricCryptoKey): Promise { return { id: this.id, - name: await cryptoService.encrypt(this.name), + name: await cryptoService.encrypt(this.name, key), revisionDate: null, }; } diff --git a/common/src/services/sync.service.ts b/common/src/services/sync.service.ts index 7f5155df..b229029b 100644 --- a/common/src/services/sync.service.ts +++ b/common/src/services/sync.service.ts @@ -1,3 +1,5 @@ +import { Store } from "@ngrx/store"; + import { ApiService } from "../abstractions/api.service"; import { CipherService } from "../abstractions/cipher.service"; import { CollectionService } from "../abstractions/collection.service"; @@ -33,6 +35,8 @@ import { import { PolicyResponse } from "../models/response/policyResponse"; import { ProfileResponse } from "../models/response/profileResponse"; import { SendResponse } from "../models/response/sendResponse"; +import * as FoldersActions from "../state/folders.actions"; + export class SyncService implements SyncServiceAbstraction { syncInProgress = false; @@ -52,6 +56,7 @@ export class SyncService implements SyncServiceAbstraction { private stateService: StateService, private organizationService: OrganizationService, private providerService: ProviderService, + private store: Store, private logoutCallback: (expired: boolean) => Promise ) {} @@ -340,6 +345,12 @@ export class SyncService implements SyncServiceAbstraction { response.forEach((f) => { folders[f.id] = new FolderData(f.toFolder()); }); + + console.log("hi"); + this.store.dispatch( + FoldersActions.loadBooksSuccess({ folders: response.map((f) => f.toFolder()) }) + ); + return await this.folderService.replace(folders); } diff --git a/common/src/state/collection.reducer.ts b/common/src/state/collection.reducer.ts new file mode 100644 index 00000000..d3ed2266 --- /dev/null +++ b/common/src/state/collection.reducer.ts @@ -0,0 +1,51 @@ +import { createReducer, on } from "@ngrx/store"; + +import * as FolderActions from "./folders.actions"; + +export const collectionFeatureKey = "collection"; + +export interface State { + loaded: boolean; + ids: string[]; +} + +const initialState: State = { + loaded: false, + ids: [], +}; + +export const reducer = createReducer( + initialState, + on(FolderActions.loadBooksSuccess, (_state, { folders }) => ({ + loaded: true, + ids: folders.map((f) => f.id), + })), + /** + * Optimistically add book to collection. + * If this succeeds there's nothing to do. + * If this fails we revert state by removing the book. + * + * `on` supports handling multiple types of actions + */ + on(FolderActions.addFolder, (state, { folder }) => { + if (state.ids.indexOf(folder.id) > -1) { + return state; + } + return { + ...state, + ids: [...state.ids, folder.id], + }; + }), + /** + * Optimistically remove book from collection. + * If addBook fails, we "undo" adding the book. + */ + on(FolderActions.removeFolder, (state, { folder }) => ({ + ...state, + ids: state.ids.filter((id) => id !== folder.id), + })) +); + +export const getLoaded = (state: State) => state.loaded; + +export const getIds = (state: State) => state.ids; diff --git a/common/src/state/folder.ts b/common/src/state/folder.ts new file mode 100644 index 00000000..8890a6f1 --- /dev/null +++ b/common/src/state/folder.ts @@ -0,0 +1,7 @@ +import { EncString } from "jslib-common/models/domain/encString"; + +export interface Folder { + id: string; + name: EncString; + revisionDate: Date; +} diff --git a/common/src/state/folders.actions.ts b/common/src/state/folders.actions.ts new file mode 100644 index 00000000..9550de3c --- /dev/null +++ b/common/src/state/folders.actions.ts @@ -0,0 +1,17 @@ +import { createAction, props } from "@ngrx/store"; + +import { Folder } from "./folder"; + +export const addFolder = createAction("[AddEdit Folder] Add Folder", props<{ folder: Folder }>()); + +export const editFolder = createAction("[AddEdit Folder] Edit Folder", props<{ folder: Folder }>()); + +export const removeFolder = createAction( + "[AddEdit Folder] Remove Folder", + props<{ folder: Folder }>() +); + +export const loadBooksSuccess = createAction( + "[Sync API] Loaded Folders Success", + props<{ folders: Folder[] }>() +); diff --git a/common/src/state/folders.reducer.ts b/common/src/state/folders.reducer.ts new file mode 100644 index 00000000..42fe0daa --- /dev/null +++ b/common/src/state/folders.reducer.ts @@ -0,0 +1,20 @@ +import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity"; +import { createReducer, on } from "@ngrx/store"; + +import { Folder } from "./folder"; +import * as FolderActions from "./folders.actions"; + +export const foldersFeatureKey = "folders"; + +export type State = EntityState + +export const adapter: EntityAdapter = createEntityAdapter({ + sortComparer: false, +}); + +export const initialState: State = adapter.getInitialState({}); + +export const reducer = createReducer( + initialState, + on(FolderActions.loadBooksSuccess, (state, { folders }) => adapter.addMany(folders, state)) +); diff --git a/common/src/state/index.ts b/common/src/state/index.ts new file mode 100644 index 00000000..e7917892 --- /dev/null +++ b/common/src/state/index.ts @@ -0,0 +1,47 @@ +import { createSelector, createFeatureSelector } from "@ngrx/store"; + +import * as fromCollection from "./collection.reducer"; +import { Folder } from "./folder"; +import * as fromBooks from "./folders.reducer"; + +export interface State { + [fromBooks.foldersFeatureKey]: fromBooks.State; + [fromCollection.collectionFeatureKey]: fromCollection.State; +} + +export const reducers = { + [fromBooks.foldersFeatureKey]: fromBooks.reducer, + [fromCollection.collectionFeatureKey]: fromCollection.reducer, +}; + +export const selectFoldersState = createFeatureSelector(fromBooks.foldersFeatureKey); + +export const selectFolderEntitiesState = createSelector( + selectFoldersState, + (state) => state.folders +); + +export const { + selectIds: selectFolderIds, + selectEntities: selectFolderEntities, + selectAll: selectAllFolders, + selectTotal: selectTotalFolders, +} = fromBooks.adapter.getSelectors(selectFolderEntitiesState); + +export const selectCollectionState = createSelector( + selectFoldersState, + (state) => state.collection +); + +export const selectCollectionFolderIds = createSelector( + selectCollectionState, + fromCollection.getIds +); + +export const selectFolderCollection = createSelector( + selectFolderEntities, + selectCollectionFolderIds, + (entities, ids) => { + return ids.map((id) => entities[id]).filter((folder): folder is Folder => folder != null); + } +);