1

Stackblitz

In @ngrx/data doing an optimistic delete (of our Hero "Ant-Man") causes changeState to be updated as shown below:

{
  "entityCache": {
    "Hero": {
      "ids": [1, 2, 3, 5, 6],
      "entities": {
        "1": {
          "id": 1,
          "name": "Spiderman",
          "power": 1
        },
        "2": {
          "id": 2,
          "name": "Thor",
          "power": 5
        },
        "3": {
          "id": 3,
          "name": "Hulk",
          "power": 6
        },
        "5": {
          "id": 5,
          "name": "Iron Man",
          "power": 9
        },
        "6": {
          "id": 6,
          "name": "Thanos",
          "power": 10
        }
      },
      "entityName": "Hero",
      "filter": "",
      "loaded": true,
      "loading": true,
      "changeState": {
        "4": {
          "changeType": 2,
          "originalValue": {
            "id": 4,
            "name": "Ant-Man",
            "power": 7
          }
        }
      }
    }
  }
}

Using the effect below I've fired an UNDO_ONE when the delete fails due to a http request error:

  deleteError$ = createEffect(() => {
    return this.actions$.pipe(
      ofEntityType("Hero"),
      ofEntityOp([EntityOp.SAVE_DELETE_ONE_ERROR]),
      map(action => {
        const id = action.payload.data.originalAction.payload.data;
        const options: EntityActionOptions = {
            // tried various values
        }
        return new EntityActionFactory().create( <-----------------------dispatch UNDO_ONE action-----------
          "Hero",
          EntityOp.UNDO_ONE,
          id,
          options
        );
      })
    );
  });

Question: Should dispatching an UNDO_ONE action revert the changeState
i.e. remove the changes to this part of the entities state caused by a delete action?
If so, how do you correctly dispatch an UNDO_ONE and what arguments are required?
I've explored different values for both data and options for the EntityActionFactory.create() method:

EntityActionFactory.create<P = any>(entityName: string, entityOp: EntityOp, data?: P, options?: EntityActionOptions): EntityAction<P>

Here I'm doing an optimistic delete and on a SAVE_DELETE_ONE_ERROR dispatching an UNDO_ONE action via an effect.

When I swap out UNDO_ONE for UNDO_ALL changeState does revert back to {} which gives me cause to think changeState should revert back to {} given we're cancelling the delete.

Andrew Allen
  • 6,512
  • 5
  • 30
  • 73
  • @Mickers you're missing the point of the question. @ngrx/data covers these with its out of the box actions (SAVE_DELETE_ONE, SAVE_DELETE_ONE_ERROR, SAVE_DELETE_ONE_SUCCESS) that occur when using its DefaultDataService for example – Andrew Allen Feb 05 '20 at 10:05

1 Answers1

1

According to the documentation here, it should :

The undo operations replace entities in the collection based on information in the changeState map, reverting them their last known server-side state, and removing them from the changeState map. These entities become "unchanged."

In order to overcome this issue, you can create a metaReducer which removes the relevant modifications remaining in the changeState after an undo action. Here is the content of my entity-metadata.ts with the relevant metareducer.

import { EntityMetadataMap, EntityDataModuleConfig, EntityCache } from '@ngrx/data';
import { MetaReducer, ActionReducer, Action } from '@ngrx/store';

const entityMetadata: EntityMetadataMap = {};

const pluralNames = {};

const objectWithoutProperties = (obj, keys) => {
  const target = {};
  for (const i in obj) {
    if (keys.indexOf(i) >= 0) { continue; }
    if (!Object.prototype.hasOwnProperty.call(obj, i)) { continue; }
    target[i] = obj[i];
  }
  return target;
};

function revertStateChanges(reducer: ActionReducer<any>): ActionReducer<any> {
  return (state, action: any) => {
    if (action.type.includes('@ngrx/data/undo-one')) {
      
      //  Note that you need to execute the reducer first if you have an effect to add back a failed removal
      state = reducer(state, action);

      const updatedChangeState = objectWithoutProperties(state[action.payload.entityName].changeState, [action.payload.data.toString()]);
      const updatedState = {
        ...state,
        [action.payload.entityName]: {
          ...state[action.payload.entityName],
          changeState: updatedChangeState
        }
      };

      return reducer(updatedState, action);
    }

    return reducer(state, action);
  };
}

const entityCacheMetaReducers: MetaReducer<EntityCache, Action>[] = [revertStateChanges];

export const entityConfig: EntityDataModuleConfig = {
  entityMetadata,
  pluralNames,
  entityCacheMetaReducers
};

There might be a better way of writing this code (in particular the way I handled the override changeState property) but for my case it proved to work.

Moreover, it might need some updates in order to handle the different undo cases, as, when I wrote it I just needed to make it work for an undo concerning a delete action, where action.payload.data is the entity id.

vbourdeix
  • 128
  • 5
  • I've submitted a PR requested. When I get chance I'll look over this answer in detail to see how viable as a temporary fix this is. – Andrew Allen Feb 06 '20 at 19:35