1
0
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:
Justin Baur
2023-10-02 16:54:35 -04:00
parent 39574fe6e4
commit 33c8d55f0d
18 changed files with 286 additions and 225 deletions

View File

@@ -5,6 +5,7 @@ import {
map,
distinctUntilChanged,
share,
tap,
} from "rxjs";
import { AccountInfo, InternalAccountService } from "../../auth/abstractions/account.service";
@@ -23,6 +24,7 @@ export class AccountServiceImplementation implements InternalAccountService {
activeAccount$ = this.activeAccountId.pipe(
combineLatestWith(this.accounts$),
map(([id, accounts]) => (id ? { id, ...accounts[id] } : undefined)),
tap((stuff) => console.log("stuff", stuff)),
distinctUntilChanged(),
share()
);

View File

@@ -4,8 +4,10 @@ import { DerivedActiveUserState } from "../services/default-active-user-state.pr
import { DerivedStateDefinition } from "../types/derived-state-definition";
export interface ActiveUserState<T> {
readonly state$: Observable<T>
readonly getFromState: () => Promise<T>
readonly update: (configureState: (state: T) => void) => Promise<void>
createDerived: <TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>) => DerivedActiveUserState<T, TTo>
readonly state$: Observable<T>;
readonly getFromState: () => Promise<T>;
readonly update: (configureState: (state: T) => void) => Promise<void>;
createDerived: <TTo>(
derivedStateDefinition: DerivedStateDefinition<T, TTo>
) => DerivedActiveUserState<T, TTo>;
}

View File

@@ -1,6 +1,6 @@
import { Observable } from "rxjs"
import { Observable } from "rxjs";
export interface GlobalState<T> {
update: (configureState: (state: T) => void) => Promise<void>
state$: Observable<T>
update: (configureState: (state: T) => void) => Promise<void>;
state$: Observable<T>;
}

View File

