@forgerock/storage 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,12 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  /*
3
2
  * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
4
3
  *
5
4
  * This software may be modified and distributed under the terms
6
5
  * of the MIT license. See the LICENSE file for details.
7
6
  */
8
- import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
8
  import { createStorage, type StorageConfig } from './storage.effects.js';
10
- import type { CustomStorageObject } from '@forgerock/sdk-types';
9
+ import type { CustomStorageObject, GenericError } from '@forgerock/sdk-types';
11
10
 
12
11
  const localStorageMock = (() => {
13
12
  let store: Record<string, string> = {};
@@ -19,7 +18,7 @@ const localStorageMock = (() => {
19
18
  }
20
19
  return Promise.resolve(value);
21
20
  }),
22
- setItem: vi.fn((key: string, value: any) => {
21
+ setItem: vi.fn((key: string, value: unknown) => {
23
22
  const valueIsString = typeof value === 'string';
24
23
  store[key] = valueIsString ? value : JSON.stringify(value);
25
24
  return Promise.resolve();
@@ -76,16 +75,47 @@ const sessionStorageMock = (() => {
76
75
 
77
76
  Object.defineProperty(global, 'sessionStorage', { value: sessionStorageMock });
78
77
 
78
+ let customStore: Record<string, string> = {};
79
79
  const mockCustomStore: CustomStorageObject = {
80
- get: vi.fn((key: string) => Promise.resolve<string | null>(null)),
81
- set: vi.fn((key: string, value: unknown) => Promise.resolve()),
82
- remove: vi.fn((key: string) => Promise.resolve()),
80
+ get: vi.fn(async (key: string): Promise<string | GenericError> => {
81
+ const keys = Object.keys(customStore);
82
+ if (!keys.includes(key)) {
83
+ return {
84
+ error: 'Retrieving_error',
85
+ message: 'Key not found',
86
+ type: 'unknown_error',
87
+ };
88
+ }
89
+ return customStore[key];
90
+ }),
91
+ set: vi.fn(async (key: string, valueToSet: unknown): Promise<void | GenericError> => {
92
+ if (valueToSet === `"bad-value"` || typeof valueToSet !== 'string') {
93
+ return {
94
+ error: 'Storing_error',
95
+ message: 'Value is bad',
96
+ type: 'unknown_error',
97
+ };
98
+ }
99
+ customStore[key] = valueToSet;
100
+ }),
101
+ remove: vi.fn(async (key: string): Promise<void | GenericError> => {
102
+ const keys = Object.keys(customStore);
103
+ if (!keys.includes(key)) {
104
+ return {
105
+ error: 'Removing_error',
106
+ message: 'Key not found',
107
+ type: 'unknown_error',
108
+ };
109
+ }
110
+ delete customStore[key];
111
+ }),
83
112
  };
84
113
 
85
114
  describe('storage Effect', () => {
86
115
  const storageName = 'MyStorage';
87
116
  const baseConfig: Omit<StorageConfig, 'tokenStore'> = {
88
- storeType: 'localStorage',
117
+ type: 'localStorage',
118
+ name: storageName,
89
119
  prefix: 'testPrefix',
90
120
  };
91
121
  const expectedKey = `${baseConfig.prefix}-${storageName}`;
@@ -96,21 +126,20 @@ describe('storage Effect', () => {
96
126
  sessionStorageMock.clear();
97
127
  vi.clearAllMocks();
98
128
 
99
- (mockCustomStore.get as Mock).mockResolvedValue(null);
100
- (mockCustomStore.set as Mock).mockResolvedValue(undefined);
101
- (mockCustomStore.remove as Mock).mockResolvedValue(undefined);
129
+ customStore = {};
102
130
  });
103
131
 
104
132
  describe('with localStorage', () => {
105
133
  const config: StorageConfig = {
106
134
  ...baseConfig,
107
- storeType: 'localStorage',
135
+ name: storageName,
136
+ type: 'localStorage',
108
137
  };
109
138
 
110
- const storageInstance = createStorage(config, storageName);
139
+ const storageInstance = createStorage(config);
111
140
 
112
141
  it('should call localStorage.getItem with the correct key and return value', async () => {
113
- localStorageMock.setItem(expectedKey, JSON.stringify(testValue));
142
+ await localStorageMock.setItem(expectedKey, JSON.stringify(testValue));
114
143
  const result = await storageInstance.get();
115
144
  expect(localStorageMock.getItem).toHaveBeenCalledTimes(1);
116
145
  expect(localStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
@@ -127,7 +156,8 @@ describe('storage Effect', () => {
127
156
  });
128
157
 
129
158
  it('should call localStorage.setItem with the correct key and value', async () => {
130
- await storageInstance.set(testValue);
159
+ const result = await storageInstance.set(testValue);
160
+ expect(result).toBeNull();
131
161
  expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
132
162
  expect(localStorageMock.setItem).toHaveBeenCalledWith(expectedKey, JSON.stringify(testValue));
133
163
  expect(await localStorageMock.getItem(expectedKey)).toBe(JSON.stringify(testValue));
@@ -146,8 +176,9 @@ describe('storage Effect', () => {
146
176
  });
147
177
 
148
178
  it('should call localStorage.removeItem with the correct key', async () => {
149
- localStorageMock.setItem(expectedKey, testValue);
150
- await storageInstance.remove();
179
+ await localStorageMock.setItem(expectedKey, testValue);
180
+ const result = await storageInstance.remove();
181
+ expect(result).toBeNull();
151
182
  expect(localStorageMock.removeItem).toHaveBeenCalledTimes(1);
152
183
  expect(localStorageMock.removeItem).toHaveBeenCalledWith(expectedKey);
153
184
  expect(await localStorageMock.getItem(expectedKey)).toBeNull();
@@ -156,28 +187,28 @@ describe('storage Effect', () => {
156
187
 
157
188
  it('should parse objects/arrays when calling localStorage.getItem', async () => {
158
189
  const testObject = { a: 1, b: 'test' };
159
- storageInstance.set(testObject);
190
+ await storageInstance.set(testObject);
160
191
 
161
192
  const result = await storageInstance.get();
162
- console.log(result);
163
193
 
164
194
  expect(localStorageMock.getItem).toHaveBeenCalledTimes(1);
165
195
  expect(localStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
166
- // expect(result).toEqual(testObject);
167
- // expect(mockCustomStore.remove).not.toHaveBeenCalled();
196
+ expect(result).toEqual(testObject);
197
+ expect(mockCustomStore.remove).not.toHaveBeenCalled();
168
198
  });
169
199
  });
170
200
 
171
201
  describe('with sessionStorage', () => {
202
+ const storageName = 'MyStorage';
172
203
  const config: StorageConfig = {
173
204
  ...baseConfig,
174
- storeType: 'sessionStorage',
205
+ name: storageName,
206
+ type: 'sessionStorage',
175
207
  };
176
- const storageName = 'MyStorage';
177
- const storageInstance = createStorage(config, storageName);
208
+ const storageInstance = createStorage(config);
178
209
 
179
210
  it('should call sessionStorage.getItem with the correct key and return value', async () => {
180
- sessionStorageMock.setItem(expectedKey, JSON.stringify(testValue));
211
+ await sessionStorageMock.setItem(expectedKey, JSON.stringify(testValue));
181
212
  const result = await storageInstance.get();
182
213
  expect(sessionStorageMock.getItem).toHaveBeenCalledTimes(1);
183
214
  expect(sessionStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
@@ -196,16 +227,14 @@ describe('storage Effect', () => {
196
227
  const obj = { tokens: 123 };
197
228
  await storageInstance.set(obj);
198
229
  const result = await storageInstance.get();
199
- if (!result) {
200
- // we should not hit this expect
201
- expect(false).toBe(true);
202
- }
230
+
203
231
  expect(result).toStrictEqual(obj);
204
232
  expect(sessionStorageMock.getItem).toHaveBeenCalledTimes(1);
205
233
  expect(sessionStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
206
234
  });
207
235
  it('should call sessionStorage.setItem with the correct key and value', async () => {
208
- await storageInstance.set(testValue);
236
+ const result = await storageInstance.set(testValue);
237
+ expect(result).toBeNull();
209
238
  expect(sessionStorageMock.setItem).toHaveBeenCalledTimes(1);
210
239
  expect(sessionStorageMock.setItem).toHaveBeenCalledWith(
211
240
  expectedKey,
@@ -215,9 +244,10 @@ describe('storage Effect', () => {
215
244
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
216
245
  expect(mockCustomStore.set).not.toHaveBeenCalled();
217
246
  });
218
- it('should call sessionStorage.setItem with the correct key and value', async () => {
247
+ it('should call sessionStorage.setItem with the correct key and value and stringify objects', async () => {
219
248
  const obj = { tokens: 123 };
220
- await storageInstance.set(obj);
249
+ const result = await storageInstance.set(obj);
250
+ expect(result).toBeNull();
221
251
  expect(sessionStorageMock.setItem).toHaveBeenCalledTimes(1);
222
252
  expect(sessionStorageMock.setItem).toHaveBeenCalledWith(expectedKey, JSON.stringify(obj));
223
253
  expect(await sessionStorageMock.getItem(expectedKey)).toBe(JSON.stringify(obj));
@@ -225,7 +255,8 @@ describe('storage Effect', () => {
225
255
  expect(mockCustomStore.set).not.toHaveBeenCalled();
226
256
  });
227
257
  it('should call sessionStorage.removeItem with the correct key', async () => {
228
- await storageInstance.remove();
258
+ const result = await storageInstance.remove();
259
+ expect(result).toBeNull();
229
260
  expect(sessionStorageMock.removeItem).toHaveBeenCalledTimes(1);
230
261
  expect(sessionStorageMock.removeItem).toHaveBeenCalledWith(expectedKey);
231
262
  expect(await sessionStorageMock.getItem(expectedKey)).toBeNull();
@@ -234,16 +265,16 @@ describe('storage Effect', () => {
234
265
  });
235
266
  });
236
267
 
237
- describe('with custom TokenStoreObject', () => {
268
+ describe('with custom storage', () => {
238
269
  const config: StorageConfig = {
239
270
  ...baseConfig,
240
- storeType: 'localStorage',
271
+ type: 'custom',
272
+ custom: mockCustomStore,
241
273
  };
242
- const myStorage = 'MyStorage';
243
- const storageInstance = createStorage(config, myStorage, mockCustomStore);
274
+ const storageInstance = createStorage(config);
244
275
 
245
276
  it('should call customStore.get with the correct key and return its value', async () => {
246
- (mockCustomStore.get as Mock).mockResolvedValueOnce(JSON.stringify(testValue));
277
+ await storageInstance.set(testValue);
247
278
  const result = await storageInstance.get();
248
279
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
249
280
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
@@ -252,32 +283,37 @@ describe('storage Effect', () => {
252
283
  expect(sessionStorageMock.getItem).not.toHaveBeenCalled();
253
284
  });
254
285
 
255
- it('should return null if customStore.get returns null', async () => {
256
- (mockCustomStore.get as Mock).mockResolvedValueOnce(null);
286
+ it('should parse objects/arrays returned from customStore.get', async () => {
287
+ const testObject = { token: 'abc', user: 'xyz' };
288
+ await storageInstance.set(testObject);
289
+
257
290
  const result = await storageInstance.get();
291
+
258
292
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
259
293
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
294
+ expect(result).toEqual(testObject); // Verify it was parsed
260
295
  });
261
296
 
262
- it('should parse JSON strings returned from customStore.get', async () => {
263
- const testObject = { token: 'abc', user: 'xyz' };
264
- const jsonString = JSON.stringify(testObject);
265
- (mockCustomStore.get as Mock).mockResolvedValueOnce(jsonString); // Mock returns JSON string
266
-
297
+ it('should return an error if customStore.get errors', async () => {
267
298
  const result = await storageInstance.get();
268
-
299
+ expect(result).toStrictEqual({
300
+ error: 'Retrieving_error',
301
+ message: 'Key not found',
302
+ type: 'unknown_error',
303
+ });
269
304
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
270
305
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
271
- expect(result).toEqual(testObject); // Verify it was parsed
272
306
  });
273
307
 
274
308
  it('should call customStore.set with the correct key and value', async () => {
275
- await storageInstance.set(testValue);
309
+ const result = await storageInstance.set(testValue);
310
+ expect(result).toBeNull();
276
311
  expect(mockCustomStore.set).toHaveBeenCalledTimes(1);
277
312
  expect(mockCustomStore.set).toHaveBeenCalledWith(expectedKey, JSON.stringify(testValue));
278
313
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
279
314
  expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
280
315
  });
316
+
281
317
  it('should call customStore.set with the correct key and value and stringify objects', async () => {
282
318
  await storageInstance.set({ test: { tokens: '123' } });
283
319
  expect(mockCustomStore.set).toHaveBeenCalledTimes(1);
@@ -288,19 +324,43 @@ describe('storage Effect', () => {
288
324
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
289
325
  expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
290
326
  });
327
+
328
+ it('should return an error if customStore.set errors', async () => {
329
+ const result = await storageInstance.set('bad-value');
330
+ expect(result).toStrictEqual({
331
+ error: 'Storing_error',
332
+ message: 'Value is bad',
333
+ type: 'unknown_error',
334
+ });
335
+ expect(mockCustomStore.set).toHaveBeenCalledTimes(1);
336
+ expect(mockCustomStore.set).toHaveBeenCalledWith(expectedKey, JSON.stringify('bad-value'));
337
+ });
338
+
291
339
  it('should call customStore.remove with the correct key', async () => {
292
- await storageInstance.remove();
340
+ await storageInstance.set(testValue);
341
+ const result = await storageInstance.remove();
342
+ expect(result).toBeNull();
293
343
  expect(mockCustomStore.remove).toHaveBeenCalledTimes(1);
294
344
  expect(mockCustomStore.remove).toHaveBeenCalledWith(expectedKey);
295
345
  expect(localStorageMock.removeItem).not.toHaveBeenCalled();
296
346
  expect(sessionStorageMock.removeItem).not.toHaveBeenCalled();
297
347
  });
348
+
349
+ it('should return an error if customStore.remove errors', async () => {
350
+ const result = await storageInstance.remove();
351
+ expect(result).toStrictEqual({
352
+ error: 'Removing_error',
353
+ message: 'Key not found',
354
+ type: 'unknown_error',
355
+ });
356
+ expect(mockCustomStore.remove).toHaveBeenCalledTimes(1);
357
+ expect(mockCustomStore.remove).toHaveBeenCalledWith(expectedKey);
358
+ });
298
359
  });
299
360
 
300
361
  it('should return a function that returns the storage interface', () => {
301
- const config: StorageConfig = { ...baseConfig, storeType: 'localStorage' };
302
- const myStorage = 'MyStorage';
303
- const storageInterface = createStorage(config, myStorage);
362
+ const config: StorageConfig = { ...baseConfig, type: 'localStorage' };
363
+ const storageInterface = createStorage(config);
304
364
  expect(storageInterface).toHaveProperty('get');
305
365
  expect(storageInterface).toHaveProperty('set');
306
366
  expect(storageInterface).toHaveProperty('remove');
@@ -4,128 +4,126 @@
4
4
  * This software may be modified and distributed under the terms
5
5
  * of the MIT license. See the LICENSE file for details.
6
6
  */
7
- import { CustomStorageObject } from '@forgerock/sdk-types';
7
+ import { CustomStorageObject, GenericError } from '@forgerock/sdk-types';
8
8
 
9
- export interface StorageConfig {
10
- storeType: CustomStorageObject | 'localStorage' | 'sessionStorage';
9
+ export interface StorageClient<Value> {
10
+ get: () => Promise<Value | GenericError | null>;
11
+ set: (value: Value) => Promise<GenericError | null>;
12
+ remove: () => Promise<GenericError | null>;
13
+ }
14
+
15
+ export type StorageConfig = BrowserStorageConfig | CustomStorageConfig;
16
+
17
+ export interface BrowserStorageConfig {
18
+ type: 'localStorage' | 'sessionStorage';
11
19
  prefix?: string;
20
+ name: string;
12
21
  }
13
22
 
14
- export interface GenericError {
15
- code?: string | number;
16
- message: string;
17
- type:
18
- | 'argument_error'
19
- | 'davinci_error'
20
- | 'internal_error'
21
- | 'network_error'
22
- | 'state_error'
23
- | 'unknown_error';
23
+ export interface CustomStorageConfig {
24
+ type: 'custom';
25
+ prefix?: string;
26
+ name: string;
27
+ custom: CustomStorageObject;
24
28
  }
25
29
 
26
- export function createStorage<Value>(
27
- config: StorageConfig,
28
- storageName: string,
29
- customStore?: CustomStorageObject,
30
- ) {
31
- const { storeType, prefix = 'pic' } = config;
30
+ function createStorageError(
31
+ storeType: 'localStorage' | 'sessionStorage' | 'custom',
32
+ action: 'Storing' | 'Retrieving' | 'Removing' | 'Parsing',
33
+ ): GenericError {
34
+ let storageName;
35
+ switch (storeType) {
36
+ case 'localStorage':
37
+ storageName = 'local';
38
+ break;
39
+ case 'sessionStorage':
40
+ storageName = 'session';
41
+ break;
42
+ case 'custom':
43
+ storageName = 'custom';
44
+ break;
45
+ default:
46
+ break;
47
+ }
48
+
49
+ return {
50
+ error: `${action}_error`,
51
+ message: `Error ${action.toLowerCase()} value from ${storageName} storage`,
52
+ type: action === 'Parsing' ? 'parse_error' : 'unknown_error',
53
+ };
54
+ }
55
+
56
+ export function createStorage<Value>(config: StorageConfig): StorageClient<Value> {
57
+ const { type: storeType, prefix = 'pic', name } = config;
58
+ const key = `${prefix}-${name}`;
59
+ const storageTypes = {
60
+ sessionStorage,
61
+ localStorage,
62
+ };
63
+
64
+ if (storeType === 'custom' && !('custom' in config)) {
65
+ throw new Error('Custom storage configuration must include a custom storage object');
66
+ }
32
67
 
33
- const key = `${prefix}-${storageName}`;
34
68
  return {
35
69
  get: async function storageGet(): Promise<Value | GenericError | null> {
36
- if (customStore) {
37
- const value = await customStore.get(key);
38
- if (value === null) {
70
+ if (storeType === 'custom') {
71
+ const value = await config.custom.get(key);
72
+ if (value === null || (typeof value === 'object' && 'error' in value)) {
39
73
  return value;
40
74
  }
75
+
41
76
  try {
42
77
  const parsed = JSON.parse(value);
43
78
  return parsed as Value;
44
79
  } catch {
45
- return {
46
- code: 'Parse_Error',
47
- message: 'Eror parsing value from provided storage',
48
- type: 'unknown_error',
49
- };
80
+ return createStorageError(storeType, 'Parsing');
50
81
  }
51
82
  }
52
- if (storeType === 'sessionStorage') {
53
- const value = await sessionStorage.getItem(key);
83
+
84
+ let value: string | null;
85
+ try {
86
+ value = await storageTypes[storeType].getItem(key);
54
87
  if (value === null) {
55
88
  return value;
56
89
  }
57
- try {
58
- const parsed = JSON.parse(value);
59
- return parsed as Value;
60
- } catch {
61
- return {
62
- code: 'Parse_Error',
63
- message: 'Eror parsing value from session storage',
64
- type: 'unknown_error',
65
- };
66
- }
90
+ } catch {
91
+ return createStorageError(storeType, 'Retrieving');
67
92
  }
68
- const value = await localStorage.getItem(key);
69
93
 
70
- if (value === null) {
71
- return value;
72
- }
73
94
  try {
74
95
  const parsed = JSON.parse(value);
75
96
  return parsed as Value;
76
97
  } catch {
77
- return {
78
- code: 'Parse_Error',
79
- message: 'Eror parsing value from local storage',
80
- type: 'unknown_error',
81
- };
98
+ return createStorageError(storeType, 'Parsing');
82
99
  }
83
100
  },
84
- set: async function storageSet(value: Value) {
101
+ set: async function storageSet(value: Value): Promise<GenericError | null> {
85
102
  const valueToStore = JSON.stringify(value);
86
- if (customStore) {
87
- try {
88
- await customStore.set(key, valueToStore);
89
- return Promise.resolve();
90
- } catch {
91
- return {
92
- code: 'Storing_Error',
93
- message: 'Eror storing value in custom storage',
94
- type: 'unknown_error',
95
- };
96
- }
97
- }
98
- if (storeType === 'sessionStorage') {
99
- try {
100
- await sessionStorage.setItem(key, valueToStore);
101
- return Promise.resolve();
102
- } catch {
103
- return {
104
- code: 'Storing_Error',
105
- message: 'Eror storing value in session storage',
106
- type: 'unknown_error',
107
- };
108
- }
103
+ if (storeType === 'custom') {
104
+ const value = await config.custom.set(key, valueToStore);
105
+ return Promise.resolve(value ?? null);
109
106
  }
107
+
110
108
  try {
111
- await localStorage.setItem(key, valueToStore);
112
- return Promise.resolve();
109
+ await storageTypes[storeType].setItem(key, valueToStore);
110
+ return Promise.resolve(null);
113
111
  } catch {
114
- return {
115
- code: 'Storing_Error',
116
- message: 'Eror storing value in local storage',
117
- type: 'unknown_error',
118
- };
112
+ return createStorageError(storeType, 'Storing');
119
113
  }
120
114
  },
121
- remove: async function storageSet() {
122
- if (customStore) {
123
- return await customStore.remove(key);
115
+ remove: async function storageRemove(): Promise<GenericError | null> {
116
+ if (storeType === 'custom') {
117
+ const value = await config.custom.remove(key);
118
+ return Promise.resolve(value ?? null);
124
119
  }
125
- if (storeType === 'sessionStorage') {
126
- return await sessionStorage.removeItem(key);
120
+
121
+ try {
122
+ await storageTypes[storeType].removeItem(key);
123
+ return Promise.resolve(null);
124
+ } catch {
125
+ return createStorageError(storeType, 'Removing');
127
126
  }
128
- return await localStorage.removeItem(key);
129
127
  },
130
- };
128
+ } as StorageClient<Value>;
131
129
  }
package/vite.config.ts CHANGED
Binary file