jsondiffpatch icon indicating copy to clipboard operation
jsondiffpatch copied to clipboard

Patching an array using the objectHash

Open hgrubst opened this issue 7 years ago • 4 comments

It looks like currently when patching an array, only the index is used but not the object hash. I have run into the following scenario which can be resumed by the following steps

  • my instance of jsondiffpatch has a custom objectHash function looking for object 'code' property
  • create a diff on 2 arrays : let d = jsondiffpatch.diff([{ code: '1' }, { code: '2' }], [{ code: '3' }, { code: '1' }, { code: '2' }]) //res { '0': [{ code: '3' }], _t: 'a' }
  • apply this diff in reverse to the same array but which has been retrieved (from a db) and is now not in the same order : jsondiffpatch.unpatch([{ code: '1' }, { code: '2' }, { code: '3' }], { '0': [{ code: '3' }], _t: 'a' }) //res [ { code: '2' }, { code: '3' } ] As we can see the 1st element of the array got removed in the operation. I would've liked for the object with {code:3} to get removed following the usage of the objectHash as was done during the diffing.

Is this me misusing the library or is there any way to achieve this ? Thanks

hgrubst avatar Oct 18 '18 05:10 hgrubst

Ok so I've managed to implement a new patcher that takes care of the scenario above. I've basically copied the initial patchFilter$2 method and changed the relevant part so that it removes objects from arrays using the provided hash function. One thing I need is to access PatchContext as it does in the original function : child = new PatchContext(context.left[modification.index], modification.delta); Unfortunately, it is not exposed in the exports. Any idea how I could go around this ?

For those that want something similary here is my code. Happy to provide a PR if needed:

var arraysByHashPatchFilter = function (context) {
    // if (context.delta && context.delta._t Array.isArray(context.delta) && context.delta[2] === NUMERIC_DIFFERENCE) {
    //     context.setResult(context.left + context.delta[1]).exit();
    // }
    if (!context.nested) {
        return;
    }
    if (context.delta._t !== 'a') {
        return;
    }
    var index = void 0;
    var index1 = void 0;

    var delta = context.delta;
    var array = context.left;

    // first, separate removals, insertions and modifications
    var toRemoveOverridenByHash = [];
    var toRemove = [];
    var toInsert = [];
    var toModify = [];
    for (index in delta) {
        if (index !== '_t') {
            if (index[0] === '_') {
                // removed item from original array
                if (delta[index][2] === 0 || delta[index][2] === ARRAY_MOVE) {
                    //find the object by hash instead of by index
                    //if we find an index to remove by hash we create an entry in toRemoveOverridenByHash to map that index to the original one
                    //this is used later in the case of moves to know in which position to reinsert the item if necessary
                    if (context.options.objectHash) {
                        let indexOfObjectToRemove = array.findIndex(e => context.options.objectHash(e) === context.options.objectHash(delta[index][0]))
                        if (indexOfObjectToRemove !== -1) {
                            toRemove.push(indexOfObjectToRemove);
                            toRemoveOverridenByHash.push({
                                indexToRemoveCalculatedByIndex: parseInt(index.slice(1), 10),
                                indexToRemoveCalculatedByHash: indexOfObjectToRemove
                            })
                        }
                    } else {
                        toRemove.push(parseInt(index.slice(1), 10));
                    }

                } else {
                    throw new Error('only removal or move can be applied at original array indices,' + (' invalid diff type: ' + delta[index][2]));
                }
            } else {
                if (delta[index].length === 1) {
                    // added item at new array
                    toInsert.push({
                        index: parseInt(index, 10),
                        value: delta[index][0]
                    });
                } else {
                    // modified item at new array
                    toModify.push({
                        index: parseInt(index, 10),
                        delta: delta[index]
                    });
                }
            }
        }
    }

    // remove items, in reverse order to avoid sawing our own floor
    toRemove = toRemove.sort(compare.numerically);
    for (index = toRemove.length - 1; index >= 0; index--) {
        let indexToRemove = toRemove[index];
        let overridenByHashIndex = toRemoveOverridenByHash.find(o => o.indexToRemoveCalculatedByHash === indexToRemove);
        var indexDiff = delta[`_${overridenByHashIndex ? overridenByHashIndex.indexToRemoveCalculatedByIndex : indexToRemove}`];
        var removedValue = array.splice(indexToRemove, 1)[0];
        if (indexDiff[2] === ARRAY_MOVE) {
            // reinsert later
            toInsert.push({
                index: indexDiff[1],
                value: removedValue
            });
        }
    }

    // insert items, in reverse order to avoid moving our own floor
    toInsert = toInsert.sort(compare.numericallyBy('index'));
    var toInsertLength = toInsert.length;
    for (index = 0; index < toInsertLength; index++) {
        var insertion = toInsert[index];
        array.splice(insertion.index, 0, insertion.value);
    }

    // apply modifications
    var toModifyLength = toModify.length;
    var child = void 0;
    if (toModifyLength > 0) {
        for (index = 0; index < toModifyLength; index++) {
            var modification = toModify[index];
            child = new PatchContext(context.left[modification.index], modification.delta);
            context.push(child, modification.index);
        }
    }

    if (!context.children) {
        context.setResult(context.left).exit();
        return;
    }
    context.exit();
};`

hgrubst avatar Oct 19 '18 05:10 hgrubst

Ok so for the PatchContext issue I have done child = new context.constructor(context.left[modification.index], modification.delta) which seems to solve it.

hgrubst avatar Oct 19 '18 05:10 hgrubst

Would it solve #269?

Would be nice to have a pull request as well as more details on how to implement ourselves in the meantime :) Like, where do you use your arraysByHashPatchFilter()?

Thanks!

gahabeen avatar Oct 27 '19 10:10 gahabeen

I cant see a real problem with #269 actually. This issue is about unpatching once a delta has already been generated. To come back to your question about how to use this, you need to plug the arraysByHashPatchFilter function as explained in the plugins doc here : https://github.com/benjamine/jsondiffpatch/blob/master/docs/plugins.md In summary the following should make it work : (<any>arraysByHashPatchFilter).filterName = 'arraysByHash'; (<any>jsondiffpatch).processor.pipes.patch.before('arrays', arraysByHashPatchFilter);

hgrubst avatar Jul 27 '20 00:07 hgrubst