From 166269520c8a36eeebdf59b22e091f9c7eddea32 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 6 Feb 2024 11:35:22 -0500 Subject: [PATCH] Allow common get and set operations from state providers (#7824) * Allow common get and set operations from state providers * Use finnish endings for observables --- libs/common/spec/fake-state-provider.ts | 23 +++++- libs/common/spec/fake-state.ts | 4 +- ...default-active-user-state.provider.spec.ts | 43 +++++++++++ .../default-active-user-state.provider.ts | 9 ++- .../default-state.provider.spec.ts | 71 ++++++++++++++++++- .../implementations/default-state.provider.ts | 23 +++++- .../src/platform/state/state.provider.ts | 17 +++++ .../src/platform/state/user-state.provider.ts | 6 ++ 8 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts index 558114890e..34cf34d974 100644 --- a/libs/common/spec/fake-state-provider.ts +++ b/libs/common/spec/fake-state-provider.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { Observable } from "rxjs"; +import { Observable, map } from "rxjs"; import { GlobalState, @@ -99,11 +99,14 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider { } export class FakeActiveUserStateProvider implements ActiveUserStateProvider { + activeUserId$: Observable; establishedMocks: Map> = new Map(); states: Map> = new Map(); - constructor(public accountService: FakeAccountService) {} + constructor(public accountService: FakeAccountService) { + this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a.id)); + } get(keyDefinition: KeyDefinition): ActiveUserState { let result = this.states.get(keyDefinition.fullName); @@ -137,6 +140,21 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider { } export class FakeStateProvider implements StateProvider { + getUserState$(keyDefinition: KeyDefinition, userId?: UserId): Observable { + if (userId) { + return this.getUser(userId, keyDefinition).state$; + } + return this.getActive(keyDefinition).state$; + } + + async setUserState(keyDefinition: KeyDefinition, value: T, userId?: UserId): Promise { + if (userId) { + await this.getUser(userId, keyDefinition).update(() => value); + } else { + await this.getActive(keyDefinition).update(() => value); + } + } + getActive(keyDefinition: KeyDefinition): ActiveUserState { return this.activeUser.get(keyDefinition); } @@ -163,6 +181,7 @@ export class FakeStateProvider implements StateProvider { singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider(); activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider(this.accountService); derived: FakeDerivedStateProvider = new FakeDerivedStateProvider(); + activeUserId$: Observable = this.activeUser.activeUserId$; } export class FakeDerivedStateProvider implements DerivedStateProvider { diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts index 6c29c20ad8..11b18e76a2 100644 --- a/libs/common/spec/fake-state.ts +++ b/libs/common/spec/fake-state.ts @@ -182,13 +182,13 @@ export class FakeActiveUserState implements ActiveUserState { } const newState = configureState(current, combinedDependencies); this.stateSubject.next([this.userId, newState]); - this.nextMock(this.userId, newState); + this.nextMock([this.userId, newState]); return newState; } updateMock = this.update as jest.MockedFunction; - nextMock = jest.fn(); + nextMock = jest.fn(); private _keyDefinition: KeyDefinition | null = null; get keyDefinition() { diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts new file mode 100644 index 0000000000..d0fe0aa3f4 --- /dev/null +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts @@ -0,0 +1,43 @@ +import { mock } from "jest-mock-extended"; + +import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; +import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { UserId } from "../../../types/guid"; +import { + AbstractMemoryStorageService, + AbstractStorageService, + ObservableStorageService, +} from "../../abstractions/storage.service"; + +import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider"; + +describe("DefaultActiveUserStateProvider", () => { + const memoryStorage = mock(); + const diskStorage = mock(); + const userId = "userId" as UserId; + const accountInfo = { + id: userId, + name: "name", + email: "email", + status: AuthenticationStatus.Locked, + }; + const accountService = mockAccountServiceWith(userId, accountInfo); + let sut: DefaultActiveUserStateProvider; + + beforeEach(() => { + sut = new DefaultActiveUserStateProvider(accountService, memoryStorage, diskStorage); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should track the active User id from account service", () => { + const emissions = trackEmissions(sut.activeUserId$); + + accountService.activeAccountSubject.next(undefined); + accountService.activeAccountSubject.next(accountInfo); + + expect(emissions).toEqual([userId, undefined, userId]); + }); +}); diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts index 7bb5b44113..7a6d633bd6 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.ts @@ -1,4 +1,7 @@ +import { Observable, map } from "rxjs"; + import { AccountService } from "../../../auth/abstractions/account.service"; +import { UserId } from "../../../types/guid"; import { AbstractMemoryStorageService, AbstractStorageService, @@ -14,11 +17,15 @@ import { DefaultActiveUserState } from "./default-active-user-state"; export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { private cache: Record> = {}; + activeUserId$: Observable; + constructor( protected readonly accountService: AccountService, protected readonly memoryStorage: AbstractMemoryStorageService & ObservableStorageService, protected readonly diskStorage: AbstractStorageService & ObservableStorageService, - ) {} + ) { + this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id)); + } get(keyDefinition: KeyDefinition): ActiveUserState { const cacheKey = this.buildCacheKey(keyDefinition); diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index 704dbbccc5..f81c4dfce2 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -21,9 +21,10 @@ describe("DefaultStateProvider", () => { let globalStateProvider: FakeGlobalStateProvider; let derivedStateProvider: FakeDerivedStateProvider; let accountService: FakeAccountService; + const userId = "fakeUserId" as UserId; beforeEach(() => { - accountService = mockAccountServiceWith("fakeUserId" as UserId); + accountService = mockAccountServiceWith(userId); activeUserStateProvider = new FakeActiveUserStateProvider(accountService); singleUserStateProvider = new FakeSingleUserStateProvider(); globalStateProvider = new FakeGlobalStateProvider(); @@ -36,6 +37,74 @@ describe("DefaultStateProvider", () => { ); }); + describe("activeUserId$", () => { + it("should track the active User id from active user state provider", () => { + expect(sut.activeUserId$).toBe(activeUserStateProvider.activeUserId$); + }); + }); + + describe("getUserState$", () => { + const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { + deserializer: (s) => s, + }); + + it("should get the state for the active user if no userId is provided", () => { + const state = sut.getUserState$(keyDefinition); + expect(state).toBe(activeUserStateProvider.get(keyDefinition).state$); + }); + + it("should not return state for a single user if no userId is provided", () => { + const state = sut.getUserState$(keyDefinition); + expect(state).not.toBe(singleUserStateProvider.get(userId, keyDefinition).state$); + }); + + it("should get the state for the provided userId", () => { + const userId = "user" as UserId; + const state = sut.getUserState$(keyDefinition, userId); + expect(state).toBe(singleUserStateProvider.get(userId, keyDefinition).state$); + }); + + it("should not get the active user state if userId is provided", () => { + const userId = "user" as UserId; + const state = sut.getUserState$(keyDefinition, userId); + expect(state).not.toBe(activeUserStateProvider.get(keyDefinition).state$); + }); + }); + + describe("setUserState", () => { + const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { + deserializer: (s) => s, + }); + + it("should set the state for the active user if no userId is provided", async () => { + const value = "value"; + await sut.setUserState(keyDefinition, value); + const state = activeUserStateProvider.getFake(keyDefinition); + expect(state.nextMock).toHaveBeenCalledWith([expect.any(String), value]); + }); + + it("should not set state for a single user if no userId is provided", async () => { + const value = "value"; + await sut.setUserState(keyDefinition, value); + const state = singleUserStateProvider.getFake(userId, keyDefinition); + expect(state.nextMock).not.toHaveBeenCalled(); + }); + + it("should set the state for the provided userId", async () => { + const value = "value"; + await sut.setUserState(keyDefinition, value, userId); + const state = singleUserStateProvider.getFake(userId, keyDefinition); + expect(state.nextMock).toHaveBeenCalledWith(value); + }); + + it("should not set the active user state if userId is provided", async () => { + const value = "value"; + await sut.setUserState(keyDefinition, value, userId); + const state = activeUserStateProvider.getFake(keyDefinition); + expect(state.nextMock).not.toHaveBeenCalled(); + }); + }); + it("should bind the activeUserStateProvider", () => { const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: () => null, diff --git a/libs/common/src/platform/state/implementations/default-state.provider.ts b/libs/common/src/platform/state/implementations/default-state.provider.ts index 77873a5547..8af3ba0539 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.ts @@ -1,20 +1,41 @@ import { Observable } from "rxjs"; +import { UserId } from "../../../types/guid"; import { DerivedStateDependencies } from "../../../types/state"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; import { DerivedStateProvider } from "../derived-state.provider"; import { GlobalStateProvider } from "../global-state.provider"; +import { KeyDefinition } from "../key-definition"; import { StateProvider } from "../state.provider"; import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider"; export class DefaultStateProvider implements StateProvider { + activeUserId$: Observable; constructor( private readonly activeUserStateProvider: ActiveUserStateProvider, private readonly singleUserStateProvider: SingleUserStateProvider, private readonly globalStateProvider: GlobalStateProvider, private readonly derivedStateProvider: DerivedStateProvider, - ) {} + ) { + this.activeUserId$ = this.activeUserStateProvider.activeUserId$; + } + + getUserState$(keyDefinition: KeyDefinition, userId?: UserId): Observable { + if (userId) { + return this.getUser(userId, keyDefinition).state$; + } else { + return this.getActive(keyDefinition).state$; + } + } + + async setUserState(keyDefinition: KeyDefinition, value: T, userId?: UserId): Promise { + if (userId) { + await this.getUser(userId, keyDefinition).update(() => value); + } else { + await this.getActive(keyDefinition).update(() => value); + } + } getActive: InstanceType["get"] = this.activeUserStateProvider.get.bind(this.activeUserStateProvider); diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index 6159552184..8e37120a20 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -17,6 +17,23 @@ import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.p * and {@link GlobalStateProvider}. */ export abstract class StateProvider { + /** @see{@link ActiveUserState.activeUserId$} */ + activeUserId$: Observable; + /** + * Gets a state observable for a given key and userId. + * + * @param keyDefinition - The key definition for the state you want to get. + * @param userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned. + */ + getUserState$: (keyDefinition: KeyDefinition, userId?: UserId) => Observable; + /** + * Sets the state for a given key and userId. + * + * @param keyDefinition - The key definition for the state you want to set. + * @param value - The value to set the state to. + * @param userId - The userId for which you want to set the state for. If not provided, the state for the currently active user will be set. + */ + setUserState: (keyDefinition: KeyDefinition, value: T, userId?: UserId) => Promise; /** @see{@link ActiveUserStateProvider.get} */ getActive: (keyDefinition: KeyDefinition) => ActiveUserState; /** @see{@link SingleUserStateProvider.get} */ diff --git a/libs/common/src/platform/state/user-state.provider.ts b/libs/common/src/platform/state/user-state.provider.ts index 78e44c2916..62e87d9b8f 100644 --- a/libs/common/src/platform/state/user-state.provider.ts +++ b/libs/common/src/platform/state/user-state.provider.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import { UserId } from "../../types/guid"; import { KeyDefinition } from "./key-definition"; @@ -18,6 +20,10 @@ export abstract class SingleUserStateProvider { * to the currently active user */ export abstract class ActiveUserStateProvider { + /** + * Convenience re-emission of active user ID from {@link AccountService.activeAccount$} + */ + activeUserId$: Observable; /** * Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such * that the emitted values always represents the state for the currently active user.