-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(workspace): Created reusable ScopedStorage (#13920)
- Loading branch information
1 parent
0808b19
commit e371e77
Showing
4 changed files
with
194 additions
and
0 deletions.
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { type ScopedStorage, ScopedStorageImpl } from './ScopedStorage'; | ||
|
||
describe('ScopedStorage', () => { | ||
beforeEach(() => { | ||
window.localStorage.clear(); | ||
}); | ||
|
||
describe('add new key', () => { | ||
it('should create a single scoped key with the provided key-value pair as its value', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
scopedStorage.setItem('firstName', 'Random Value'); | ||
expect(scopedStorage.getItem('firstName')).toBe('Random Value'); | ||
}); | ||
}); | ||
|
||
describe('get item', () => { | ||
it('should return "null" if key does not exist', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
expect(scopedStorage.getItem('firstName')).toBeNull(); | ||
}); | ||
}); | ||
|
||
describe('update existing key', () => { | ||
it('should append a new key-value pair to the existing scoped key', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
scopedStorage.setItem('firstKey', 'first value'); | ||
scopedStorage.setItem('secondKey', 'secondValue'); | ||
|
||
expect(scopedStorage.getItem('firstKey')).toBe('first value'); | ||
expect(scopedStorage.getItem('secondKey')).toBe('secondValue'); | ||
}); | ||
|
||
it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
scopedStorage.setItem('firstKey', 'first value'); | ||
scopedStorage.setItem('firstKey', 'first value is updated'); | ||
expect(scopedStorage.getItem('firstKey')).toBe('first value is updated'); | ||
}); | ||
}); | ||
|
||
describe('delete values from key', () => { | ||
it('should remove a specific key-value pair from the existing scoped key', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
scopedStorage.setItem('firstKey', 'first value'); | ||
expect(scopedStorage.getItem('firstKey')).toBeDefined(); | ||
|
||
scopedStorage.removeItem('firstKey'); | ||
expect(scopedStorage.getItem('firstKey')).toBeUndefined(); | ||
}); | ||
|
||
it('should not remove key if it does not exist', () => { | ||
const removeItemMock = jest.fn(); | ||
const customStorage = { | ||
getItem: jest.fn().mockImplementation(() => null), | ||
removeItem: removeItemMock, | ||
setItem: jest.fn(), | ||
}; | ||
|
||
const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); | ||
scopedStorage.removeItem('keyDoesNotExist'); | ||
|
||
expect(removeItemMock).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('Storage parsing', () => { | ||
const consoleErrorMock = jest.fn(); | ||
const originalConsoleError = console.error; | ||
beforeEach(() => { | ||
console.error = consoleErrorMock; | ||
}); | ||
|
||
afterEach(() => { | ||
console.error = originalConsoleError; | ||
}); | ||
|
||
it('should console.error when parsing the storage fails', () => { | ||
window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}'); | ||
const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test'); | ||
expect(scopedStorage.getItem('person')).toBeNull(); | ||
expect(consoleErrorMock).toHaveBeenCalledWith( | ||
expect.stringContaining( | ||
'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:', | ||
), | ||
); | ||
}); | ||
}); | ||
|
||
// Verify that Dependency Inversion works as expected | ||
describe('when using localStorage', () => { | ||
it('should store and retrieve values using localStorage', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage'); | ||
scopedStorage.setItem('firstNameInSession', 'Random Session Value'); | ||
expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); | ||
}); | ||
}); | ||
|
||
describe('when using sessionStorage', () => { | ||
it('should store and retrieve values using sessionStorage', () => { | ||
const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage'); | ||
scopedStorage.setItem('firstNameInSession', 'Random Session Value'); | ||
expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value'); | ||
}); | ||
}); | ||
|
||
describe('when using a custom storage implementation', () => { | ||
it('should store and retrieve values using the provided custom storage', () => { | ||
const setItemMock = jest.fn(); | ||
|
||
const customStorage: ScopedStorage = { | ||
setItem: setItemMock, | ||
getItem: jest.fn(), | ||
removeItem: jest.fn(), | ||
}; | ||
|
||
const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test'); | ||
scopedStorage.setItem('testKey', 'testValue'); | ||
expect(setItemMock).toHaveBeenCalledWith('unit/test', '{"testKey":"testValue"}'); | ||
}); | ||
}); | ||
}); |
71 changes: 71 additions & 0 deletions
71
frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
type StorageKey = string; | ||
|
||
export interface ScopedStorage extends Pick<Storage, 'setItem' | 'getItem' | 'removeItem'> {} | ||
|
||
export class ScopedStorageImpl implements ScopedStorage { | ||
private readonly storageKey: StorageKey; | ||
private readonly scopedStorage: ScopedStorage; | ||
|
||
constructor( | ||
private storage: ScopedStorage, | ||
private key: StorageKey, | ||
) { | ||
this.storageKey = this.key; | ||
this.scopedStorage = this.storage; | ||
} | ||
|
||
public setItem<T>(key: string, value: T): void { | ||
const storageRecords: T = this.getAllRecordsInStorage(); | ||
this.saveToStorage( | ||
JSON.stringify({ | ||
...storageRecords, | ||
[key]: value, | ||
}), | ||
); | ||
} | ||
|
||
public getItem<T>(key: string) { | ||
const records: T = this.getAllRecordsInStorage(); | ||
|
||
if (!records) { | ||
return null; | ||
} | ||
|
||
return records[key] as T; | ||
} | ||
|
||
public removeItem<T>(key: string): void { | ||
const storageRecords: T | null = this.getAllRecordsInStorage<T>(); | ||
|
||
if (!storageRecords) { | ||
return; | ||
} | ||
|
||
const storageCopy = { ...storageRecords }; | ||
delete storageCopy[key]; | ||
this.saveToStorage(JSON.stringify({ ...storageCopy })); | ||
} | ||
|
||
private getAllRecordsInStorage<T>(): T | null { | ||
return this.parseStorageData<T>(this.scopedStorage.getItem(this.storageKey)); | ||
} | ||
|
||
private saveToStorage(value: string) { | ||
this.storage.setItem(this.storageKey, value); | ||
} | ||
|
||
private parseStorageData<T>(storage: string | null): T | null { | ||
if (!storage) { | ||
return null; | ||
} | ||
|
||
try { | ||
return JSON.parse(storage) satisfies T; | ||
} catch (error) { | ||
console.error( | ||
`Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`, | ||
); | ||
return null; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ScopedStorageImpl, type ScopedStorage } from './ScopedStorage'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters