diff --git a/libs/common/spec/misc/utils.spec.ts b/libs/common/spec/misc/utils.spec.ts index e1de2655322..b7c8c53c4f2 100644 --- a/libs/common/spec/misc/utils.spec.ts +++ b/libs/common/spec/misc/utils.spec.ts @@ -241,4 +241,59 @@ describe("Utils Service", () => { expect(Utils.fromByteStringToArray(null)).toEqual(null); }); }); + + describe("mapToRecord", () => { + it("should handle null", () => { + expect(Utils.mapToRecord(null)).toEqual(null); + }); + + it("should handle empty map", () => { + expect(Utils.mapToRecord(new Map())).toEqual({}); + }); + + it("should handle convert a Map to a Record", () => { + const map = new Map([ + ["key1", "value1"], + ["key2", "value2"], + ]); + expect(Utils.mapToRecord(map)).toEqual({ key1: "value1", key2: "value2" }); + }); + + it("should handle convert a Map to a Record with non-string keys", () => { + const map = new Map([ + [1, "value1"], + [2, "value2"], + ]); + const result = Utils.mapToRecord(map); + expect(result).toEqual({ 1: "value1", 2: "value2" }); + expect(Utils.recordToMap(result)).toEqual(map); + }); + }); + + describe("recordToMap", () => { + it("should handle null", () => { + expect(Utils.recordToMap(null)).toEqual(null); + }); + + it("should handle empty record", () => { + expect(Utils.recordToMap({})).toEqual(new Map()); + }); + + it("should handle convert a Record to a Map", () => { + const record = { key1: "value1", key2: "value2" }; + expect(Utils.recordToMap(record)).toEqual(new Map(Object.entries(record))); + }); + + it("should handle convert a Record to a Map with non-string keys", () => { + const record = { 1: "value1", 2: "value2" }; + const result = Utils.recordToMap(record); + expect(result).toEqual( + new Map([ + [1, "value1"], + [2, "value2"], + ]) + ); + expect(Utils.mapToRecord(result)).toEqual(record); + }); + }); }); diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index ff21a919f1f..c62d64d2f7d 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -423,6 +423,40 @@ export class Utils { return this.global.bitwardenContainerService; } + /** + * Converts map to a Record with the same data. Inverse of recordToMap + * Useful in toJSON methods, since Maps are not serializable + * @param map + * @returns + */ + static mapToRecord(map: Map): Record { + return map == null ? null : Object.fromEntries(map); + } + + /** + * Converts record to a Map with the same data. Inverse of mapToRecord + * Useful in fromJSON methods, since Maps are not serializable + * + * Warning: If the record has string keys that are numbers, they will be converted to numbers in the map + * @param record + * @returns + */ + static recordToMap(record: Record): Map { + if (record == null) { + return null; + } + const entries = Object.entries(record); + if (entries.length === 0) { + return new Map(); + } + + if (isNaN(Number(entries[0][0]))) { + return new Map(entries) as Map; + } else { + return new Map(entries.map((e) => [Number(e[0]), e[1]])) as Map; + } + } + private static isMobile(win: Window) { let mobile = false; ((a) => { diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 0c1c3b725ab..8391cc6e3e0 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -13,6 +13,7 @@ import { StorageLocation } from "../enums/storageLocation"; import { ThemeType } from "../enums/themeType"; import { UriMatchType } from "../enums/uriMatchType"; import { StateFactory } from "../factories/stateFactory"; +import { Utils } from "../misc/utils"; import { CipherData } from "../models/data/cipher.data"; import { CollectionData } from "../models/data/collection.data"; import { EncryptedOrganizationKeyData } from "../models/data/encrypted-organization-key.data"; @@ -65,13 +66,13 @@ export class StateService< TAccount extends Account = Account > implements StateServiceAbstraction { - private accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); + protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); accounts$ = this.accountsSubject.asObservable(); - private activeAccountSubject = new BehaviorSubject(null); + protected activeAccountSubject = new BehaviorSubject(null); activeAccount$ = this.activeAccountSubject.asObservable(); - private activeAccountUnlockedSubject = new BehaviorSubject(false); + protected activeAccountUnlockedSubject = new BehaviorSubject(false); activeAccountUnlocked$ = this.activeAccountUnlockedSubject.asObservable(); private hasBeenInited = false; @@ -676,7 +677,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - return this.recordToMap(account?.keys?.organizationKeys?.decrypted); + return Utils.recordToMap(account?.keys?.organizationKeys?.decrypted); } async setDecryptedOrganizationKeys( @@ -686,7 +687,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.organizationKeys.decrypted = this.mapToRecord(value); + account.keys.organizationKeys.decrypted = Utils.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -774,7 +775,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - return this.recordToMap(account?.keys?.providerKeys?.decrypted); + return Utils.recordToMap(account?.keys?.providerKeys?.decrypted); } async setDecryptedProviderKeys( @@ -784,7 +785,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.providerKeys.decrypted = this.mapToRecord(value); + account.keys.providerKeys.decrypted = Utils.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -2765,14 +2766,6 @@ export class StateService< await this.setState(updatedState); }); } - - private mapToRecord(map: Map): Record { - return map == null ? null : Object.fromEntries(map); - } - - private recordToMap(record: Record): Map { - return record == null ? null : new Map(Object.entries(record)); - } } export function withPrototype(