mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Rebase: Start Implementing AccountService
This commit is contained in:
@@ -2,6 +2,7 @@ import * as path from "path";
|
|||||||
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
|
||||||
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||||
@@ -28,6 +29,7 @@ export class Main {
|
|||||||
storageService: ElectronStorageService;
|
storageService: ElectronStorageService;
|
||||||
memoryStorageService: MemoryStorageService;
|
memoryStorageService: MemoryStorageService;
|
||||||
messagingService: ElectronMainMessagingService;
|
messagingService: ElectronMainMessagingService;
|
||||||
|
accountService: AccountServiceImplementation;
|
||||||
stateService: ElectronStateService;
|
stateService: ElectronStateService;
|
||||||
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
desktopCredentialStorageListener: DesktopCredentialStorageListener;
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ export class Main {
|
|||||||
this.memoryStorageService,
|
this.memoryStorageService,
|
||||||
this.logService,
|
this.logService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
|
this.accountService, // TODO: This is circular
|
||||||
false // Do not use disk caching because this will get out of sync with the renderer service
|
false // Do not use disk caching because this will get out of sync with the renderer service
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,6 +111,9 @@ export class Main {
|
|||||||
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
|
this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => {
|
||||||
this.messagingMain.onMessage(message);
|
this.messagingMain.onMessage(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.accountService = new AccountServiceImplementation(this.messagingService, this.logService);
|
||||||
|
|
||||||
this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
|
this.powerMonitorMain = new PowerMonitorMain(this.messagingService);
|
||||||
this.menuMain = new MenuMain(
|
this.menuMain = new MenuMain(
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
|
|||||||
@@ -176,7 +176,6 @@ import { ModalService } from "./modal.service";
|
|||||||
import { ThemingService } from "./theming/theming.service";
|
import { ThemingService } from "./theming/theming.service";
|
||||||
import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -731,9 +730,9 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
EncryptService,
|
EncryptService,
|
||||||
MEMORY_STORAGE,
|
MEMORY_STORAGE,
|
||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
SECURE_STORAGE
|
SECURE_STORAGE,
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
map,
|
map,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
share,
|
share,
|
||||||
|
tap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service";
|
import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service";
|
||||||
@@ -23,6 +24,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
|||||||
activeAccount$ = this.activeAccountId.pipe(
|
activeAccount$ = this.activeAccountId.pipe(
|
||||||
combineLatestWith(this.accounts$),
|
combineLatestWith(this.accounts$),
|
||||||
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
|
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
|
||||||
|
tap((stuff) => console.log("stuff", stuff)),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { DerivedActiveUserState } from "../services/default-active-user-state.pr
|
|||||||
import { DerivedStateDefinition } from "../types/derived-state-definition";
|
import { DerivedStateDefinition } from "../types/derived-state-definition";
|
||||||
|
|
||||||
export interface ActiveUserState<T> {
|
export interface ActiveUserState<T> {
|
||||||
readonly state$: Observable<T>
|
readonly state$: Observable<T>;
|
||||||
readonly getFromState: () => Promise<T>
|
readonly getFromState: () => Promise<T>;
|
||||||
readonly update: (configureState: (state: T) => void) => Promise<void>
|
readonly update: (configureState: (state: T) => void) => Promise<void>;
|
||||||
createDerived: <TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>) => DerivedActiveUserState<T, TTo>
|
createDerived: <TTo>(
|
||||||
|
derivedStateDefinition: DerivedStateDefinition<T, TTo>
|
||||||
|
) => DerivedActiveUserState<T, TTo>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Observable } from "rxjs"
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
export interface GlobalState<T> {
|
export interface GlobalState<T> {
|
||||||
update: (configureState: (state: T) => void) => Promise<void>
|
update: (configureState: (state: T) => void) => Promise<void>;
|
||||||
state$: Observable<T>
|
state$: Observable<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { KeyDefinition } from "../types/key-definition";
|
import { KeyDefinition } from "../types/key-definition";
|
||||||
|
|
||||||
// TODO: Use Matt's `UserId` type
|
// TODO: Use Matts `UserId` type
|
||||||
export function userKeyBuilder(
|
export function userKeyBuilder(userId: string, keyDefinition: KeyDefinition<unknown>): string {
|
||||||
userId: string,
|
if (userId == null) {
|
||||||
keyDefinition: KeyDefinition<unknown>
|
throw new Error("You cannot build a user key without");
|
||||||
): string {
|
}
|
||||||
return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`;
|
return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function globalKeyBuilder(
|
export function globalKeyBuilder(keyDefinition: KeyDefinition<unknown>): string {
|
||||||
keyDefinition: KeyDefinition<unknown>
|
|
||||||
): string {
|
|
||||||
// TODO: Do we want the _global_ part?
|
// TODO: Do we want the _global_ part?
|
||||||
return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`;
|
return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { matches, mock, mockReset } from "jest-mock-extended";
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { StateService } from "../abstractions/state.service"
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
|
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
|
||||||
import { KeyDefinition } from "../types/key-definition";
|
import { KeyDefinition } from "../types/key-definition";
|
||||||
import { StateDefinition } from "../types/state-definition";
|
import { StateDefinition } from "../types/state-definition";
|
||||||
@@ -11,7 +11,7 @@ import { DefaultActiveUserStateProvider } from "./default-active-user-state.prov
|
|||||||
|
|
||||||
class TestState {
|
class TestState {
|
||||||
date: Date;
|
date: Date;
|
||||||
array: string[]
|
array: string[];
|
||||||
// TODO: More complex data types
|
// TODO: More complex data types
|
||||||
|
|
||||||
static fromJSON(jsonState: Jsonify<TestState>) {
|
static fromJSON(jsonState: Jsonify<TestState>) {
|
||||||
@@ -24,7 +24,11 @@ class TestState {
|
|||||||
|
|
||||||
const testStateDefinition = new StateDefinition("fake", "disk");
|
const testStateDefinition = new StateDefinition("fake", "disk");
|
||||||
|
|
||||||
const testKeyDefinition = new KeyDefinition<TestState>(testStateDefinition, "fake", TestState.fromJSON);
|
const testKeyDefinition = new KeyDefinition<TestState>(
|
||||||
|
testStateDefinition,
|
||||||
|
"fake",
|
||||||
|
TestState.fromJSON
|
||||||
|
);
|
||||||
|
|
||||||
describe("DefaultStateProvider", () => {
|
describe("DefaultStateProvider", () => {
|
||||||
const stateService = mock<StateService>();
|
const stateService = mock<StateService>();
|
||||||
@@ -52,13 +56,12 @@ describe("DefaultStateProvider", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("createUserState", async () => {
|
it("createUserState", async () => {
|
||||||
diskStorageService.get
|
diskStorageService.get.mockImplementation(async (key, options) => {
|
||||||
.mockImplementation(async (key, options) => {
|
if (key == "fake_1") {
|
||||||
if (key == "fake_1") {
|
return { date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"] };
|
||||||
return {date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"]}
|
}
|
||||||
}
|
return undefined;
|
||||||
return undefined;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const fakeDomainState = activeUserStateProvider.create(testKeyDefinition);
|
const fakeDomainState = activeUserStateProvider.create(testKeyDefinition);
|
||||||
|
|
||||||
@@ -67,11 +70,11 @@ describe("DefaultStateProvider", () => {
|
|||||||
|
|
||||||
// User signs in
|
// User signs in
|
||||||
activeAccountSubject.next("1");
|
activeAccountSubject.next("1");
|
||||||
await new Promise<void>(resolve => setTimeout(resolve, 10));
|
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
// Service does an update
|
// Service does an update
|
||||||
await fakeDomainState.update(state => state.array.push("value3"));
|
await fakeDomainState.update((state) => state.array.push("value3"));
|
||||||
await new Promise<void>(resolve => setTimeout(resolve, 10));
|
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
|
|
||||||
@@ -79,16 +82,24 @@ describe("DefaultStateProvider", () => {
|
|||||||
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
|
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
|
||||||
|
|
||||||
// Gotten starter user data
|
// Gotten starter user data
|
||||||
expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches<TestState>(value => {
|
expect(subscribeCallback).toHaveBeenNthCalledWith(
|
||||||
return true;
|
2,
|
||||||
}));
|
matches<TestState>((value) => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Gotten update callback data
|
// Gotten update callback data
|
||||||
expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches<TestState>((value) => {
|
expect(subscribeCallback).toHaveBeenNthCalledWith(
|
||||||
return value != null &&
|
3,
|
||||||
typeof value.date == "object" &&
|
matches<TestState>((value) => {
|
||||||
value.date.getFullYear() == 2023 &&
|
return (
|
||||||
value.array.length == 3
|
value != null &&
|
||||||
}));
|
typeof value.date == "object" &&
|
||||||
|
value.date.getFullYear() == 2023 &&
|
||||||
|
value.array.length == 3
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import { BehaviorSubject, Observable, defer, firstValueFrom, map, share, switchMap, tap } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Observable,
|
||||||
|
defer,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
share,
|
||||||
|
switchMap,
|
||||||
|
tap,
|
||||||
|
} from "rxjs";
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider";
|
import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider";
|
||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import {
|
||||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
AbstractMemoryStorageService,
|
||||||
|
AbstractStorageService,
|
||||||
|
} from "../abstractions/storage.service";
|
||||||
import { ActiveUserState } from "../interfaces/active-user-state";
|
import { ActiveUserState } from "../interfaces/active-user-state";
|
||||||
import { userKeyBuilder } from "../misc/key-builders";
|
import { userKeyBuilder } from "../misc/key-builders";
|
||||||
import { UserKey } from "../models/domain/symmetric-crypto-key";
|
import { UserKey } from "../models/domain/symmetric-crypto-key";
|
||||||
@@ -13,20 +25,15 @@ import { KeyDefinition } from "../types/key-definition";
|
|||||||
import { StorageLocation } from "./default-global-state.provider";
|
import { StorageLocation } from "./default-global-state.provider";
|
||||||
|
|
||||||
class ConverterContext {
|
class ConverterContext {
|
||||||
constructor(
|
constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
|
||||||
readonly activeUserKey: UserKey,
|
|
||||||
readonly encryptService: EncryptService
|
|
||||||
) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DerivedStateDefinition<TFrom, TTo> {
|
class DerivedStateDefinition<TFrom, TTo> {
|
||||||
constructor(
|
constructor(readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>) {}
|
||||||
readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>
|
|
||||||
) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DerivedActiveUserState<TFrom, TTo> {
|
export class DerivedActiveUserState<TFrom, TTo> {
|
||||||
state$: Observable<TTo>
|
state$: Observable<TTo>;
|
||||||
|
|
||||||
// TODO: Probably needs to take state service
|
// TODO: Probably needs to take state service
|
||||||
/**
|
/**
|
||||||
@@ -37,12 +44,16 @@ export class DerivedActiveUserState<TFrom, TTo> {
|
|||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private activeUserState: ActiveUserState<TFrom>
|
private activeUserState: ActiveUserState<TFrom>
|
||||||
) {
|
) {
|
||||||
this.state$ = activeUserState.state$
|
this.state$ = activeUserState.state$.pipe(
|
||||||
.pipe(switchMap(async from => {
|
switchMap(async (from) => {
|
||||||
// TODO: How do I get the key?
|
// TODO: How do I get the key?
|
||||||
const convertedData = await derivedStateDefinition.converter(from, new ConverterContext(null, encryptService));
|
const convertedData = await derivedStateDefinition.converter(
|
||||||
|
from,
|
||||||
|
new ConverterContext(null, encryptService)
|
||||||
|
);
|
||||||
return convertedData;
|
return convertedData;
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFromState(): Promise<TTo> {
|
async getFromState(): Promise<TTo> {
|
||||||
@@ -61,7 +72,6 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
private formattedKey$: Observable<string>;
|
private formattedKey$: Observable<string>;
|
||||||
private chosenStorageLocation: AbstractStorageService;
|
private chosenStorageLocation: AbstractStorageService;
|
||||||
|
|
||||||
// TODO: Use BitSubject
|
|
||||||
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
||||||
private stateSubject$ = this.stateSubject.asObservable();
|
private stateSubject$ = this.stateSubject.asObservable();
|
||||||
|
|
||||||
@@ -69,7 +79,7 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private keyDefinition: KeyDefinition<T>,
|
private keyDefinition: KeyDefinition<T>,
|
||||||
private stateService: StateService,
|
private accountService: AccountService,
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private memoryStorageService: AbstractMemoryStorageService,
|
private memoryStorageService: AbstractMemoryStorageService,
|
||||||
private secureStorageService: AbstractStorageService,
|
private secureStorageService: AbstractStorageService,
|
||||||
@@ -78,20 +88,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
this.chosenStorageLocation = this.chooseStorage(
|
this.chosenStorageLocation = this.chooseStorage(
|
||||||
this.keyDefinition.stateDefinition.storageLocation
|
this.keyDefinition.stateDefinition.storageLocation
|
||||||
);
|
);
|
||||||
|
|
||||||
const unformattedKey = `${this.keyDefinition.stateDefinition.name}_{userId}_${this.keyDefinition.key}`;
|
|
||||||
|
|
||||||
// startWith?
|
// startWith?
|
||||||
this.formattedKey$ = this.stateService.activeAccount$
|
this.formattedKey$ = this.accountService.activeAccount$.pipe(
|
||||||
.pipe(
|
tap((user) => console.log("user", user)), // Temp
|
||||||
map(accountId => accountId != null
|
map((account) =>
|
||||||
? unformattedKey.replace("{userId}", accountId)
|
account != null && account.id != null
|
||||||
: null)
|
? userKeyBuilder(account.id, this.keyDefinition)
|
||||||
);
|
: null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const activeAccountData$ = this.formattedKey$
|
const activeAccountData$ = this.formattedKey$.pipe(
|
||||||
.pipe(switchMap(async key => {
|
switchMap(async (key) => {
|
||||||
console.log("user emitted", key);
|
console.log("user emitted: ", key); // temp
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -99,56 +108,69 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
const data = keyDefinition.serializer(jsonData);
|
const data = keyDefinition.serializer(jsonData);
|
||||||
return data;
|
return data;
|
||||||
}),
|
}),
|
||||||
tap(data => {
|
tap((data) => {
|
||||||
console.log("data:", data);
|
this.seededInitial = true;
|
||||||
this.stateSubject.next(data);
|
this.stateSubject.next(data);
|
||||||
}),
|
}),
|
||||||
// Share the execution
|
// Share the execution
|
||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Whomever subscribes to this data, should be notified of updated data
|
// Whomever subscribes to this data, should be notified of updated data
|
||||||
// if someone calls my update() method, or the active user changes.
|
// if someone calls my update() method, or the active user changes.
|
||||||
this.state$ = defer(() => {
|
this.state$ = defer(() => {
|
||||||
console.log("starting subscription.");
|
|
||||||
const subscription = activeAccountData$.subscribe();
|
const subscription = activeAccountData$.subscribe();
|
||||||
return this.stateSubject$
|
return this.stateSubject$.pipe(
|
||||||
.pipe(tap({
|
tap({
|
||||||
complete: () => subscription.unsubscribe(),
|
complete: () => subscription.unsubscribe(),
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(configureState: (state: T) => void): Promise<void> {
|
async update(configureState: (state: T) => void): Promise<void> {
|
||||||
const currentState = await firstValueFrom(this.state$);
|
|
||||||
console.log("data to update:", currentState);
|
|
||||||
configureState(currentState);
|
|
||||||
const key = await this.createKey();
|
const key = await this.createKey();
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
throw new Error("Attempting to active user state, when no user is active.");
|
throw new Error("Attempting to active user state, when no user is active.");
|
||||||
}
|
}
|
||||||
console.log(`updating ${key} to ${currentState}`);
|
const currentState = this.seededInitial
|
||||||
|
? this.stateSubject.getValue()
|
||||||
|
: await this.seedInitial(key);
|
||||||
|
|
||||||
|
configureState(currentState);
|
||||||
|
|
||||||
await this.chosenStorageLocation.save(await this.createKey(), currentState);
|
await this.chosenStorageLocation.save(await this.createKey(), currentState);
|
||||||
this.stateSubject.next(currentState);
|
this.stateSubject.next(currentState);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFromState(): Promise<T> {
|
async getFromState(): Promise<T> {
|
||||||
const activeUserId = await this.stateService.getUserId();
|
const activeUser = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
const key = userKeyBuilder(activeUserId, this.keyDefinition);
|
if (activeUser == null || activeUser.id == null) {
|
||||||
const data = await this.chosenStorageLocation.get(key) as Jsonify<T>;
|
throw new Error("You cannot get data from state while there is no active user.");
|
||||||
|
}
|
||||||
|
const key = userKeyBuilder(activeUser.id, this.keyDefinition);
|
||||||
|
const data = (await this.chosenStorageLocation.get(key)) as Jsonify<T>;
|
||||||
return this.keyDefinition.serializer(data);
|
return this.keyDefinition.serializer(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
createDerived<TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>): DerivedActiveUserState<T, TTo> {
|
createDerived<TTo>(
|
||||||
return new DerivedActiveUserState<T, TTo>(
|
derivedStateDefinition: DerivedStateDefinition<T, TTo>
|
||||||
derivedStateDefinition,
|
): DerivedActiveUserState<T, TTo> {
|
||||||
this.encryptService,
|
return new DerivedActiveUserState<T, TTo>(derivedStateDefinition, this.encryptService, this);
|
||||||
this
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createKey(): Promise<string> {
|
private async createKey(): Promise<string> {
|
||||||
return `${(await firstValueFrom(this.formattedKey$))}`;
|
const formattedKey = await firstValueFrom(this.formattedKey$);
|
||||||
|
if (formattedKey == null) {
|
||||||
|
throw new Error("Cannot create a key while there is no active user.");
|
||||||
|
}
|
||||||
|
return formattedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async seedInitial(key: string): Promise<T> {
|
||||||
|
const data = await this.chosenStorageLocation.get<Jsonify<T>>(key);
|
||||||
|
this.seededInitial = true;
|
||||||
|
return this.keyDefinition.serializer(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private chooseStorage(storageLocation: StorageLocation): AbstractStorageService {
|
private chooseStorage(storageLocation: StorageLocation): AbstractStorageService {
|
||||||
@@ -163,21 +185,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
||||||
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
|
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService, // Inject the lightest weight service that provides accountUserId$
|
private accountService: AccountService, // Inject the lightest weight service that provides accountUserId$
|
||||||
private encryptService: EncryptService,
|
private encryptService: EncryptService,
|
||||||
private memoryStorage: AbstractMemoryStorageService,
|
private memoryStorage: AbstractMemoryStorageService,
|
||||||
private diskStorage: AbstractStorageService,
|
private diskStorage: AbstractStorageService,
|
||||||
private secureStorage: AbstractStorageService) {
|
private secureStorage: AbstractStorageService
|
||||||
}
|
) {}
|
||||||
|
|
||||||
create<T>(keyDefinition: KeyDefinition<T>): DefaultActiveUserState<T> {
|
create<T>(keyDefinition: KeyDefinition<T>): DefaultActiveUserState<T> {
|
||||||
const locationDomainKey =
|
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
||||||
`${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
|
||||||
const existingActiveUserState = this.userStateCache[locationDomainKey];
|
const existingActiveUserState = this.userStateCache[locationDomainKey];
|
||||||
if (existingActiveUserState != null) {
|
if (existingActiveUserState != null) {
|
||||||
// I have to cast out of the unknown generic but this should be safe if rules
|
// I have to cast out of the unknown generic but this should be safe if rules
|
||||||
@@ -187,7 +207,7 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
|
|
||||||
const newActiveUserState = new DefaultActiveUserState<T>(
|
const newActiveUserState = new DefaultActiveUserState<T>(
|
||||||
keyDefinition,
|
keyDefinition,
|
||||||
this.stateService,
|
this.accountService,
|
||||||
this.encryptService,
|
this.encryptService,
|
||||||
this.memoryStorage,
|
this.memoryStorage,
|
||||||
this.secureStorage,
|
this.secureStorage,
|
||||||
|
|||||||
@@ -1,84 +1,89 @@
|
|||||||
import { BehaviorSubject, Observable, defer, firstValueFrom } from "rxjs";
|
import { BehaviorSubject, Observable } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { GlobalStateProvider } from "../abstractions/global-state.provider";
|
import { GlobalStateProvider } from "../abstractions/global-state.provider";
|
||||||
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
|
import {
|
||||||
import { ActiveUserState } from "../interfaces/active-user-state";
|
AbstractMemoryStorageService,
|
||||||
import { KeyDefinition } from "../types/key-definition";
|
AbstractStorageService,
|
||||||
import { Jsonify } from "type-fest";
|
} from "../abstractions/storage.service";
|
||||||
|
import { GlobalState } from "../interfaces/global-state";
|
||||||
import { globalKeyBuilder } from "../misc/key-builders";
|
import { globalKeyBuilder } from "../misc/key-builders";
|
||||||
|
import { KeyDefinition } from "../types/key-definition";
|
||||||
|
|
||||||
|
|
||||||
// TODO: Move type
|
// TODO: Move type
|
||||||
export type StorageLocation = "memory" | "disk" | "secure";
|
export type StorageLocation = "memory" | "disk" | "secure";
|
||||||
|
|
||||||
// class DefaultGlobalState<T> implements ActiveUserState<T> {
|
class GlobalStateImplementation<T> implements GlobalState<T> {
|
||||||
// private storageKey: string;
|
private storageKey: string;
|
||||||
|
private seededPromise: Promise<void>;
|
||||||
|
|
||||||
// protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
|
||||||
|
|
||||||
// state$: Observable<T>;
|
state$: Observable<T>;
|
||||||
|
|
||||||
// constructor(
|
constructor(
|
||||||
// private keyDefinition: KeyDefinition<T>,
|
private keyDefinition: KeyDefinition<T>,
|
||||||
// private chosenLocation: AbstractStorageService
|
private chosenLocation: AbstractStorageService
|
||||||
// ) {
|
) {
|
||||||
// this.storageKey = globalKeyBuilder(this.keyDefinition);
|
this.storageKey = globalKeyBuilder(this.keyDefinition);
|
||||||
|
|
||||||
// // TODO: When subsribed to, we need to read data from the chosen storage location
|
this.seededPromise = this.chosenLocation.get<Jsonify<T>>(this.storageKey).then((data) => {
|
||||||
// // and give it back
|
const serializedData = this.keyDefinition.serializer(data);
|
||||||
// this.state$ = new Observable<T>()
|
this.stateSubject.next(serializedData);
|
||||||
// }
|
});
|
||||||
|
|
||||||
// async update(configureState: (state: T) => void): Promise<void> {
|
this.state$ = this.stateSubject.asObservable();
|
||||||
// const currentState = await firstValueFrom(this.state$);
|
}
|
||||||
// configureState(currentState);
|
|
||||||
// await this.chosenLocation.save(this.storageKey, currentState);
|
|
||||||
// this.stateSubject.next(currentState);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async getFromState(): Promise<T> {
|
async update(configureState: (state: T) => void): Promise<void> {
|
||||||
// const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
|
await this.seededPromise;
|
||||||
// return this.keyDefinition.serializer(data);
|
const currentState = this.stateSubject.getValue();
|
||||||
// }
|
configureState(currentState);
|
||||||
// }
|
await this.chosenLocation.save(this.storageKey, currentState);
|
||||||
|
this.stateSubject.next(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
// export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
async getFromState(): Promise<T> {
|
||||||
// private globalStateCache: Record<string, DefaultGlobalState<unknown>> = {};
|
const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
|
||||||
|
return this.keyDefinition.serializer(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// constructor(
|
export class DefaultGlobalStateProvider implements GlobalStateProvider {
|
||||||
// private memoryStorage: AbstractMemoryStorageService,
|
private globalStateCache: Record<string, GlobalState<unknown>> = {};
|
||||||
// private diskStorage: AbstractStorageService,
|
|
||||||
// private secureStorage: AbstractStorageService) {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// create<T>(keyDefinition: KeyDefinition<T>): DefaultGlobalState<T> {
|
constructor(
|
||||||
// const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
private memoryStorage: AbstractMemoryStorageService,
|
||||||
// const existingGlobalState = this.globalStateCache[locationDomainKey];
|
private diskStorage: AbstractStorageService,
|
||||||
// if (existingGlobalState != null) {
|
private secureStorage: AbstractStorageService
|
||||||
// // I have to cast out of the unknown generic but this should be safe if rules
|
) {}
|
||||||
// // around domain token are made
|
|
||||||
// return existingGlobalState as DefaultGlobalState<T>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
create<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
|
||||||
|
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
|
||||||
|
const existingGlobalState = this.globalStateCache[locationDomainKey];
|
||||||
|
if (existingGlobalState != null) {
|
||||||
|
// I have to cast out of the unknown generic but this should be safe if rules
|
||||||
|
// around domain token are made
|
||||||
|
return existingGlobalState as GlobalStateImplementation<T>;
|
||||||
|
}
|
||||||
|
|
||||||
// const newGlobalState = new DefaultGlobalState<T>(
|
const newGlobalState = new GlobalStateImplementation<T>(
|
||||||
// keyDefinition,
|
keyDefinition,
|
||||||
// this.getLocation(keyDefinition.stateDefinition.storageLocation)
|
this.getLocation(keyDefinition.stateDefinition.storageLocation)
|
||||||
// );
|
);
|
||||||
|
|
||||||
// this.globalStateCache[locationDomainKey] = newGlobalState;
|
this.globalStateCache[locationDomainKey] = newGlobalState;
|
||||||
// return newGlobalState;
|
return newGlobalState;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// private getLocation(location: StorageLocation) {
|
private getLocation(location: StorageLocation) {
|
||||||
// switch (location) {
|
switch (location) {
|
||||||
// case "disk":
|
case "disk":
|
||||||
// return this.diskStorage;
|
return this.diskStorage;
|
||||||
// case "secure":
|
case "secure":
|
||||||
// return this.secureStorage;
|
return this.secureStorage;
|
||||||
// case "memory":
|
case "memory":
|
||||||
// return this.memoryStorage;
|
return this.memoryStorage;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -190,14 +190,21 @@ export class StateService<
|
|||||||
state.accounts[userId] = this.createAccount();
|
state.accounts[userId] = this.createAccount();
|
||||||
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
const diskAccount = await this.getAccountFromDisk({ userId: userId });
|
||||||
state.accounts[userId].profile = diskAccount.profile;
|
state.accounts[userId].profile = diskAccount.profile;
|
||||||
|
this.accountService.addAccount(userId as UserId, {
|
||||||
|
status: AuthenticationStatus.Locked,
|
||||||
|
name: diskAccount.profile.name,
|
||||||
|
email: diskAccount.profile.email,
|
||||||
|
});
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Temporary update to avoid routing all account status changes through account service for now.
|
|
||||||
this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.Locked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccount(account: TAccount) {
|
async addAccount(account: TAccount) {
|
||||||
|
// this.accountService.addAccount(account.profile.userId as UserId, {
|
||||||
|
// email: account.profile.email,
|
||||||
|
// name: account.profile.name,
|
||||||
|
// status: AuthenticationStatus.Locked,
|
||||||
|
// });
|
||||||
account = await this.setAccountEnvironment(account);
|
account = await this.setAccountEnvironment(account);
|
||||||
await this.updateState(async (state) => {
|
await this.updateState(async (state) => {
|
||||||
state.authenticatedAccounts.push(account.profile.userId);
|
state.authenticatedAccounts.push(account.profile.userId);
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { StorageLocation } from "../services/default-global-state.provider";
|
|||||||
|
|
||||||
// TODO: Move type
|
// TODO: Move type
|
||||||
export class DeriveContext {
|
export class DeriveContext {
|
||||||
constructor(
|
constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
|
||||||
readonly activeUserKey: UserKey,
|
|
||||||
readonly encryptService: EncryptService
|
|
||||||
) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DerivedStateDefinition<TFrom, TTo> {
|
export class DerivedStateDefinition<TFrom, TTo> {
|
||||||
|
|||||||
@@ -18,16 +18,24 @@ export class KeyDefinition<T> {
|
|||||||
readonly stateDefinition: StateDefinition,
|
readonly stateDefinition: StateDefinition,
|
||||||
readonly key: string,
|
readonly key: string,
|
||||||
readonly serializer: (jsonValue: Jsonify<T>) => T
|
readonly serializer: (jsonValue: Jsonify<T>) => T
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
static array<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) {
|
static array<T>(
|
||||||
|
stateDefinition: StateDefinition,
|
||||||
|
key: string,
|
||||||
|
serializer: (jsonValue: Jsonify<T>) => T
|
||||||
|
) {
|
||||||
return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => {
|
return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => {
|
||||||
// TODO: Should we handle null for them, I feel like we should discourage null for an array?
|
// TODO: Should we handle null for them, I feel like we should discourage null for an array?
|
||||||
return jsonValue.map(v => serializer(v));
|
return jsonValue.map((v) => serializer(v));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static record<T>(stateDefinition: StateDefinition, key: string, serializer: (jsonValue: Jsonify<T>) => T) {
|
static record<T>(
|
||||||
|
stateDefinition: StateDefinition,
|
||||||
|
key: string,
|
||||||
|
serializer: (jsonValue: Jsonify<T>) => T
|
||||||
|
) {
|
||||||
return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => {
|
return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => {
|
||||||
const output: Record<string, T> = {};
|
const output: Record<string, T> = {};
|
||||||
for (const key in jsonValue) {
|
for (const key in jsonValue) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// TODO: How can we protect the creation of these so that platform can maintain the allowed creations?
|
// TODO: Make this not allowed to be able to be imported anywhere willy-nilly.
|
||||||
|
|
||||||
// TODO: Where should this live
|
// TODO: Where should this live
|
||||||
export type StorageLocation = "disk" | "memory" | "secure";
|
export type StorageLocation = "disk" | "memory" | "secure";
|
||||||
@@ -8,12 +8,9 @@ export type StorageLocation = "disk" | "memory" | "secure";
|
|||||||
*/
|
*/
|
||||||
export class StateDefinition {
|
export class StateDefinition {
|
||||||
/**
|
/**
|
||||||
*
|
* Creates a new instance of {@link StateDefinition}, the creation of which is owned by the platform team.
|
||||||
* @param name The name of the state, this needs to be unique from all other {@link StateDefinition}'s.
|
* @param name The name of the state, this needs to be unique from all other {@link StateDefinition}'s.
|
||||||
* @param storageLocation The location of where this state should be stored.
|
* @param storageLocation The location of where this state should be stored.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(readonly name: string, readonly storageLocation: StorageLocation) {}
|
||||||
readonly name: string,
|
|
||||||
readonly storageLocation: StorageLocation
|
|
||||||
) { }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ it("has all unique definitions", () => {
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const definition = (definitions as unknown as Record<string, StateDefinition>)[key];
|
const definition = (definitions as unknown as Record<string, StateDefinition>)[key];
|
||||||
if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) {
|
if (Object.getPrototypeOf(definition) !== StateDefinition.prototype) {
|
||||||
throw new Error(`${key} from import ./state-definitions is expected to be a StateDefinition but wasn't.`);
|
throw new Error(
|
||||||
|
`${key} from import ./state-definitions is expected to be a StateDefinition but wasn't.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = `${definition.name}_${definition.storageLocation}`;
|
const name = `${definition.name}_${definition.storageLocation}`;
|
||||||
|
|
||||||
if (uniqueNames.includes(name)) {
|
if (uniqueNames.includes(name)) {
|
||||||
throw new Error(`Definition ${key} is invalid, it's elements have already been claimed. Please choose a unique name.`);
|
throw new Error(
|
||||||
|
`Definition ${key} is invalid, its elements have already been claimed. Please choose a unique name.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueNames.push(name);
|
uniqueNames.push(name);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { IRREVERSIBLE, Migrator } from "../migrator";
|
|||||||
type ExpectedAccountType = {
|
type ExpectedAccountType = {
|
||||||
data: {
|
data: {
|
||||||
folders: {
|
folders: {
|
||||||
encrypted: Record<string, { name: string, id: string, revisionDate: string }>
|
encrypted: Record<string, { name: string; id: string; revisionDate: string }>;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const FOLDER_STATE = new StateDefinition("FolderService", "disk");
|
const FOLDER_STATE = new StateDefinition("FolderService", "disk");
|
||||||
@@ -40,9 +40,7 @@ export class MoveFolderToOwnedMigrator extends Migrator<8, 9> {
|
|||||||
// await helper.set("", account);
|
// await helper.set("", account);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account)));
|
||||||
accounts.map(({ userId, account}) => updateAccount(userId, account))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rollback(helper: MigrationHelper): Promise<void> {
|
rollback(helper: MigrationHelper): Promise<void> {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { FolderResponse } from "../response/folder.response";
|
import { FolderResponse } from "../response/folder.response";
|
||||||
|
|
||||||
export class FolderData {
|
export class FolderData {
|
||||||
@@ -5,9 +7,17 @@ export class FolderData {
|
|||||||
name: string;
|
name: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
|
|
||||||
constructor(response: FolderResponse) {
|
constructor(response: Partial<FolderResponse>) {
|
||||||
this.name = response.name;
|
this.name = response.name;
|
||||||
this.id = response.id;
|
this.id = response.id;
|
||||||
this.revisionDate = response.revisionDate;
|
this.revisionDate = response.revisionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(jsonObj: Jsonify<FolderData>) {
|
||||||
|
return new FolderData({
|
||||||
|
id: jsonObj.id,
|
||||||
|
name: jsonObj.name,
|
||||||
|
revisionDate: jsonObj.revisionDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ import { Folder } from "../../../vault/models/domain/folder";
|
|||||||
import { FolderView } from "../../../vault/models/view/folder.view";
|
import { FolderView } from "../../../vault/models/view/folder.view";
|
||||||
import { FOLDERS } from "../../types/key-definitions";
|
import { FOLDERS } from "../../types/key-definitions";
|
||||||
|
|
||||||
|
|
||||||
export class FolderService implements InternalFolderServiceAbstraction {
|
export class FolderService implements InternalFolderServiceAbstraction {
|
||||||
|
folderState: ActiveUserState<Record<string, FolderData>>;
|
||||||
folderState: ActiveUserState<Record<string, Folder>>;
|
decryptedFolderState: DerivedActiveUserState<Record<string, FolderData>, FolderView[]>;
|
||||||
decryptedFolderState: DerivedActiveUserState<Record<string, Folder>, FolderView[]>
|
|
||||||
|
|
||||||
folders$: Observable<Folder[]>;
|
folders$: Observable<Folder[]>;
|
||||||
folderViews$: Observable<FolderView[]>;
|
folderViews$: Observable<FolderView[]>;
|
||||||
@@ -32,18 +30,22 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
private activeUserStateProvider: ActiveUserStateProvider,
|
private activeUserStateProvider: ActiveUserStateProvider,
|
||||||
private stateService: StateService
|
private stateService: StateService
|
||||||
) {
|
) {
|
||||||
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => {
|
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition(
|
||||||
const folders = this.flattenMap(foldersMap);
|
"memory",
|
||||||
const decryptedFolders = await this.decryptFolders(folders);
|
async (foldersMap) => {
|
||||||
return decryptedFolders;
|
const folders = this.flattenMap(foldersMap);
|
||||||
})
|
const decryptedFolders = await this.decryptFolders(folders);
|
||||||
|
return decryptedFolders;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.folderState = this.activeUserStateProvider.create(FOLDERS);
|
this.folderState = this.activeUserStateProvider.create(FOLDERS);
|
||||||
|
|
||||||
this.folders$ = this.folderState.state$
|
this.folders$ = this.folderState.state$.pipe(
|
||||||
.pipe(map(foldersMap => {
|
map((foldersMap) => {
|
||||||
return this.flattenMap(foldersMap);
|
return this.flattenMap(foldersMap);
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition);
|
this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition);
|
||||||
this.folderViews$ = this.decryptedFolderState.state$;
|
this.folderViews$ = this.decryptedFolderState.state$;
|
||||||
@@ -64,7 +66,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
|
|
||||||
async get(id: string): Promise<Folder> {
|
async get(id: string): Promise<Folder> {
|
||||||
const folders = await firstValueFrom(this.folderState.state$);
|
const folders = await firstValueFrom(this.folderState.state$);
|
||||||
return folders[id];
|
return new Folder(folders[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllFromState(): Promise<Folder[]> {
|
async getAllFromState(): Promise<Folder[]> {
|
||||||
@@ -83,7 +85,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return folder;
|
return new Folder(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,37 +96,28 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsert(folder: FolderData | FolderData[]): Promise<void> {
|
async upsert(folder: FolderData | FolderData[]): Promise<void> {
|
||||||
console.log("upsert", folder);
|
await this.folderState.update((folders) => {
|
||||||
await this.folderState.update(folders => {
|
|
||||||
if (folder instanceof FolderData) {
|
if (folder instanceof FolderData) {
|
||||||
const f = folder as FolderData;
|
folders[folder.id] = folder;
|
||||||
folders[f.id] = new Folder(f);
|
|
||||||
} else {
|
} else {
|
||||||
(folder as FolderData[]).forEach((f) => {
|
(folder as FolderData[]).forEach((f) => {
|
||||||
folders[f.id] = new Folder(f);
|
folders[f.id] = f;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async replace(folders: { [id: string]: FolderData }): Promise<void> {
|
async replace(folders: { [id: string]: FolderData }): Promise<void> {
|
||||||
const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => {
|
await this.folderState.update((f) => (f = folders));
|
||||||
agg[key] = new Folder(value);
|
|
||||||
return agg;
|
|
||||||
}, {} as Record<string, Folder>);
|
|
||||||
console.log("replace", folders, convertedFolders);
|
|
||||||
await this.folderState.update(f => f = convertedFolders);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(userId?: string): Promise<any> {
|
async clear(userId?: string): Promise<any> {
|
||||||
console.log("clear", userId);
|
await this.folderState.update((f) => (f = null));
|
||||||
await this.folderState.update(f => f = null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string | string[]): Promise<void> {
|
async delete(id: string | string[]): Promise<void> {
|
||||||
const folderIds = typeof id === "string" ? [id] : id;
|
const folderIds = typeof id === "string" ? [id] : id;
|
||||||
console.log("delete", folderIds);
|
await this.folderState.update((folders) => {
|
||||||
await this.folderState.update(folders => {
|
|
||||||
for (const folderId in folderIds) {
|
for (const folderId in folderIds) {
|
||||||
delete folders[folderId];
|
delete folders[folderId];
|
||||||
}
|
}
|
||||||
@@ -159,10 +152,10 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
return decryptedFolders;
|
return decryptedFolders;
|
||||||
}
|
}
|
||||||
|
|
||||||
private flattenMap(foldersMap: Record<string, Folder>): Folder[] {
|
private flattenMap(foldersMap: Record<string, FolderData>): Folder[] {
|
||||||
const folders: Folder[] = [];
|
const folders: Folder[] = [];
|
||||||
for (const id in foldersMap) {
|
for (const id in foldersMap) {
|
||||||
folders.push(foldersMap[id]);
|
folders.push(new Folder(foldersMap[id]));
|
||||||
}
|
}
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { KeyDefinition } from "../../platform/types/key-definition";
|
import { KeyDefinition } from "../../platform/types/key-definition";
|
||||||
import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions";
|
import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions";
|
||||||
import { Folder } from "../models/domain/folder";
|
import { FolderData } from "../models/data/folder.data";
|
||||||
|
|
||||||
// FolderService Keys
|
// FolderService Keys
|
||||||
export const FOLDERS = KeyDefinition.record<Folder>(FOLDER_SERVICE_DISK, "folders", Folder.fromJSON);
|
export const FOLDERS = KeyDefinition.record<FolderData>(
|
||||||
|
FOLDER_SERVICE_DISK,
|
||||||
|
"folders",
|
||||||
|
FolderData.fromJSON
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user