From e21793a4e5b20853c35c3ad98521b9fc69eb1a61 Mon Sep 17 00:00:00 2001 From: John Fewell Date: Thu, 26 Oct 2023 01:49:51 -0400 Subject: [PATCH] =?UTF-8?q?feat(persist-state):=20=F0=9F=94=A5=20Add=20cal?= =?UTF-8?q?lback=20to=20change=20state=20before=20save=20(#495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preStorageUpdate feature allows the modification of state before it is saved to storage. ✅ Closes: 490 --- docs/docs/features/persist-state.mdx | 34 ++++++++++++++++++- packages/mocks/src/index.ts | 1 + .../src/lib/persist-state.spec.ts | 32 +++++++++++++++++ .../persist-state/src/lib/persist-state.ts | 11 +++++- 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/docs/features/persist-state.mdx b/docs/docs/features/persist-state.mdx index 396c81c7..01a6509f 100644 --- a/docs/docs/features/persist-state.mdx +++ b/docs/docs/features/persist-state.mdx @@ -39,7 +39,8 @@ As the second parameter you should pass a `Options` object, which can be used to - `storage`: an Object with `setItem`, `getItem` and `removeItem` method for storing the state (required). - `source`: a method that receives the store and return what to save from it (by default - the entire store). -- `preStoreInit`: a method that run upon initializing the store with a saved value, used for any required modifications before the value is set. +- `preStoreInit`: a method that runs upon initializing the store with a saved value, used for any required modifications before the value is set. +- `preStorageUpdate`: a method that runs before saving the store, used for any required modifications before the value is set. - `key`: the name under which the store state is saved (by default - the store name plus a `@store` suffix). - `runGuard` - returns whether the actual implementation should be run. The default is `() => typeof window !== 'undefined'` @@ -113,3 +114,34 @@ persistState(todoStore, { source: () => todoStore.pipe(debounceTime(1000)), }); ``` + +## preStorageUpdate + +The `preStorageUpdate` option is a function that is called before the state is saved to the storage. It receives two parameters: `storeName` and `state`. The `storeName` is a string representing the name of the store, and `state` is the current state of the store. + +This function is useful when you want to modify the state before it is saved to the storage. For example, you might want to remove some sensitive data or transform the state in some way. + +The function should return the modified state which will be saved to the storage. If you don't return anything from this function, the original state will be saved. + +Here is an example of how to use `preStorageUpdate`: + +```ts +import { persistState, localStorageStrategy } from '@ngneat/elf-persist-state'; + +const preStorageUpdate = (storeName, state) => { + const newState = { ...state }; + if (storeName === 'todos') { + delete newState.sensitiveData; + } + return newState; +} + + +persistState(todoStore, { + key: 'todos', + storage: localStorageStrategy, + preStorageUpdate, +}); +``` + +In this example, the `preStorageUpdate` function removes the `sensitiveData` property from the state before it is saved to storage. \ No newline at end of file diff --git a/packages/mocks/src/index.ts b/packages/mocks/src/index.ts index 0a1d3337..f1fc7f2d 100644 --- a/packages/mocks/src/index.ts +++ b/packages/mocks/src/index.ts @@ -5,6 +5,7 @@ export interface Todo { id: number; title: string; completed: boolean; + sensitiveData?: string; } const { state, config } = createState(withEntities()); diff --git a/packages/persist-state/src/lib/persist-state.spec.ts b/packages/persist-state/src/lib/persist-state.spec.ts index 93397608..89267b34 100644 --- a/packages/persist-state/src/lib/persist-state.spec.ts +++ b/packages/persist-state/src/lib/persist-state.spec.ts @@ -127,4 +127,36 @@ describe('persist state', () => { new TypeError(`Cannot read properties of undefined (reading '_storage')`) ); }); + + it('should call preStorageUpdate and remove sensitive data before saving to storage', () => { + const storage: StateStorage = { + getItem: jest.fn().mockImplementation(() => of(null)), + setItem: jest.fn().mockImplementation(() => of(true)), + removeItem: jest.fn().mockImplementation(() => of(true)), + }; + + const preStorageUpdate = jest + .fn() + .mockImplementation((storeName, state) => { + const newState = { ...state }; + if (storeName === 'todos') { + delete newState.sensitiveData; + } + return newState; + }); + + const store = createEntitiesStore(); + persistState(store, { storage, preStorageUpdate }); + expect(preStorageUpdate).not.toHaveBeenCalled(); + + const todo = createTodo(1); + todo.sensitiveData = 'secret'; + store.update(addEntities(todo)); + expect(preStorageUpdate).toHaveBeenCalledTimes(1); + expect(preStorageUpdate).toHaveBeenCalledWith('todos', store.getValue()); + + const savedState = store.getValue(); + expect(savedState).not.toHaveProperty('sensitiveData'); + expect(storage.setItem).toHaveBeenCalledWith('todos@store', savedState); + }); }); diff --git a/packages/persist-state/src/lib/persist-state.ts b/packages/persist-state/src/lib/persist-state.ts index c7429c71..0ee98aa7 100644 --- a/packages/persist-state/src/lib/persist-state.ts +++ b/packages/persist-state/src/lib/persist-state.ts @@ -7,6 +7,10 @@ interface Options { storage: StateStorage; source?: (store: S) => Observable>>; preStoreInit?: (value: StoreValue) => Partial>; + preStorageUpdate?: ( + storeName: string, + state: Partial> + ) => Partial>; key?: string; runGuard?(): boolean; } @@ -54,7 +58,12 @@ export function persistState(store: S, options: Options) { const saveToStorageSubscription = merged.source!(store) .pipe( skip(1), - switchMap((value) => storage.setItem(merged.key!, value)) + switchMap((value) => { + const updatedValue = merged.preStorageUpdate + ? merged.preStorageUpdate(store.name, value) + : value; + return storage.setItem(merged.key!, updatedValue); + }) ) .subscribe();