@forgerock/storage 0.0.0-beta-20250825180717 → 0.0.0-beta-20251003204059

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,10 +75,40 @@ 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', () => {
@@ -97,9 +126,7 @@ describe('storage Effect', () => {
97
126
  sessionStorageMock.clear();
98
127
  vi.clearAllMocks();
99
128
 
100
- (mockCustomStore.get as Mock).mockResolvedValue(null);
101
- (mockCustomStore.set as Mock).mockResolvedValue(undefined);
102
- (mockCustomStore.remove as Mock).mockResolvedValue(undefined);
129
+ customStore = {};
103
130
  });
104
131
 
105
132
  describe('with localStorage', () => {
@@ -112,7 +139,7 @@ describe('storage Effect', () => {
112
139
  const storageInstance = createStorage(config);
113
140
 
114
141
  it('should call localStorage.getItem with the correct key and return value', async () => {
115
- localStorageMock.setItem(expectedKey, JSON.stringify(testValue));
142
+ await localStorageMock.setItem(expectedKey, JSON.stringify(testValue));
116
143
  const result = await storageInstance.get();
117
144
  expect(localStorageMock.getItem).toHaveBeenCalledTimes(1);
118
145
  expect(localStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
@@ -129,7 +156,8 @@ describe('storage Effect', () => {
129
156
  });
130
157
 
131
158
  it('should call localStorage.setItem with the correct key and value', async () => {
132
- await storageInstance.set(testValue);
159
+ const result = await storageInstance.set(testValue);
160
+ expect(result).toBeNull();
133
161
  expect(localStorageMock.setItem).toHaveBeenCalledTimes(1);
134
162
  expect(localStorageMock.setItem).toHaveBeenCalledWith(expectedKey, JSON.stringify(testValue));
135
163
  expect(await localStorageMock.getItem(expectedKey)).toBe(JSON.stringify(testValue));
@@ -148,8 +176,9 @@ describe('storage Effect', () => {
148
176
  });
149
177
 
150
178
  it('should call localStorage.removeItem with the correct key', async () => {
151
- localStorageMock.setItem(expectedKey, testValue);
152
- await storageInstance.remove();
179
+ await localStorageMock.setItem(expectedKey, testValue);
180
+ const result = await storageInstance.remove();
181
+ expect(result).toBeNull();
153
182
  expect(localStorageMock.removeItem).toHaveBeenCalledTimes(1);
154
183
  expect(localStorageMock.removeItem).toHaveBeenCalledWith(expectedKey);
155
184
  expect(await localStorageMock.getItem(expectedKey)).toBeNull();
@@ -158,15 +187,14 @@ describe('storage Effect', () => {
158
187
 
159
188
  it('should parse objects/arrays when calling localStorage.getItem', async () => {
160
189
  const testObject = { a: 1, b: 'test' };
161
- storageInstance.set(testObject);
190
+ await storageInstance.set(testObject);
162
191
 
163
192
  const result = await storageInstance.get();
164
- console.log(result);
165
193
 
166
194
  expect(localStorageMock.getItem).toHaveBeenCalledTimes(1);
167
195
  expect(localStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
168
- // expect(result).toEqual(testObject);
169
- // expect(mockCustomStore.remove).not.toHaveBeenCalled();
196
+ expect(result).toEqual(testObject);
197
+ expect(mockCustomStore.remove).not.toHaveBeenCalled();
170
198
  });
171
199
  });
172
200
 
@@ -180,7 +208,7 @@ describe('storage Effect', () => {
180
208
  const storageInstance = createStorage(config);
181
209
 
182
210
  it('should call sessionStorage.getItem with the correct key and return value', async () => {
183
- sessionStorageMock.setItem(expectedKey, JSON.stringify(testValue));
211
+ await sessionStorageMock.setItem(expectedKey, JSON.stringify(testValue));
184
212
  const result = await storageInstance.get();
185
213
  expect(sessionStorageMock.getItem).toHaveBeenCalledTimes(1);
186
214
  expect(sessionStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
@@ -199,16 +227,14 @@ describe('storage Effect', () => {
199
227
  const obj = { tokens: 123 };
200
228
  await storageInstance.set(obj);
201
229
  const result = await storageInstance.get();
202
- if (!result) {
203
- // we should not hit this expect
204
- expect(false).toBe(true);
205
- }
230
+
206
231
  expect(result).toStrictEqual(obj);
207
232
  expect(sessionStorageMock.getItem).toHaveBeenCalledTimes(1);
208
233
  expect(sessionStorageMock.getItem).toHaveBeenCalledWith(expectedKey);
209
234
  });
210
235
  it('should call sessionStorage.setItem with the correct key and value', async () => {
211
- await storageInstance.set(testValue);
236
+ const result = await storageInstance.set(testValue);
237
+ expect(result).toBeNull();
212
238
  expect(sessionStorageMock.setItem).toHaveBeenCalledTimes(1);
213
239
  expect(sessionStorageMock.setItem).toHaveBeenCalledWith(
214
240
  expectedKey,
@@ -218,9 +244,10 @@ describe('storage Effect', () => {
218
244
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
219
245
  expect(mockCustomStore.set).not.toHaveBeenCalled();
220
246
  });
221
- 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 () => {
222
248
  const obj = { tokens: 123 };
223
- await storageInstance.set(obj);
249
+ const result = await storageInstance.set(obj);
250
+ expect(result).toBeNull();
224
251
  expect(sessionStorageMock.setItem).toHaveBeenCalledTimes(1);
225
252
  expect(sessionStorageMock.setItem).toHaveBeenCalledWith(expectedKey, JSON.stringify(obj));
226
253
  expect(await sessionStorageMock.getItem(expectedKey)).toBe(JSON.stringify(obj));
@@ -228,7 +255,8 @@ describe('storage Effect', () => {
228
255
  expect(mockCustomStore.set).not.toHaveBeenCalled();
229
256
  });
230
257
  it('should call sessionStorage.removeItem with the correct key', async () => {
231
- await storageInstance.remove();
258
+ const result = await storageInstance.remove();
259
+ expect(result).toBeNull();
232
260
  expect(sessionStorageMock.removeItem).toHaveBeenCalledTimes(1);
233
261
  expect(sessionStorageMock.removeItem).toHaveBeenCalledWith(expectedKey);
234
262
  expect(await sessionStorageMock.getItem(expectedKey)).toBeNull();
@@ -237,8 +265,7 @@ describe('storage Effect', () => {
237
265
  });
238
266
  });
239
267
 
240
- describe('with custom TokenStoreObject', () => {
241
- const myStorage = 'MyStorage';
268
+ describe('with custom storage', () => {
242
269
  const config: StorageConfig = {
243
270
  ...baseConfig,
244
271
  type: 'custom',
@@ -247,7 +274,7 @@ describe('storage Effect', () => {
247
274
  const storageInstance = createStorage(config);
248
275
 
249
276
  it('should call customStore.get with the correct key and return its value', async () => {
250
- (mockCustomStore.get as Mock).mockResolvedValueOnce(JSON.stringify(testValue));
277
+ await storageInstance.set(testValue);
251
278
  const result = await storageInstance.get();
252
279
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
253
280
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
@@ -256,32 +283,37 @@ describe('storage Effect', () => {
256
283
  expect(sessionStorageMock.getItem).not.toHaveBeenCalled();
257
284
  });
258
285
 
259
- it('should return null if customStore.get returns null', async () => {
260
- (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
+
261
290
  const result = await storageInstance.get();
291
+
262
292
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
263
293
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
294
+ expect(result).toEqual(testObject); // Verify it was parsed
264
295
  });
265
296
 
266
- it('should parse JSON strings returned from customStore.get', async () => {
267
- const testObject = { token: 'abc', user: 'xyz' };
268
- const jsonString = JSON.stringify(testObject);
269
- (mockCustomStore.get as Mock).mockResolvedValueOnce(jsonString); // Mock returns JSON string
270
-
297
+ it('should return an error if customStore.get errors', async () => {
271
298
  const result = await storageInstance.get();
272
-
299
+ expect(result).toStrictEqual({
300
+ error: 'Retrieving_error',
301
+ message: 'Key not found',
302
+ type: 'unknown_error',
303
+ });
273
304
  expect(mockCustomStore.get).toHaveBeenCalledTimes(1);
274
305
  expect(mockCustomStore.get).toHaveBeenCalledWith(expectedKey);
275
- expect(result).toEqual(testObject); // Verify it was parsed
276
306
  });
277
307
 
278
308
  it('should call customStore.set with the correct key and value', async () => {
279
- await storageInstance.set(testValue);
309
+ const result = await storageInstance.set(testValue);
310
+ expect(result).toBeNull();
280
311
  expect(mockCustomStore.set).toHaveBeenCalledTimes(1);
281
312
  expect(mockCustomStore.set).toHaveBeenCalledWith(expectedKey, JSON.stringify(testValue));
282
313
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
283
314
  expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
284
315
  });
316
+
285
317
  it('should call customStore.set with the correct key and value and stringify objects', async () => {
286
318
  await storageInstance.set({ test: { tokens: '123' } });
287
319
  expect(mockCustomStore.set).toHaveBeenCalledTimes(1);
@@ -292,17 +324,41 @@ describe('storage Effect', () => {
292
324
  expect(localStorageMock.setItem).not.toHaveBeenCalled();
293
325
  expect(sessionStorageMock.setItem).not.toHaveBeenCalled();
294
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
+
295
339
  it('should call customStore.remove with the correct key', async () => {
296
- await storageInstance.remove();
340
+ await storageInstance.set(testValue);
341
+ const result = await storageInstance.remove();
342
+ expect(result).toBeNull();
297
343
  expect(mockCustomStore.remove).toHaveBeenCalledTimes(1);
298
344
  expect(mockCustomStore.remove).toHaveBeenCalledWith(expectedKey);
299
345
  expect(localStorageMock.removeItem).not.toHaveBeenCalled();
300
346
  expect(sessionStorageMock.removeItem).not.toHaveBeenCalled();
301
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
+ });
302
359
  });
303
360
 
304
361
  it('should return a function that returns the storage interface', () => {
305
- const myStorage = 'MyStorage';
306
362
  const config: StorageConfig = { ...baseConfig, type: 'localStorage' };
307
363
  const storageInterface = createStorage(config);
308
364
  expect(storageInterface).toHaveProperty('get');
@@ -8,12 +8,8 @@ import { CustomStorageObject, GenericError } from '@forgerock/sdk-types';
8
8
 
9
9
  export interface StorageClient<Value> {
10
10
  get: () => Promise<Value | GenericError | null>;
11
- set: (value: Value) => Promise<void | {
12
- code: string;
13
- message: string;
14
- type: string;
15
- }>;
16
- remove: () => Promise<void>;
11
+ set: (value: Value) => Promise<GenericError | null>;
12
+ remove: () => Promise<GenericError | null>;
17
13
  }
18
14
 
19
15
  export type StorageConfig = BrowserStorageConfig | CustomStorageConfig;
@@ -31,109 +27,103 @@ export interface CustomStorageConfig {
31
27
  custom: CustomStorageObject;
32
28
  }
33
29
 
34
- export function createStorage<Value>(config: StorageConfig) {
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> {
35
57
  const { type: storeType, prefix = 'pic', name } = config;
58
+ const key = `${prefix}-${name}`;
59
+ const storageTypes = {
60
+ sessionStorage,
61
+ localStorage,
62
+ };
36
63
 
37
64
  if (storeType === 'custom' && !('custom' in config)) {
38
65
  throw new Error('Custom storage configuration must include a custom storage object');
39
66
  }
40
67
 
41
- const key = `${prefix}-${name}`;
42
68
  return {
43
69
  get: async function storageGet(): Promise<Value | GenericError | null> {
44
- if ('custom' in config) {
70
+ if (storeType === 'custom') {
45
71
  const value = await config.custom.get(key);
46
- if (value === null) {
72
+ if (value === null || (typeof value === 'object' && 'error' in value)) {
47
73
  return value;
48
74
  }
75
+
49
76
  try {
50
77
  const parsed = JSON.parse(value);
51
78
  return parsed as Value;
52
79
  } catch {
53
- return {
54
- error: 'Parse_Error',
55
- message: 'Error parsing value from provided storage',
56
- type: 'parse_error',
57
- };
80
+ return createStorageError(storeType, 'Parsing');
58
81
  }
59
82
  }
60
- if (storeType === 'sessionStorage') {
61
- const value = await sessionStorage.getItem(key);
83
+
84
+ let value: string | null;
85
+ try {
86
+ value = await storageTypes[storeType].getItem(key);
62
87
  if (value === null) {
63
88
  return value;
64
89
  }
65
- try {
66
- const parsed = JSON.parse(value);
67
- return parsed as Value;
68
- } catch {
69
- return {
70
- error: 'Parse_Error',
71
- message: 'Error parsing value from session storage',
72
- type: 'parse_error',
73
- };
74
- }
90
+ } catch {
91
+ return createStorageError(storeType, 'Retrieving');
75
92
  }
76
- const value = await localStorage.getItem(key);
77
93
 
78
- if (value === null) {
79
- return value;
80
- }
81
94
  try {
82
95
  const parsed = JSON.parse(value);
83
96
  return parsed as Value;
84
97
  } catch {
85
- return {
86
- error: 'Parse_Error',
87
- message: 'Error parsing value from local storage',
88
- type: 'parse_error',
89
- };
98
+ return createStorageError(storeType, 'Parsing');
90
99
  }
91
100
  },
92
- set: async function storageSet(value: Value) {
101
+ set: async function storageSet(value: Value): Promise<GenericError | null> {
93
102
  const valueToStore = JSON.stringify(value);
94
- if ('custom' in config) {
95
- try {
96
- await config.custom.set(key, valueToStore);
97
- return Promise.resolve();
98
- } catch {
99
- return {
100
- code: 'Storing_Error',
101
- message: 'Error storing value in custom storage',
102
- type: 'unknown_error',
103
- };
104
- }
105
- }
106
- if (storeType === 'sessionStorage') {
107
- try {
108
- await sessionStorage.setItem(key, valueToStore);
109
- return Promise.resolve();
110
- } catch {
111
- return {
112
- code: 'Storing_Error',
113
- message: 'Error storing value in session storage',
114
- type: 'unknown_error',
115
- };
116
- }
103
+ if (storeType === 'custom') {
104
+ const value = await config.custom.set(key, valueToStore);
105
+ return Promise.resolve(value ?? null);
117
106
  }
107
+
118
108
  try {
119
- await localStorage.setItem(key, valueToStore);
120
- return Promise.resolve();
109
+ await storageTypes[storeType].setItem(key, valueToStore);
110
+ return Promise.resolve(null);
121
111
  } catch {
122
- return {
123
- code: 'Storing_Error',
124
- message: 'Error storing value in local storage',
125
- type: 'unknown_error',
126
- };
112
+ return createStorageError(storeType, 'Storing');
127
113
  }
128
114
  },
129
- remove: async function storageSet() {
130
- if ('custom' in config) {
131
- return await config.custom.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);
132
119
  }
133
- if (storeType === 'sessionStorage') {
134
- 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');
135
126
  }
136
- return await localStorage.removeItem(key);
137
127
  },
138
- };
128
+ } as StorageClient<Value>;
139
129
  }
package/vite.config.ts CHANGED
Binary file