From 94378326169e81e994b36f9b2b47249289c82fa9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 May 2020 00:11:29 +0200 Subject: [PATCH 01/17] feat(core-api/UserSettings): Ported across the UserSettings API from internal and convert to ts --- .../src/apis/definitions/UserSettingsApi.ts | 78 +++++++++++++++ .../core-api/src/apis/definitions/index.ts | 1 + .../implementations/UserSettingsApi/index.ts | 97 +++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 packages/core-api/src/apis/definitions/UserSettingsApi.ts create mode 100644 packages/core-api/src/apis/implementations/UserSettingsApi/index.ts diff --git a/packages/core-api/src/apis/definitions/UserSettingsApi.ts b/packages/core-api/src/apis/definitions/UserSettingsApi.ts new file mode 100644 index 0000000000..62045e0e51 --- /dev/null +++ b/packages/core-api/src/apis/definitions/UserSettingsApi.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '../ApiRef'; + +type setValue = (value: T | null) => void; +type removeValue = () => void; +export type UserSettingsApi = { + /** + * Get persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String?} key (Optional) Unique key associated with the data. + * If not key is specified,the whole store is returned. + * @param {String} key (Optional) Empty value to return if there is not data. + * @return {Object} data The data that should is stored. + */ + getFromStore( + storeName: string, + key: string, + defaultValue: T | null, + ): T | null; + /** + * Remove persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + */ + removeFromStore(storeName: string, key: string): void; + + /** + * Save persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @param {Object} data The data that should be stored. + */ + saveToStore(storeName: string, key: string, data: any): void; + + /** + * React hook that observes a single store value and provides functions for updating the value. + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @return {[value, setValue(value), removeValue()]} An array to be deconstructed, + * The first element is the value, the second is a function to call to update the value, + * and the third is a function to call to clear the value. + */ + useStoreValue( + storeName: string, + key: string, + ): [T | null, setValue, removeValue]; +}; + +export const userSettingsApiRef = createApiRef({ + id: 'core.user.settings', + description: + 'Provides the ability to modify settings that are personalised to the user', +}); diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 2e008965cd..0acef2abb4 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -27,3 +27,4 @@ export * from './AppThemeApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; +export * from './UserSettingsApi'; diff --git a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts new file mode 100644 index 0000000000..8d73202414 --- /dev/null +++ b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useEffect } from 'react'; +import { UserSettingsApi } from '../../definitions'; + +export class UserSettings implements UserSettingsApi { + getFromStore( + storeName: string, + key: string = '', + defaultValue: T | null, + ): T | null { + let store; + try { + store = JSON.parse(localStorage.getItem(storeName)!) || {}; + } catch (e) { + window.console.error( + `Error when parsing JSON config from storage for: ${storeName}`, + ); + return defaultValue as T; + } + + if (key) { + return (store[key] ?? defaultValue) as T; + } + return store as T; + } + + saveToStore(storeName: string, key: string, data: any): void { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + store[key] = data; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + removeFromStore(storeName: string, key: string): void { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + delete store[key]; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + useStoreValue( + storeName: string, + key: string, + ): [T | null, (value: T | null) => void, () => void] { + // We hardcode the emptyValue to null, since allowing objects or arrays would make + // the useEffect params a lot more complex and it would easy to get stuck in a loop. + const [value, setValue] = useState(() => + this.getFromStore(storeName, key, null), + ); + + // Listen to storage change events from other tabs + useEffect(() => { + const onChange = (event: StorageEvent) => { + if (event.key === storeName) { + // Avoid sending unnecessary updates when only the reference changes because of JSON.parse + setValue((currentValue: T | null) => { + const newValue = this.getFromStore(storeName, key, null); + + // Fastest way to do a deep equals, since we know references will differ, + // the values are serializable, and it's ok to signal a change if key order changes. + if (JSON.stringify(currentValue) === JSON.stringify(newValue)) { + return currentValue; + } + return newValue; + }); + } + }; + + window.addEventListener('storage', onChange); + return () => window.removeEventListener('storage', onChange); + }, [storeName, key]); + + const saveValue = (newValue: T | null) => { + this.saveToStore(storeName, key, newValue); + setValue(this.getFromStore(storeName, key, null)); + }; + + const removeValue = () => { + this.removeFromStore(storeName, key); + setValue(null); + }; + + return [value, saveValue, removeValue]; + } +} From 335d6f36b76175bea50d79ac5cd87f1af9a732cf Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 May 2020 13:11:06 +0200 Subject: [PATCH 02/17] feat(core-api/Storage): Reworking the StorageApi to have a better interface --- .../{UserSettingsApi.ts => StorageApi.ts} | 57 ++++++----- .../core-api/src/apis/definitions/index.ts | 2 +- .../implementations/StorageApi/WebStorage.ts | 51 ++++++++++ .../apis/implementations/StorageApi/index.ts | 17 ++++ .../implementations/UserSettingsApi/index.ts | 97 ------------------- 5 files changed, 104 insertions(+), 120 deletions(-) rename packages/core-api/src/apis/definitions/{UserSettingsApi.ts => StorageApi.ts} (59%) create mode 100644 packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts create mode 100644 packages/core-api/src/apis/implementations/StorageApi/index.ts delete mode 100644 packages/core-api/src/apis/implementations/UserSettingsApi/index.ts diff --git a/packages/core-api/src/apis/definitions/UserSettingsApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts similarity index 59% rename from packages/core-api/src/apis/definitions/UserSettingsApi.ts rename to packages/core-api/src/apis/definitions/StorageApi.ts index 62045e0e51..0cb5ccc14d 100644 --- a/packages/core-api/src/apis/definitions/UserSettingsApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -16,14 +16,22 @@ import { createApiRef } from '../ApiRef'; -type setValue = (value: T | null) => void; -type removeValue = () => void; -export type UserSettingsApi = { +type UnsubscribeFromStore = () => void; +type SubscribeToStoreHandler = ({ + storeName, + key, + oldValue, + newValue, +}: { + storeName: string; + key: string; + oldValue?: T; + newValue?: T; +}) => void; + +export type StorageApi = { /** * Get persistent data. - * - * TODO: Replace with something less volatile than LocalStorage - * * @param {String} storeName Name of the store. * @param {String?} key (Optional) Unique key associated with the data. * If not key is specified,the whole store is returned. @@ -33,45 +41,50 @@ export type UserSettingsApi = { getFromStore( storeName: string, key: string, - defaultValue: T | null, - ): T | null; + defaultValue?: T, + ): Promise; + /** * Remove persistent data. * - * TODO: Replace with something less volatile than LocalStorage - * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. */ - removeFromStore(storeName: string, key: string): void; + removeFromStore(storeName: string, key: string): Promise; /** - * Save persistent data. - * - * TODO: Replace with something less volatile than LocalStorage + * Save persiswtent data. * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. * @param {Object} data The data that should be stored. */ - saveToStore(storeName: string, key: string, data: any): void; + saveToStore(storeName: string, key: string, data: any): Promise; /** - * React hook that observes a single store value and provides functions for updating the value. + * Callback for Changes in the store + * @callback subscribeHandler + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @param {Object} oldValue The old value that was in the store. + * @param {Object} newValue The new value that has been set in the store. + */ + /** + * Subscribe to Key changes in a store and get the new and old value * * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. - * @return {[value, setValue(value), removeValue()]} An array to be deconstructed, - * The first element is the value, the second is a function to call to update the value, - * and the third is a function to call to clear the value. + * @param {subscribeHandler} handler Handler which is called with the old value and new value in the store. + * @returns {Function} Unsubscribe to changes in the store. */ - useStoreValue( + subscribeToChange( storeName: string, key: string, - ): [T | null, setValue, removeValue]; + handler: SubscribeToStoreHandler, + ): UnsubscribeFromStore; }; -export const userSettingsApiRef = createApiRef({ +export const storageApiRef = createApiRef({ id: 'core.user.settings', description: 'Provides the ability to modify settings that are personalised to the user', diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 0acef2abb4..475dba9189 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -27,4 +27,4 @@ export * from './AppThemeApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; -export * from './UserSettingsApi'; +export * from './StorageApi'; diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts new file mode 100644 index 0000000000..c48a1ee931 --- /dev/null +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { StorageApi } from '../../definitions'; + +export class WebStorage implements StorageApi { + async getFromStore( + storeName: string, + key: string = '', + defaultValue?: T, + ): Promise { + let store; + try { + store = JSON.parse(localStorage.getItem(storeName)!) || {}; + } catch (e) { + window.console.error( + `Error when parsing JSON config from storage for: ${storeName}`, + ); + return defaultValue; + } + + if (key) { + return store[key] ?? defaultValue; + } + return store; + } + + async saveToStore(storeName: string, key: string, data: any): Promise { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + store[key] = data; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + async removeFromStore(storeName: string, key: string): Promise { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + delete store[key]; + localStorage.setItem(storeName, JSON.stringify(store)); + } +} diff --git a/packages/core-api/src/apis/implementations/StorageApi/index.ts b/packages/core-api/src/apis/implementations/StorageApi/index.ts new file mode 100644 index 0000000000..33b0094551 --- /dev/null +++ b/packages/core-api/src/apis/implementations/StorageApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { WebStorage } from './WebStorage'; diff --git a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts deleted file mode 100644 index 8d73202414..0000000000 --- a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { useState, useEffect } from 'react'; -import { UserSettingsApi } from '../../definitions'; - -export class UserSettings implements UserSettingsApi { - getFromStore( - storeName: string, - key: string = '', - defaultValue: T | null, - ): T | null { - let store; - try { - store = JSON.parse(localStorage.getItem(storeName)!) || {}; - } catch (e) { - window.console.error( - `Error when parsing JSON config from storage for: ${storeName}`, - ); - return defaultValue as T; - } - - if (key) { - return (store[key] ?? defaultValue) as T; - } - return store as T; - } - - saveToStore(storeName: string, key: string, data: any): void { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - store[key] = data; - localStorage.setItem(storeName, JSON.stringify(store)); - } - - removeFromStore(storeName: string, key: string): void { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - delete store[key]; - localStorage.setItem(storeName, JSON.stringify(store)); - } - - useStoreValue( - storeName: string, - key: string, - ): [T | null, (value: T | null) => void, () => void] { - // We hardcode the emptyValue to null, since allowing objects or arrays would make - // the useEffect params a lot more complex and it would easy to get stuck in a loop. - const [value, setValue] = useState(() => - this.getFromStore(storeName, key, null), - ); - - // Listen to storage change events from other tabs - useEffect(() => { - const onChange = (event: StorageEvent) => { - if (event.key === storeName) { - // Avoid sending unnecessary updates when only the reference changes because of JSON.parse - setValue((currentValue: T | null) => { - const newValue = this.getFromStore(storeName, key, null); - - // Fastest way to do a deep equals, since we know references will differ, - // the values are serializable, and it's ok to signal a change if key order changes. - if (JSON.stringify(currentValue) === JSON.stringify(newValue)) { - return currentValue; - } - return newValue; - }); - } - }; - - window.addEventListener('storage', onChange); - return () => window.removeEventListener('storage', onChange); - }, [storeName, key]); - - const saveValue = (newValue: T | null) => { - this.saveToStore(storeName, key, newValue); - setValue(this.getFromStore(storeName, key, null)); - }; - - const removeValue = () => { - this.removeFromStore(storeName, key); - setValue(null); - }; - - return [value, saveValue, removeValue]; - } -} From 4fd99fec251c86bd826744fc39bb7d1bc2583072 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 2 Jun 2020 17:05:49 +0200 Subject: [PATCH 03/17] feat(core-api/Storage): reworking the api for storage. get's should be sync --- .../src/apis/definitions/StorageApi.ts | 61 ++++--------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 0cb5ccc14d..62a8fd6895 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -15,77 +15,40 @@ */ import { createApiRef } from '../ApiRef'; - -type UnsubscribeFromStore = () => void; -type SubscribeToStoreHandler = ({ - storeName, - key, - oldValue, - newValue, -}: { - storeName: string; - key: string; - oldValue?: T; - newValue?: T; -}) => void; +import { Observable } from '@backstage/core-api'; export type StorageApi = { /** * Get persistent data. - * @param {String} storeName Name of the store. - * @param {String?} key (Optional) Unique key associated with the data. - * If not key is specified,the whole store is returned. - * @param {String} key (Optional) Empty value to return if there is not data. + * + * @param {String} key Unique key associated with the data. * @return {Object} data The data that should is stored. */ - getFromStore( - storeName: string, - key: string, - defaultValue?: T, - ): Promise; + get(key: string): T | undefined; /** * Remove persistent data. * - * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. */ - removeFromStore(storeName: string, key: string): Promise; + remove(key: string): Promise; /** - * Save persiswtent data. + * Save persistant data. * - * @param {String} storeName Name of the store. * @param {String} key Unique key associated with the data. - * @param {Object} data The data that should be stored. + * @return */ - saveToStore(storeName: string, key: string, data: any): Promise; + save(key: string, data: any): Promise; /** - * Callback for Changes in the store - * @callback subscribeHandler - * @param {String} storeName Name of the store. - * @param {String} key Unique key associated with the data. - * @param {Object} oldValue The old value that was in the store. - * @param {Object} newValue The new value that has been set in the store. - */ - /** - * Subscribe to Key changes in a store and get the new and old value * - * @param {String} storeName Name of the store. - * @param {String} key Unique key associated with the data. - * @param {subscribeHandler} handler Handler which is called with the old value and new value in the store. - * @returns {Function} Unsubscribe to changes in the store. + * @param {String} key Unique key associated with the data */ - subscribeToChange( - storeName: string, - key: string, - handler: SubscribeToStoreHandler, - ): UnsubscribeFromStore; + observe$(key: string): Observable; }; export const storageApiRef = createApiRef({ - id: 'core.user.settings', - description: - 'Provides the ability to modify settings that are personalised to the user', + id: 'core.storage', + description: 'Provides the ability to store data which is unique to the user', }); From 99a2387076fa845afd5a1201c78e0abe6d610d9f Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 2 Jun 2020 23:12:32 +0200 Subject: [PATCH 04/17] feat(core-api/Storage): Add the simple storage API for now with some simpler tests --- .../src/apis/definitions/StorageApi.ts | 8 +++- .../StorageApi/WebStorage.test.ts | 48 +++++++++++++++++++ .../implementations/StorageApi/WebStorage.ts | 42 ++++++++-------- 3 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 62a8fd6895..c8e72a518f 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -17,6 +17,11 @@ import { createApiRef } from '../ApiRef'; import { Observable } from '@backstage/core-api'; +export type ObservableMessage = { + key: string; + newValue?: T; +}; + export type StorageApi = { /** * Get persistent data. @@ -37,9 +42,8 @@ export type StorageApi = { * Save persistant data. * * @param {String} key Unique key associated with the data. - * @return */ - save(key: string, data: any): Promise; + set(key: string, data: any): Promise; /** * diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts new file mode 100644 index 0000000000..6509ba6edb --- /dev/null +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { WebStorage } from './WebStorage'; +describe('WebStorage Storage API', () => { + it('should return undefined for values which are unset', async () => { + const storage = new WebStorage(); + + expect(storage.get('myfakekey')).toBeUndefined(); + }); + + it('should allow the setting and getting of the simple data structures', async () => { + const storage = new WebStorage(); + + await storage.set('myfakekey', 'helloimastring'); + await storage.set('mysecondfakekey', 1234); + await storage.set('mythirdfakekey', true); + + expect(storage.get('myfakekey')).toBe('helloimastring'); + expect(storage.get('mysecondfakekey')).toBe(1234); + expect(storage.get('mythirdfakekey')).toBe(true); + }); + + it('should allow setting of complex datastructures', async () => { + const storage = new WebStorage(); + + const mockData = { + something: 'here', + is: [{ super: { complex: [{ but: 'something', why: true }] } }], + }; + + await storage.set('myfakekey', mockData); + + expect(storage.get('myfakekey')).toEqual(mockData); + }); +}); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index c48a1ee931..cbcab6349e 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -13,39 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { StorageApi } from '../../definitions'; +import { StorageApi, ObservableMessage } from '../../definitions'; +import { Observable } from '../../../types'; +import ObservableImpl from 'zen-observable'; export class WebStorage implements StorageApi { - async getFromStore( - storeName: string, - key: string = '', - defaultValue?: T, - ): Promise { - let store; + private readonly observable = new ObservableImpl(() => {}); + get(key: string): T | undefined { try { - store = JSON.parse(localStorage.getItem(storeName)!) || {}; + const storage = JSON.parse(localStorage.getItem(key)!); + return storage ?? undefined; } catch (e) { window.console.error( - `Error when parsing JSON config from storage for: ${storeName}`, + `Error when parsing JSON config from storage for: ${key}`, + e, ); - return defaultValue; } - if (key) { - return store[key] ?? defaultValue; - } - return store; + return undefined; } - async saveToStore(storeName: string, key: string, data: any): Promise { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - store[key] = data; - localStorage.setItem(storeName, JSON.stringify(store)); + async set(key: string, data: T): Promise { + localStorage.setItem(key, JSON.stringify(data, null, 2)); } - async removeFromStore(storeName: string, key: string): Promise { - const store = JSON.parse(localStorage.getItem(storeName)!) || {}; - delete store[key]; - localStorage.setItem(storeName, JSON.stringify(store)); + async remove(key: string): Promise { + localStorage.removeItem(key); + } + + observe$(key: string): Observable { + return this.observable.filter( + ({ key: messageKey }) => messageKey === key, + ) as Observable; } } From 38256679239b875ff88ce824fc70291730bd17d5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 14:41:47 +0200 Subject: [PATCH 05/17] feat(core-api/Storage): Added a simple implementatation of subscriptions --- .../StorageApi/WebStorage.test.ts | 59 ++++++++++++++++++- .../implementations/StorageApi/WebStorage.ts | 22 ++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index 6509ba6edb..85343d58ff 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -27,7 +27,6 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); expect(storage.get('mysecondfakekey')).toBe(1234); expect(storage.get('mythirdfakekey')).toBe(true); @@ -45,4 +44,62 @@ describe('WebStorage Storage API', () => { expect(storage.get('myfakekey')).toEqual(mockData); }); + + it('should subscribe to key changes when setting a new value', async () => { + const storage = new WebStorage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.set('correctKey', mockData); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + newValue: mockData, + }); + }); + + it('should subscribe to key changes when deleting a value', async () => { + const storage = new WebStorage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + storage.set('correctKey', mockData); + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.remove('correctKey'); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + newValue: undefined, + }); + }); }); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index cbcab6349e..432c08e3d1 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -18,7 +18,19 @@ import { Observable } from '../../../types'; import ObservableImpl from 'zen-observable'; export class WebStorage implements StorageApi { - private readonly observable = new ObservableImpl(() => {}); + subscribers: Set< + ZenObservable.SubscriptionObserver + > = new Set(); + + private readonly observable = new ObservableImpl( + subscriber => { + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }, + ); + get(key: string): T | undefined { try { const storage = JSON.parse(localStorage.getItem(key)!); @@ -35,10 +47,18 @@ export class WebStorage implements StorageApi { async set(key: string, data: T): Promise { localStorage.setItem(key, JSON.stringify(data, null, 2)); + this.notifyChanges({ key, newValue: data }); } async remove(key: string): Promise { localStorage.removeItem(key); + this.notifyChanges({ key, newValue: undefined }); + } + + notifyChanges(message: ObservableMessage) { + for (const subscription of this.subscribers) { + subscription.next(message); + } } observe$(key: string): Observable { From f1ba79fec03fd209d1734e49cfbacac97537c8f7 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 14:51:53 +0200 Subject: [PATCH 06/17] chore(core-api/Storage): Fixing order of the functions --- .../src/apis/definitions/StorageApi.ts | 2 +- .../implementations/StorageApi/WebStorage.ts | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index c8e72a518f..e0bd2c7ed3 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -15,7 +15,7 @@ */ import { createApiRef } from '../ApiRef'; -import { Observable } from '@backstage/core-api'; +import { Observable } from '../../types'; export type ObservableMessage = { key: string; diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index 432c08e3d1..a6a646d3dc 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -22,15 +22,6 @@ export class WebStorage implements StorageApi { ZenObservable.SubscriptionObserver > = new Set(); - private readonly observable = new ObservableImpl( - subscriber => { - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }, - ); - get(key: string): T | undefined { try { const storage = JSON.parse(localStorage.getItem(key)!); @@ -55,15 +46,24 @@ export class WebStorage implements StorageApi { this.notifyChanges({ key, newValue: undefined }); } - notifyChanges(message: ObservableMessage) { - for (const subscription of this.subscribers) { - subscription.next(message); - } - } - observe$(key: string): Observable { return this.observable.filter( ({ key: messageKey }) => messageKey === key, ) as Observable; } + + private notifyChanges(message: ObservableMessage) { + for (const subscription of this.subscribers) { + subscription.next(message); + } + } + + private readonly observable = new ObservableImpl( + subscriber => { + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }, + ); } From fb32b09226bc9bbfb0bc40f42be703b6c6126851 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 17:21:15 +0200 Subject: [PATCH 07/17] feat(core-api/Storage): Be able to namespace with forBucket function --- .../src/apis/definitions/StorageApi.ts | 11 ++++++++-- .../StorageApi/WebStorage.test.ts | 15 +++++++++++++ .../implementations/StorageApi/WebStorage.ts | 22 ++++++++++++++----- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index e0bd2c7ed3..90c1961588 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -22,7 +22,14 @@ export type ObservableMessage = { newValue?: T; }; -export type StorageApi = { +export interface StorageApi { + /** + * The names + * @param {String} name Namespace for the storage to be stored under, + * will inherit previous namespaces too + */ + forBucket(name: string): StorageApi; + /** * Get persistent data. * @@ -50,7 +57,7 @@ export type StorageApi = { * @param {String} key Unique key associated with the data */ observe$(key: string): Observable; -}; +} export const storageApiRef = createApiRef({ id: 'core.storage', diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index 85343d58ff..f28cb9e41b 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -102,4 +102,19 @@ describe('WebStorage Storage API', () => { newValue: undefined, }); }); + + it('should be able to create different buckets for different uses', async () => { + const rootStorage = new WebStorage(); + + const firstStorage = rootStorage.forBucket('userSettings'); + const secondStorage = rootStorage.forBucket('profileSettings'); + const keyName = 'blobby'; + + await firstStorage.set(keyName, 'boop'); + await secondStorage.set(keyName, 'deerp'); + + expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); + expect(firstStorage.get(keyName)).toBe('boop'); + expect(secondStorage.get(keyName)).toBe('deerp'); + }); }); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index a6a646d3dc..a7150b3647 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -18,13 +18,11 @@ import { Observable } from '../../../types'; import ObservableImpl from 'zen-observable'; export class WebStorage implements StorageApi { - subscribers: Set< - ZenObservable.SubscriptionObserver - > = new Set(); + constructor(private readonly namespace: string = '') {} get(key: string): T | undefined { try { - const storage = JSON.parse(localStorage.getItem(key)!); + const storage = JSON.parse(localStorage.getItem(this.getKeyName(key))!); return storage ?? undefined; } catch (e) { window.console.error( @@ -36,13 +34,17 @@ export class WebStorage implements StorageApi { return undefined; } + forBucket(name: string): WebStorage { + return new WebStorage(`${this.namespace}/${name}`); + } + async set(key: string, data: T): Promise { - localStorage.setItem(key, JSON.stringify(data, null, 2)); + localStorage.setItem(this.getKeyName(key), JSON.stringify(data, null, 2)); this.notifyChanges({ key, newValue: data }); } async remove(key: string): Promise { - localStorage.removeItem(key); + localStorage.removeItem(this.getKeyName(key)); this.notifyChanges({ key, newValue: undefined }); } @@ -52,12 +54,20 @@ export class WebStorage implements StorageApi { ) as Observable; } + private getKeyName(key: string) { + return `${this.namespace}/${key}`; + } + private notifyChanges(message: ObservableMessage) { for (const subscription of this.subscribers) { subscription.next(message); } } + private subscribers: Set< + ZenObservable.SubscriptionObserver + > = new Set(); + private readonly observable = new ObservableImpl( subscriber => { this.subscribers.add(subscriber); From f32e987d1546b1409dc28bd09f8ed0e2f3327253 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 17:59:26 +0200 Subject: [PATCH 08/17] chore(core-api/Storage): Fixing some code review comments, so we don't have to cast some stuff in Typescript --- packages/core-api/src/apis/definitions/StorageApi.ts | 12 ++++++------ .../apis/implementations/StorageApi/WebStorage.ts | 8 +++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 90c1961588..7d7533f5b4 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -17,21 +17,21 @@ import { createApiRef } from '../ApiRef'; import { Observable } from '../../types'; -export type ObservableMessage = { +export type ObservableMessage = { key: string; newValue?: T; }; export interface StorageApi { /** - * The names + * Create a bucket to store data in. * @param {String} name Namespace for the storage to be stored under, * will inherit previous namespaces too */ forBucket(name: string): StorageApi; /** - * Get persistent data. + * Get the current value for persistent data, use observe$ to be notified of updates. * * @param {String} key Unique key associated with the data. * @return {Object} data The data that should is stored. @@ -46,17 +46,17 @@ export interface StorageApi { remove(key: string): Promise; /** - * Save persistant data. + * Save persistant data, and emit messages to anyone that is using observe$ for this key * * @param {String} key Unique key associated with the data. */ set(key: string, data: any): Promise; /** - * + * Observe changes on a particular key in the bucket * @param {String} key Unique key associated with the data */ - observe$(key: string): Observable; + observe$(key: string): Observable>; } export const storageApiRef = createApiRef({ diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index a7150b3647..c9665253de 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -48,17 +48,15 @@ export class WebStorage implements StorageApi { this.notifyChanges({ key, newValue: undefined }); } - observe$(key: string): Observable { - return this.observable.filter( - ({ key: messageKey }) => messageKey === key, - ) as Observable; + observe$(key: string): Observable> { + return this.observable.filter(({ key: messageKey }) => messageKey === key); } private getKeyName(key: string) { return `${this.namespace}/${key}`; } - private notifyChanges(message: ObservableMessage) { + private notifyChanges(message: ObservableMessage) { for (const subscription of this.subscribers) { subscription.next(message); } From 42d58955c25d88d34ba72951b100a73dabba31d8 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 19:49:59 +0200 Subject: [PATCH 09/17] feat(core-api/Storage): Encode the key names to avoid clashes --- .../implementations/StorageApi/WebStorage.test.ts | 13 +++++++++++++ .../apis/implementations/StorageApi/WebStorage.ts | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index f28cb9e41b..de6d55c7a4 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -117,4 +117,17 @@ describe('WebStorage Storage API', () => { expect(firstStorage.get(keyName)).toBe('boop'); expect(secondStorage.get(keyName)).toBe('deerp'); }); + + it('should not clash with other namesapces when creating buckets', async () => { + const rootStorage = new WebStorage(); + + // when getting key test2 it will translate to /profile/something/deep/test2 + const firstStorage = rootStorage.forBucket('profile/something/deep'); + // when getting key deep/test2 it will translate to /profile/something/deep/test2 + const secondStorage = rootStorage.forBucket('profile/something'); + + await firstStorage.set('test2', { error: true }); + + expect(secondStorage.get('deep/test2')).toBe(undefined); + }); }); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index c9665253de..bd7c144421 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -53,7 +53,7 @@ export class WebStorage implements StorageApi { } private getKeyName(key: string) { - return `${this.namespace}/${key}`; + return `${this.namespace}/${encodeURIComponent(key)}`; } private notifyChanges(message: ObservableMessage) { @@ -62,9 +62,9 @@ export class WebStorage implements StorageApi { } } - private subscribers: Set< + private subscribers = new Set< ZenObservable.SubscriptionObserver - > = new Set(); + >(); private readonly observable = new ObservableImpl( subscriber => { From e37ad442e2e4301b15be6330a4b2936f72716143 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 20:23:41 +0200 Subject: [PATCH 10/17] chore(core-api/Storage): fixing some more code review comments. and encoding the namespace better --- .../core-api/src/apis/definitions/StorageApi.ts | 4 ++-- .../StorageApi/WebStorage.test.ts | 5 ++++- .../implementations/StorageApi/WebStorage.ts | 16 ++++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index 7d7533f5b4..a84807d314 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -17,7 +17,7 @@ import { createApiRef } from '../ApiRef'; import { Observable } from '../../types'; -export type ObservableMessage = { +export type StorageValueChange = { key: string; newValue?: T; }; @@ -56,7 +56,7 @@ export interface StorageApi { * Observe changes on a particular key in the bucket * @param {String} key Unique key associated with the data */ - observe$(key: string): Observable>; + observe$(key: string): Observable>; } export const storageApiRef = createApiRef({ diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index de6d55c7a4..e68981d157 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -122,7 +122,10 @@ describe('WebStorage Storage API', () => { const rootStorage = new WebStorage(); // when getting key test2 it will translate to /profile/something/deep/test2 - const firstStorage = rootStorage.forBucket('profile/something/deep'); + const firstStorage = rootStorage + .forBucket('profile') + .forBucket('something') + .forBucket('deep'); // when getting key deep/test2 it will translate to /profile/something/deep/test2 const secondStorage = rootStorage.forBucket('profile/something'); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index bd7c144421..bb9eb9d7ce 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { StorageApi, ObservableMessage } from '../../definitions'; +import { StorageApi, StorageValueChange } from '../../definitions'; import { Observable } from '../../../types'; import ObservableImpl from 'zen-observable'; export class WebStorage implements StorageApi { - constructor(private readonly namespace: string = '') {} + private readonly namespace: string; + + constructor(namespace: string = '') { + this.namespace = namespace ? encodeURIComponent(namespace) : namespace; + } get(key: string): T | undefined { try { @@ -48,7 +52,7 @@ export class WebStorage implements StorageApi { this.notifyChanges({ key, newValue: undefined }); } - observe$(key: string): Observable> { + observe$(key: string): Observable> { return this.observable.filter(({ key: messageKey }) => messageKey === key); } @@ -56,17 +60,17 @@ export class WebStorage implements StorageApi { return `${this.namespace}/${encodeURIComponent(key)}`; } - private notifyChanges(message: ObservableMessage) { + private notifyChanges(message: StorageValueChange) { for (const subscription of this.subscribers) { subscription.next(message); } } private subscribers = new Set< - ZenObservable.SubscriptionObserver + ZenObservable.SubscriptionObserver >(); - private readonly observable = new ObservableImpl( + private readonly observable = new ObservableImpl( subscriber => { this.subscribers.add(subscriber); return () => { From 0e5d0ea7e16c00c15a147664ca8e6be22dec02c0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Jun 2020 21:25:51 +0200 Subject: [PATCH 11/17] feat(core-api/Storage): Made the implementation a litle nicer now so that the error API is passed through to the storage API --- packages/app/src/apis.ts | 8 +++- .../src/apis/definitions/StorageApi.ts | 6 +++ .../StorageApi/WebStorage.test.ts | 44 +++++++++++++++---- .../implementations/StorageApi/WebStorage.ts | 23 ++++++---- .../src/apis/implementations/index.ts | 1 + 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 6a23de1edd..ec0088e219 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -30,6 +30,8 @@ import { OAuthRequestManager, googleAuthApiRef, githubAuthApiRef, + storageApiRef, + WebStorage, } from '@backstage/core'; import { @@ -45,8 +47,12 @@ import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; const builder = ApiRegistry.builder(); const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); +const errorApi = builder.add( + errorApiRef, + new ErrorAlerter(alertApi, new ErrorApiForwarder()), +); -builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); +builder.add(storageApiRef, WebStorage.create({ errorApi })); builder.add(circleCIApiRef, new CircleCIApi()); builder.add(featureFlagsApiRef, new FeatureFlags()); diff --git a/packages/core-api/src/apis/definitions/StorageApi.ts b/packages/core-api/src/apis/definitions/StorageApi.ts index a84807d314..920ee56811 100644 --- a/packages/core-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-api/src/apis/definitions/StorageApi.ts @@ -16,12 +16,18 @@ import { createApiRef } from '../ApiRef'; import { Observable } from '../../types'; +import { ErrorApi } from './ErrorApi'; export type StorageValueChange = { key: string; newValue?: T; }; +export type CreateStorageApiOptions = { + errorApi: ErrorApi; + namespace?: string; +}; + export interface StorageApi { /** * Create a bucket to store data in. diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts index e68981d157..a96813347e 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -14,15 +14,25 @@ * limitations under the License. */ import { WebStorage } from './WebStorage'; +import { CreateStorageApiOptions, StorageApi } from '../../definitions'; describe('WebStorage Storage API', () => { + const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; + const createWebStorage = ( + args?: Partial, + ): StorageApi => { + return WebStorage.create({ + errorApi: mockErrorApi, + ...args, + }); + }; it('should return undefined for values which are unset', async () => { - const storage = new WebStorage(); + const storage = createWebStorage(); expect(storage.get('myfakekey')).toBeUndefined(); }); it('should allow the setting and getting of the simple data structures', async () => { - const storage = new WebStorage(); + const storage = createWebStorage(); await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); @@ -33,7 +43,7 @@ describe('WebStorage Storage API', () => { }); it('should allow setting of complex datastructures', async () => { - const storage = new WebStorage(); + const storage = createWebStorage(); const mockData = { something: 'here', @@ -46,14 +56,14 @@ describe('WebStorage Storage API', () => { }); it('should subscribe to key changes when setting a new value', async () => { - const storage = new WebStorage(); + const storage = createWebStorage(); const wrongKeyNextHandler = jest.fn(); const selectedKeyNextHandler = jest.fn(); const mockData = { hello: 'im a great new value' }; await new Promise(resolve => { - storage.observe$('correctKey').subscribe({ + storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); resolve(); @@ -74,7 +84,7 @@ describe('WebStorage Storage API', () => { }); it('should subscribe to key changes when deleting a value', async () => { - const storage = new WebStorage(); + const storage = createWebStorage(); const wrongKeyNextHandler = jest.fn(); const selectedKeyNextHandler = jest.fn(); @@ -104,7 +114,7 @@ describe('WebStorage Storage API', () => { }); it('should be able to create different buckets for different uses', async () => { - const rootStorage = new WebStorage(); + const rootStorage = createWebStorage(); const firstStorage = rootStorage.forBucket('userSettings'); const secondStorage = rootStorage.forBucket('profileSettings'); @@ -119,7 +129,7 @@ describe('WebStorage Storage API', () => { }); it('should not clash with other namesapces when creating buckets', async () => { - const rootStorage = new WebStorage(); + const rootStorage = createWebStorage(); // when getting key test2 it will translate to /profile/something/deep/test2 const firstStorage = rootStorage @@ -133,4 +143,22 @@ describe('WebStorage Storage API', () => { expect(secondStorage.get('deep/test2')).toBe(undefined); }); + + it('should call the error api when the json can not be parsed in local storage', async () => { + const rootStorage = createWebStorage({ + namespace: '/Test/Mock/Thing', + }); + + localStorage.setItem('/Test/Mock/Thing/key', '{smd: asdouindA}'); + + const value = rootStorage.get('key'); + + expect(value).toBe(undefined); + expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); + expect(mockErrorApi.post).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Error when parsing JSON config from storage for: key', + }), + ); + }); }); diff --git a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts index bb9eb9d7ce..8af8f760cf 100644 --- a/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -13,15 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { StorageApi, StorageValueChange } from '../../definitions'; +import { + StorageApi, + StorageValueChange, + ErrorApi, + CreateStorageApiOptions, +} from '../../definitions'; import { Observable } from '../../../types'; import ObservableImpl from 'zen-observable'; export class WebStorage implements StorageApi { - private readonly namespace: string; + constructor( + private readonly namespace: string, + private readonly errorApi: ErrorApi, + ) {} - constructor(namespace: string = '') { - this.namespace = namespace ? encodeURIComponent(namespace) : namespace; + static create(options: CreateStorageApiOptions): WebStorage { + return new WebStorage(options.namespace ?? '', options.errorApi); } get(key: string): T | undefined { @@ -29,9 +37,8 @@ export class WebStorage implements StorageApi { const storage = JSON.parse(localStorage.getItem(this.getKeyName(key))!); return storage ?? undefined; } catch (e) { - window.console.error( - `Error when parsing JSON config from storage for: ${key}`, - e, + this.errorApi.post( + new Error(`Error when parsing JSON config from storage for: ${key}`), ); } @@ -39,7 +46,7 @@ export class WebStorage implements StorageApi { } forBucket(name: string): WebStorage { - return new WebStorage(`${this.namespace}/${name}`); + return new WebStorage(`${this.namespace}/${name}`, this.errorApi); } async set(key: string, data: T): Promise { diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index b5cc250ae4..e6d23fee21 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -25,3 +25,4 @@ export * from './AppThemeApi'; export * from './ConfigApi'; export * from './ErrorApi'; export * from './OAuthRequestApi'; +export * from './StorageApi'; From 74c492727f707d63d8784e226da217fcd40e719f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:53:58 +0200 Subject: [PATCH 12/17] build(deps-dev): bump @types/lodash from 4.14.151 to 4.14.155 (#1132) Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.151 to 4.14.155. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 40e01e639c..eef64e948b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3712,9 +3712,9 @@ integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== "@types/lodash@^4.14.151": - version "4.14.151" - resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.151.tgz#7d58cac32bedb0ec37cb7f99094a167d6176c9d5" - integrity sha512-Zst90IcBX5wnwSu7CAS0vvJkTjTELY4ssKbHiTnGcJgi170uiS8yQDdc3v6S77bRqYQIN1App5a1Pc2lceE5/g== + version "4.14.155" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" + integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== "@types/mime@*": version "2.0.1" From 6bf094d74253fdc837d93de325af7f78ea704672 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:54:21 +0200 Subject: [PATCH 13/17] build(deps-dev): bump @types/webpack from 4.41.13 to 4.41.17 (#1130) Bumps [@types/webpack](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/webpack) from 4.41.13 to 4.41.17. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/webpack) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index eef64e948b..46b65cb06c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4151,9 +4151,9 @@ source-map "^0.6.1" "@types/webpack@*", "@types/webpack@^4.41.7", "@types/webpack@^4.41.8": - version "4.41.13" - resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.13.tgz#988d114c8913d039b8a0e0502a7fe4f1f84f3d5e" - integrity sha512-RYmIHOWSxnTTa765N6jJBVE45pd2SYNblEYshVDduLw6RhocazNmRzE5/ytvBD8IkDMH6DI+bcrqxh8NILimBA== + version "4.41.17" + resolved "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz#0a69005e644d657c85b7d6ec1c826a71bebd1c93" + integrity sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw== dependencies: "@types/anymatch" "*" "@types/node" "*" From dd2f6f82eecaadd1301fc2389c2a1137a5653809 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:54:43 +0200 Subject: [PATCH 14/17] build(deps): bump fork-ts-checker-webpack-plugin from 4.1.5 to 4.1.6 (#1131) Bumps [fork-ts-checker-webpack-plugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) from 4.1.5 to 4.1.6. - [Release notes](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/releases) - [Changelog](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/compare/v4.1.5...v4.1.6) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 46b65cb06c..c5529888e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9048,9 +9048,9 @@ fork-ts-checker-webpack-plugin@3.1.1: worker-rpc "^0.1.0" fork-ts-checker-webpack-plugin@^4.0.5: - version "4.1.5" - resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.5.tgz#780d52c65183742d8c885fff42a9ec9ea7006672" - integrity sha512-nuD4IDqoOfkEIlS6shhjLGaLBDSNyVJulAlr5lFbPe0saGqlsTo+/HmhtIrs/cNLFtmaudL10byivhxr+Qhh4w== + version "4.1.6" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" + integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== dependencies: "@babel/code-frame" "^7.5.5" chalk "^2.4.1" From d8e20e2461bd22479de45a5dad03456dc219281e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:55:06 +0200 Subject: [PATCH 15/17] build(deps): bump globby from 11.0.0 to 11.0.1 (#1129) Bumps [globby](https://github.com/sindresorhus/globby) from 11.0.0 to 11.0.1. - [Release notes](https://github.com/sindresorhus/globby/releases) - [Commits](https://github.com/sindresorhus/globby/compare/v11.0.0...v11.0.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index c5529888e4..5e157517d3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9529,7 +9529,7 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.0, globby@^11.0.0: +globby@11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== @@ -9568,6 +9568,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" From 382e745da1ba756f214d734b03b80ec5ae3112d8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:55:49 +0200 Subject: [PATCH 16/17] build(deps): bump @rollup/plugin-commonjs from 11.0.2 to 12.0.0 (#1023) Bumps [@rollup/plugin-commonjs](https://github.com/rollup/plugins) from 11.0.2 to 12.0.0. - [Release notes](https://github.com/rollup/plugins/releases) - [Commits](https://github.com/rollup/plugins/compare/commonjs-v11.0.2...commonjs-v12.0.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/cli/package.json | 2 +- yarn.lock | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 784df5de6b..97e3aaa972 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", - "@rollup/plugin-commonjs": "^11.0.2", + "@rollup/plugin-commonjs": "^12.0.0", "@rollup/plugin-json": "^4.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@spotify/eslint-config": "^7.0.1", diff --git a/yarn.lock b/yarn.lock index 5e157517d3..afdf67dca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2409,13 +2409,15 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" -"@rollup/plugin-commonjs@^11.0.2": - version "11.0.2" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.2.tgz#837cc6950752327cb90177b608f0928a4e60b582" - integrity sha512-MPYGZr0qdbV5zZj8/2AuomVpnRVXRU5XKXb3HVniwRoRCreGlf5kOE081isNWeiLIi6IYkwTX9zE0/c7V8g81g== +"@rollup/plugin-commonjs@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-12.0.0.tgz#e2f308ae6057499e0f413f878fff7c3a0fdc02a1" + integrity sha512-8+mDQt1QUmN+4Y9D3yCG8AJNewuTSLYPJVzKKUZ+lGeQrI+bV12Tc5HCyt2WdlnG6ihIL/DPbKRJlB40DX40mw== dependencies: - "@rollup/pluginutils" "^3.0.0" + "@rollup/pluginutils" "^3.0.8" + commondir "^1.0.1" estree-walker "^1.0.1" + glob "^7.1.2" is-reference "^1.1.2" magic-string "^0.25.2" resolve "^1.11.0" @@ -2438,7 +2440,7 @@ is-module "^1.0.0" resolve "^1.14.2" -"@rollup/pluginutils@^3.0.0", "@rollup/pluginutils@^3.0.10", "@rollup/pluginutils@^3.0.8": +"@rollup/pluginutils@^3.0.10", "@rollup/pluginutils@^3.0.8": version "3.0.10" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.10.tgz#a659b9025920378494cd8f8c59fbf9b3a50d5f12" integrity sha512-d44M7t+PjmMrASHbhgpSbVgtL6EFyX7J4mYxwQ/c5eoaE6N2VgCgEcWVzNnwycIloti+/MpwFr8qfw+nRw00sw== From e56bce86ca38bf662b1913073c4d36ff8a2570ac Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:56:48 +0200 Subject: [PATCH 17/17] build(deps-dev): bump @types/testing-library__jest-dom (#1095) Bumps [@types/testing-library__jest-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/testing-library__jest-dom) from 5.0.4 to 5.9.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/testing-library__jest-dom) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index afdf67dca3..266e7f2a52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,15 +3680,7 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@*": - version "25.2.1" - resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5" - integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA== - dependencies: - jest-diff "^25.2.1" - pretty-format "^25.2.1" - -"@types/jest@^25.2.2": +"@types/jest@*", "@types/jest@^25.2.2": version "25.2.3" resolved "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz#33d27e4c4716caae4eced355097a47ad363fdcaf" integrity sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw== @@ -4085,17 +4077,10 @@ dependencies: pretty-format "^25.1.0" -"@types/testing-library__jest-dom@^5.0.2": - version "5.7.0" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.7.0.tgz#078790bf4dc89152a74428591a228ec5f9433251" - integrity sha512-LoZ3uonlnAbJUz4bg6UoeFl+frfndXngmkCItSjJ8DD5WlRfVqPC5/LgJASsY/dy7AHH2YJ7PcsdASOydcVeFA== - dependencies: - "@types/jest" "*" - -"@types/testing-library__jest-dom@^5.0.4": - version "5.0.4" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.0.4.tgz#c7bfbafb920cd1ce40506474e70ee73637f33701" - integrity sha512-Ns69aaNvlxvXkPxIwsqeaWH5vJpwa/pdBIlf8LGkRnbV3tiqUgifs13moLXg1NQ2AM23qRR5CtHarNshvRyEdA== +"@types/testing-library__jest-dom@^5.0.2", "@types/testing-library__jest-dom@^5.0.4": + version "5.9.1" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.1.tgz#aba5ee062b7880f69c212ef769389f30752806e5" + integrity sha512-yYn5EKHO3MPEMSOrcAb1dLWY+68CG29LiXKsWmmpVHqoP5+ZRiAVLyUHvPNrO2dABDdUGZvavMsaGpWNjM6N2g== dependencies: "@types/jest" "*"