@@ -1,16 +1,14 @@
import { KeyDefinition } from "../types/key-definition";
// TODO: Use Matt's `UserId` type
export function userKeyBuilder(
userId: string,
keyDefinition: KeyDefinition<unknown>
): string {
// TODO: Use Matts `UserId` type
export function userKeyBuilder(userId: string, keyDefinition: KeyDefinition<unknown>): string {
if (userId == null) {
throw new Error("You cannot build a user key without");
}
return `${keyDefinition.stateDefinition.name}_${userId}_${keyDefinition.key}`;
}
export function globalKeyBuilder(
keyDefinition: KeyDefinition<unknown>
): string {
export function globalKeyBuilder(keyDefinition: KeyDefinition<unknown>): string {
// TODO: Do we want the _global_ part?
return `${keyDefinition.stateDefinition.name}_global_${keyDefinition.key}`;
}

View File

@@ -2,7 +2,7 @@ import { matches, mock, mockReset } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { Jsonify } from "type-fest";
import { StateService } from "../abstractions/state.service"
import { StateService } from "../abstractions/state.service";
import { AbstractMemoryStorageService } from "../abstractions/storage.service";
import { KeyDefinition } from "../types/key-definition";
import { StateDefinition } from "../types/state-definition";
@@ -11,7 +11,7 @@ import { DefaultActiveUserStateProvider } from "./default-active-user-state.prov
class TestState {
date: Date;
array: string[]
array: string[];
// TODO: More complex data types
static fromJSON(jsonState: Jsonify<TestState>) {
@@ -24,7 +24,11 @@ class TestState {
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", () => {
const stateService = mock<StateService>();
@@ -52,13 +56,12 @@ describe("DefaultStateProvider", () => {
});
it("createUserState", async () => {
diskStorageService.get
.mockImplementation(async (key, options) => {
if (key == "fake_1") {
return {date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"]}
}
return undefined;
});
diskStorageService.get.mockImplementation(async (key, options) => {
if (key == "fake_1") {
return { date: "2023-09-21T13:14:17.648Z", array: ["value1", "value2"] };
}
return undefined;
});
const fakeDomainState = activeUserStateProvider.create(testKeyDefinition);
@@ -67,11 +70,11 @@ describe("DefaultStateProvider", () => {
// User signs in
activeAccountSubject.next("1");
await new Promise<void>(resolve => setTimeout(resolve, 10));
await new Promise<void>((resolve) => setTimeout(resolve, 10));
// Service does an update
await fakeDomainState.update(state => state.array.push("value3"));
await new Promise<void>(resolve => setTimeout(resolve, 10));
await fakeDomainState.update((state) => state.array.push("value3"));
await new Promise<void>((resolve) => setTimeout(resolve, 10));
subscription.unsubscribe();
@@ -79,16 +82,24 @@ describe("DefaultStateProvider", () => {
expect(subscribeCallback).toHaveBeenNthCalledWith(1, null);
// Gotten starter user data
expect(subscribeCallback).toHaveBeenNthCalledWith(2, matches<TestState>(value => {
return true;
}));
expect(subscribeCallback).toHaveBeenNthCalledWith(
2,
matches<TestState>((value) => {
return true;
})
);
// Gotten update callback data
expect(subscribeCallback).toHaveBeenNthCalledWith(3, matches<TestState>((value) => {
return value != null &&
typeof value.date == "object" &&
value.date.getFullYear() == 2023 &&
value.array.length == 3
}));
expect(subscribeCallback).toHaveBeenNthCalledWith(
3,
matches<TestState>((value) => {
return (
value != null &&
typeof value.date == "object" &&
value.date.getFullYear() == 2023 &&
value.array.length == 3
);
})
);
});
});

View File

@@ -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 { AccountService } from "../../auth/abstractions/account.service";
import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider";
import { EncryptService } from "../abstractions/encrypt.service";
import { StateService } from "../abstractions/state.service";
import { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { ActiveUserState } from "../interfaces/active-user-state";
import { userKeyBuilder } from "../misc/key-builders";
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";
class ConverterContext {
constructor(
readonly activeUserKey: UserKey,
readonly encryptService: EncryptService
) { }
constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
}
class DerivedStateDefinition<TFrom, TTo> {
constructor(
readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>
) { }
constructor(readonly converter: (data: TFrom, context: ConverterContext) => Promise<TTo>) {}
}
export class DerivedActiveUserState<TFrom, TTo> {
state$: Observable<TTo>
state$: Observable<TTo>;
// TODO: Probably needs to take state service
/**
@@ -37,12 +44,16 @@ export class DerivedActiveUserState<TFrom, TTo> {
private encryptService: EncryptService,
private activeUserState: ActiveUserState<TFrom>
) {
this.state$ = activeUserState.state$
.pipe(switchMap(async from => {
this.state$ = activeUserState.state$.pipe(
switchMap(async (from) => {
// 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;
}));
})
);
}
async getFromState(): Promise<TTo> {
@@ -61,7 +72,6 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
private formattedKey$: Observable<string>;
private chosenStorageLocation: AbstractStorageService;
// TODO: Use BitSubject
protected stateSubject: BehaviorSubject<T | null> = new BehaviorSubject<T | null>(null);
private stateSubject$ = this.stateSubject.asObservable();
@@ -69,7 +79,7 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
constructor(
private keyDefinition: KeyDefinition<T>,
private stateService: StateService,
private accountService: AccountService,
private encryptService: EncryptService,
private memoryStorageService: AbstractMemoryStorageService,
private secureStorageService: AbstractStorageService,
@@ -78,20 +88,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
this.chosenStorageLocation = this.chooseStorage(
this.keyDefinition.stateDefinition.storageLocation
);
const unformattedKey = `${this.keyDefinition.stateDefinition.name}_{userId}_${this.keyDefinition.key}`;
// startWith?
this.formattedKey$ = this.stateService.activeAccount$
.pipe(
map(accountId => accountId != null
? unformattedKey.replace("{userId}", accountId)
: null)
);
this.formattedKey$ = this.accountService.activeAccount$.pipe(
tap((user) => console.log("user", user)), // Temp
map((account) =>
account != null && account.id != null
? userKeyBuilder(account.id, this.keyDefinition)
: null
)
);
const activeAccountData$ = this.formattedKey$
.pipe(switchMap(async key => {
console.log("user emitted", key);
const activeAccountData$ = this.formattedKey$.pipe(
switchMap(async (key) => {
console.log("user emitted: ", key); // temp
if (key == null) {
return null;
}
@@ -99,56 +108,69 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
const data = keyDefinition.serializer(jsonData);
return data;
}),
tap(data => {
console.log("data:", data);
this.stateSubject.next(data);
}),
// Share the execution
share()
);
tap((data) => {
this.seededInitial = true;
this.stateSubject.next(data);
}),
// Share the execution
share()
);
// Whomever subscribes to this data, should be notified of updated data
// if someone calls my update() method, or the active user changes.
this.state$ = defer(() => {
console.log("starting subscription.");
const subscription = activeAccountData$.subscribe();
return this.stateSubject$
.pipe(tap({
return this.stateSubject$.pipe(
tap({
complete: () => subscription.unsubscribe(),
}));
})
);
});
}
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();
if (key == null) {
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);
this.stateSubject.next(currentState);
}
async getFromState(): Promise<T> {
const activeUserId = await this.stateService.getUserId();
const key = userKeyBuilder(activeUserId, this.keyDefinition);
const data = await this.chosenStorageLocation.get(key) as Jsonify<T>;
const activeUser = await firstValueFrom(this.accountService.activeAccount$);
if (activeUser == null || activeUser.id == null) {
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);
}
createDerived<TTo>(derivedStateDefinition: DerivedStateDefinition<T, TTo>): DerivedActiveUserState<T, TTo> {
return new DerivedActiveUserState<T, TTo>(
derivedStateDefinition,
this.encryptService,
this
);
createDerived<TTo>(
derivedStateDefinition: DerivedStateDefinition<T, TTo>
): DerivedActiveUserState<T, TTo> {
return new DerivedActiveUserState<T, TTo>(derivedStateDefinition, this.encryptService, this);
}
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 {
@@ -163,21 +185,19 @@ class DefaultActiveUserState<T> implements ActiveUserState<T> {
}
}
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
private userStateCache: Record<string, DefaultActiveUserState<unknown>> = {};
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 memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService) {
}
private secureStorage: AbstractStorageService
) {}
create<T>(keyDefinition: KeyDefinition<T>): DefaultActiveUserState<T> {
const locationDomainKey =
`${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`;
const existingActiveUserState = this.userStateCache[locationDomainKey];
if (existingActiveUserState != null) {
// 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>(
keyDefinition,
this.stateService,
this.accountService,
this.encryptService,
this.memoryStorage,
this.secureStorage,

View File

@@ -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 { AbstractMemoryStorageService, AbstractStorageService } from "../abstractions/storage.service";
import { ActiveUserState } from "../interfaces/active-user-state";
import { KeyDefinition } from "../types/key-definition";
import { Jsonify } from "type-fest";
import {
AbstractMemoryStorageService,
AbstractStorageService,
} from "../abstractions/storage.service";
import { GlobalState } from "../interfaces/global-state";
import { globalKeyBuilder } from "../misc/key-builders";
import { KeyDefinition } from "../types/key-definition";
// TODO: Move type
export type StorageLocation = "memory" | "disk" | "secure";
// class DefaultGlobalState<T> implements ActiveUserState<T> {
// private storageKey: string;
class GlobalStateImplementation<T> implements GlobalState<T> {
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(
// private keyDefinition: KeyDefinition<T>,
// private chosenLocation: AbstractStorageService
// ) {
// this.storageKey = globalKeyBuilder(this.keyDefinition);
constructor(
private keyDefinition: KeyDefinition<T>,
private chosenLocation: AbstractStorageService
) {
this.storageKey = globalKeyBuilder(this.keyDefinition);
// // TODO: When subsribed to, we need to read data from the chosen storage location
// // and give it back
// this.state$ = new Observable<T>()
// }
this.seededPromise = this.chosenLocation.get<Jsonify<T>>(this.storageKey).then((data) => {
const serializedData = this.keyDefinition.serializer(data);
this.stateSubject.next(serializedData);
});
// async update(configureState: (state: T) => void): Promise<void> {
// const currentState = await firstValueFrom(this.state$);
// configureState(currentState);
// await this.chosenLocation.save(this.storageKey, currentState);
// this.stateSubject.next(currentState);
// }
this.state$ = this.stateSubject.asObservable();
}
// async getFromState(): Promise<T> {
// const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
// return this.keyDefinition.serializer(data);
// }
// }
async update(configureState: (state: T) => void): Promise<void> {
await this.seededPromise;
const currentState = this.stateSubject.getValue();
configureState(currentState);
await this.chosenLocation.save(this.storageKey, currentState);
this.stateSubject.next(currentState);
}
// export class DefaultGlobalStateProvider implements GlobalStateProvider {
// private globalStateCache: Record<string, DefaultGlobalState<unknown>> = {};
async getFromState(): Promise<T> {
const data = await this.chosenLocation.get<Jsonify<T>>(this.storageKey);
return this.keyDefinition.serializer(data);
}
}
// constructor(
// private memoryStorage: AbstractMemoryStorageService,
// private diskStorage: AbstractStorageService,
// private secureStorage: AbstractStorageService) {
// }
export class DefaultGlobalStateProvider implements GlobalStateProvider {
private globalStateCache: Record<string, GlobalState<unknown>> = {};
// create<T>(keyDefinition: KeyDefinition<T>): DefaultGlobalState<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 DefaultGlobalState<T>;
// }
constructor(
private memoryStorage: AbstractMemoryStorageService,
private diskStorage: AbstractStorageService,
private secureStorage: AbstractStorageService
) {}
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>(
// keyDefinition,
// this.getLocation(keyDefinition.stateDefinition.storageLocation)
// );
const newGlobalState = new GlobalStateImplementation<T>(
keyDefinition,
this.getLocation(keyDefinition.stateDefinition.storageLocation)
);
// this.globalStateCache[locationDomainKey] = newGlobalState;
// return newGlobalState;
// }
this.globalStateCache[locationDomainKey] = newGlobalState;
return newGlobalState;
}
// private getLocation(location: StorageLocation) {
// switch (location) {
// case "disk":
// return this.diskStorage;
// case "secure":
// return this.secureStorage;
// case "memory":
// return this.memoryStorage;
// }
// }
// }
private getLocation(location: StorageLocation) {
switch (location) {
case "disk":
return this.diskStorage;
case "secure":
return this.secureStorage;
case "memory":
return this.memoryStorage;
}
}
}

View File

@@ -190,14 +190,21 @@ export class StateService<
state.accounts[userId] = this.createAccount();
const diskAccount = await this.getAccountFromDisk({ userId: userId });
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;
});
// 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) {
// this.accountService.addAccount(account.profile.userId as UserId, {
// email: account.profile.email,
// name: account.profile.name,
// status: AuthenticationStatus.Locked,
// });
account = await this.setAccountEnvironment(account);
await this.updateState(async (state) => {
state.authenticatedAccounts.push(account.profile.userId);

View File

@@ -4,10 +4,7 @@ import { StorageLocation } from "../services/default-global-state.provider";
// TODO: Move type
export class DeriveContext {
constructor(
readonly activeUserKey: UserKey,
readonly encryptService: EncryptService
) { }
constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {}
}
export class DerivedStateDefinition<TFrom, TTo> {

View File

@@ -18,16 +18,24 @@ export class KeyDefinition<T> {
readonly stateDefinition: StateDefinition,
readonly key: string,
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) => {
// 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) => {
const output: Record<string, T> = {};
for (const key in jsonValue) {

View File

@@ -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
export type StorageLocation = "disk" | "memory" | "secure";
@@ -8,12 +8,9 @@ export type StorageLocation = "disk" | "memory" | "secure";
*/
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 storageLocation The location of where this state should be stored.
*/
constructor(
readonly name: string,
readonly storageLocation: StorageLocation
) { }
constructor(readonly name: string, readonly storageLocation: StorageLocation) {}
}

View File

@@ -8,13 +8,17 @@ it("has all unique definitions", () => {
for (const key of keys) {
const definition = (definitions as unknown as Record<string, StateDefinition>)[key];
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}`;
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);

View File

@@ -13,9 +13,9 @@ import { IRREVERSIBLE, Migrator } from "../migrator";
type ExpectedAccountType = {
data: {
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");
@@ -40,9 +40,7 @@ export class MoveFolderToOwnedMigrator extends Migrator<8, 9> {
// await helper.set("", account);
}
await Promise.all(
accounts.map(({ userId, account}) => updateAccount(userId, account))
);
await Promise.all(accounts.map(({ userId, account }) => updateAccount(userId, account)));
}
rollback(helper: MigrationHelper): Promise<void> {

View File

@@ -1,3 +1,5 @@
import { Jsonify } from "type-fest";
import { FolderResponse } from "../response/folder.response";
export class FolderData {
@@ -5,9 +7,17 @@ export class FolderData {
name: string;
revisionDate: string;
constructor(response: FolderResponse) {
constructor(response: Partial<FolderResponse>) {
this.name = response.name;
this.id = response.id;
this.revisionDate = response.revisionDate;
}
static fromJSON(jsonObj: Jsonify<FolderData>) {
return new FolderData({
id: jsonObj.id,
name: jsonObj.name,
revisionDate: jsonObj.revisionDate,
});
}
}

View File

@@ -16,11 +16,9 @@ import { Folder } from "../../../vault/models/domain/folder";
import { FolderView } from "../../../vault/models/view/folder.view";
import { FOLDERS } from "../../types/key-definitions";
export class FolderService implements InternalFolderServiceAbstraction {
folderState: ActiveUserState<Record<string, Folder>>;
decryptedFolderState: DerivedActiveUserState<Record<string, Folder>, FolderView[]>
folderState: ActiveUserState<Record<string, FolderData>>;
decryptedFolderState: DerivedActiveUserState<Record<string, FolderData>, FolderView[]>;
folders$: Observable<Folder[]>;
folderViews$: Observable<FolderView[]>;
@@ -32,18 +30,22 @@ export class FolderService implements InternalFolderServiceAbstraction {
private activeUserStateProvider: ActiveUserStateProvider,
private stateService: StateService
) {
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition("memory", async (foldersMap) => {
const folders = this.flattenMap(foldersMap);
const decryptedFolders = await this.decryptFolders(folders);
return decryptedFolders;
})
const derivedFoldersDefinition = FOLDERS.createDerivedDefinition(
"memory",
async (foldersMap) => {
const folders = this.flattenMap(foldersMap);
const decryptedFolders = await this.decryptFolders(folders);
return decryptedFolders;
}
);
this.folderState = this.activeUserStateProvider.create(FOLDERS);
this.folders$ = this.folderState.state$
.pipe(map(foldersMap => {
this.folders$ = this.folderState.state$.pipe(
map((foldersMap) => {
return this.flattenMap(foldersMap);
}));
})
);
this.decryptedFolderState = this.folderState.createDerived(derivedFoldersDefinition);
this.folderViews$ = this.decryptedFolderState.state$;
@@ -64,7 +66,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
async get(id: string): Promise<Folder> {
const folders = await firstValueFrom(this.folderState.state$);
return folders[id];
return new Folder(folders[id]);
}
async getAllFromState(): Promise<Folder[]> {
@@ -83,7 +85,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
return null;
}
return folder;
return new Folder(folder);
}
/**
@@ -94,37 +96,28 @@ export class FolderService implements InternalFolderServiceAbstraction {
}
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) {
const f = folder as FolderData;
folders[f.id] = new Folder(f);
folders[folder.id] = folder;
} else {
(folder as FolderData[]).forEach((f) => {
folders[f.id] = new Folder(f);
folders[f.id] = f;
});
}
});
}
async replace(folders: { [id: string]: FolderData }): Promise<void> {
const convertedFolders = Object.entries(folders).reduce((agg, [key, value]) => {
agg[key] = new Folder(value);
return agg;
}, {} as Record<string, Folder>);
console.log("replace", folders, convertedFolders);
await this.folderState.update(f => f = convertedFolders);
await this.folderState.update((f) => (f = folders));
}
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> {
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) {
delete folders[folderId];
}
@@ -159,10 +152,10 @@ export class FolderService implements InternalFolderServiceAbstraction {
return decryptedFolders;
}
private flattenMap(foldersMap: Record<string, Folder>): Folder[] {
private flattenMap(foldersMap: Record<string, FolderData>): Folder[] {
const folders: Folder[] = [];
for (const id in foldersMap) {
folders.push(foldersMap[id]);
folders.push(new Folder(foldersMap[id]));
}
return folders;
}

View File

@@ -1,6 +1,10 @@
import { KeyDefinition } from "../../platform/types/key-definition";
import { FOLDER_SERVICE_DISK } from "../../platform/types/state-definitions";
import { Folder } from "../models/domain/folder";
import { FolderData } from "../models/data/folder.data";
// 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
);