From 09d626bb4bcd91878403c8d85ea0b9b601f0dac4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 6 Dec 2023 13:07:27 -0500 Subject: [PATCH] [PM-4345] Ps/provide migration helpers for key definitions (#7050) * Provide helpers to build keys from KeyDefinitions * Move usage comments to the helper class * `npm run prettier` :robot: * Prefer setters and getters to key builders * Add documentation to migration helper methods * Fix migration helper tests * Prefer defined types to ad hoc * `npm run prettier` :robot: --- .../state-migrations/migration-helper.spec.ts | 75 +++++++++ .../src/state-migrations/migration-helper.ts | 142 ++++++++++++++++++ 2 files changed, 217 insertions(+) diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index a5a4b222be..55ab137551 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -18,6 +18,8 @@ const exampleJSON = { "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { otherStuff: "otherStuff2", }, + global_serviceName_key: "global_serviceName_key", + user_userId_serviceName_key: "user_userId_serviceName_key", }; describe("RemoveLegacyEtmKeyMigrator", () => { @@ -64,6 +66,79 @@ describe("RemoveLegacyEtmKeyMigrator", () => { expect(accounts).toEqual([]); }); }); + + describe("getFromGlobal", () => { + it("should return the correct value", async () => { + sut.currentVersion = 10; + const value = await sut.getFromGlobal({ + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("global_serviceName_key"); + }); + + it("should throw if the current version is less than 10", () => { + expect(() => + sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 10."); + }); + }); + + describe("setToGlobal", () => { + it("should set the correct value", async () => { + sut.currentVersion = 10; + await sut.setToGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }, "new_value"); + expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 10", () => { + expect(() => + sut.setToGlobal( + { stateDefinition: { name: "serviceName" }, key: "key" }, + "global_serviceName_key", + ), + ).toThrowError("No key builder should be used for versions prior to 10."); + }); + }); + + describe("getFromUser", () => { + it("should return the correct value", async () => { + sut.currentVersion = 10; + const value = await sut.getFromUser("userId", { + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("user_userId_serviceName_key"); + }); + + it("should throw if the current version is less than 10", () => { + expect(() => + sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 10."); + }); + }); + + describe("setToUser", () => { + it("should set the correct value", async () => { + sut.currentVersion = 10; + await sut.setToUser( + "userId", + { stateDefinition: { name: "serviceName" }, key: "key" }, + "new_value", + ); + expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 10", () => { + expect(() => + sut.setToUser( + "userId", + { stateDefinition: { name: "serviceName" }, key: "key" }, + "new_value", + ), + ).toThrowError("No key builder should be used for versions prior to 10."); + }); + }); }); /** Helper to create well-mocked migration helpers in migration tests */ diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 08ee706ed3..3138e853b2 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -3,6 +3,12 @@ import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations import { AbstractStorageService } from "../platform/abstractions/storage.service"; +export type StateDefinitionLike = { name: string }; +export type KeyDefinitionLike = { + stateDefinition: StateDefinitionLike; + key: string; +}; + export class MigrationHelper { constructor( public currentVersion: number, @@ -10,19 +16,93 @@ export class MigrationHelper { public logService: LogService, ) {} + /** + * Gets a value from the storage service at the given key. + * + * This is a brute force method to just get a value from the storage service. If you can use {@link getFromGlobal} or {@link getFromUser}, you should. + * @param key location + * @returns the value at the location + */ get(key: string): Promise { return this.storageService.get(key); } + /** + * Sets a value in the storage service at the given key. + * + * This is a brute force method to just set a value in the storage service. If you can use {@link setToGlobal} or {@link setToUser}, you should. + * @param key location + * @param value the value to set + * @returns + */ set(key: string, value: T): Promise { this.logService.info(`Setting ${key}`); return this.storageService.save(key, value); } + /** + * Gets a globally scoped value from a location derived through the key definition + * + * This is for use with the state providers framework, DO NOT use for values stored with {@link StateService}, + * use {@link get} for those. + * @param keyDefinition unique key definition + * @returns value from store + */ + getFromGlobal(keyDefinition: KeyDefinitionLike): Promise { + return this.get(this.getGlobalKey(keyDefinition)); + } + + /** + * Sets a globally scoped value to a location derived through the key definition + * + * This is for use with the state providers framework, DO NOT use for values stored with {@link StateService}, + * use {@link set} for those. + * @param keyDefinition unique key definition + * @param value value to store + * @returns void + */ + setToGlobal(keyDefinition: KeyDefinitionLike, value: T): Promise { + return this.set(this.getGlobalKey(keyDefinition), value); + } + + /** + * Gets a user scoped value from a location derived through the user id and key definition + * + * This is for use with the state providers framework, DO NOT use for values stored with {@link StateService}, + * use {@link get} for those. + * @param userId userId to use in the key + * @param keyDefinition unique key definition + * @returns value from store + */ + getFromUser(userId: string, keyDefinition: KeyDefinitionLike): Promise { + return this.get(this.getUserKey(userId, keyDefinition)); + } + + /** + * Sets a user scoped value to a location derived through the user id and key definition + * + * This is for use with the state providers framework, DO NOT use for values stored with {@link StateService}, + * use {@link set} for those. + * @param userId userId to use in the key + * @param keyDefinition unique key definition + * @param value value to store + * @returns void + */ + setToUser(userId: string, keyDefinition: KeyDefinitionLike, value: T): Promise { + return this.set(this.getUserKey(userId, keyDefinition), value); + } + info(message: string): void { this.logService.info(message); } + /** + * Helper method to read all Account objects stored by the State Service. + * + * This is useful from creating migrations off of this paradigm, but should not be used once a value is migrated to a state provider. + * + * @returns a list of all accounts that have been authenticated with state service, cast the the expected type. + */ async getAccounts(): Promise< { userId: string; account: ExpectedAccountType }[] > { @@ -34,4 +114,66 @@ export class MigrationHelper { })), ); } + + /** + * Builds a user storage key appropriate for the current version. + * + * @param userId userId to use in the key + * @param keyDefinition state and key to use in the key + * @returns + */ + private getUserKey(userId: string, keyDefinition: KeyDefinitionLike): string { + if (this.currentVersion < 10) { + return userKeyBuilderPre10(); + } else { + return userKeyBuilder(userId, keyDefinition); + } + } + + /** + * Builds a global storage key appropriate for the current version. + * + * @param keyDefinition state and key to use in the key + * @returns + */ + private getGlobalKey(keyDefinition: KeyDefinitionLike): string { + if (this.currentVersion < 10) { + return globalKeyBuilderPre10(); + } else { + return globalKeyBuilder(keyDefinition); + } + } +} + +/** + * When this is updated, rename this function to `userKeyBuilderXToY` where `X` is the version number it + * became relevant, and `Y` prior to the version it was updated. + * + * Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version. + * @param userId The userId of the user you want the key to be for. + * @param keyDefinition the key definition of which data the key should point to. + * @returns + */ +function userKeyBuilder(userId: string, keyDefinition: KeyDefinitionLike): string { + return `user_${userId}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; +} + +function userKeyBuilderPre10(): string { + throw Error("No key builder should be used for versions prior to 10."); +} + +/** + * When this is updated, rename this function to `globalKeyBuilderXToY` where `X` is the version number + * it became relevant, and `Y` prior to the version it was updated. + * + * Be sure to update the map in `MigrationHelper` to point to the appropriate function for the current version. + * @param keyDefinition the key definition of which data the key should point to. + * @returns + */ +function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string { + return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; +} + +function globalKeyBuilderPre10(): string { + throw Error("No key builder should be used for versions prior to 10."); }