Merging partial objects while avoiding path collisions and ordering array indexes

I have been creating a small library to copy parts of deeply nested objects through path strings that supports wildcards. For that I list all paths and values actually found as ObjectPartials and merge-partials these are merged while avoiding path collisions and ordering array indexes. This review is only about this part.

The whole merge-partials.js seems too contrived and confusing. I wanted to have smaller, clearer functions that don’t rely so much on side-effects, as here the ObjectPartials passed to the functions are being changed. Maybe there is even a way simpler solution that I am not seeing.

object-partial.js

const set = require('lodash.set');  const InvalidArgError = require('./errors/invalid-arg-error'); const { isUndefined } = require('./utils');  module.exports = class ObjectPartial {   /**    * Creates a new ObjectPartial with the given path and its value    * @param {Array} path    * @param {any} value    */   constructor(path, value) {     if (!Array.isArray(path) || !path.length || isUndefined(value)) {       throw new InvalidArgError(         'No valid path or missing value for ObjectPartial'       );     }     this.path = path.slice();     this.value = value;   }    getPath() {     return this.path;   }    getValue() {     return this.value;   }    /**    * Merge the partial into a new or an existing object    * @param {*} object Optional object to merge the partial into    * @returns Resulting object    */   mergeToObject(object = {}) {     return set(object, this.path, this.value);   }    static createFromObject(object, path) {     if (!path || !Array.isArray(path) || isUndefined(object)) {       throw new InvalidArgError(         'Missing path or object for ObjectPartial.createFromObject'       );     }     let curValue = object;      for (let i = 0; i < path.length; i++) {       if (isUndefined(curValue[path[i]])) {         return null;       }       curValue = curValue[path[i]];     }      return new ObjectPartial(path, curValue);   } }; 

merge-partials.js

const { isUndefined, pushAllUnique } = require('./utils'); const InvalidArgError = require('./errors/invalid-arg-error');  /**  * Creates a map of all the keys on current partials level and its subsequent keys  * @param {ObjectPartial[]} partials  * @param {Number} pathIndex  * @returns Map of each key occurance (*key* (key, indices[], nextKeys[])) to the partials and next keys  */ function createPossibleCollisionsKeyMap(partials, pathIndex) {   const result = { possibleColisionKeys: {}, hasNextLevel: false };    for (let i = 0; i < partials.length; i++) {     const partialPath = partials[i].getPath() || [];     const key = partialPath[pathIndex];     const nextKey = partialPath[pathIndex + 1];      if (!isUndefined(key)) {       if (isUndefined(result.possibleColisionKeys[key])) {         result.possibleColisionKeys[key] = { key, indices: [], nextKeys: [] };       }       result.possibleColisionKeys[key].indices.push(i);       result.possibleColisionKeys[key].nextKeys.push(nextKey);     }     if (!isUndefined(nextKey)) {       result.hasNextLevel = true;     }   }    return result; }  function hasPossibleKeyCollision(indices, nextKeys) {   const hasMultiple = indices.length > 1;   const hasNextUndefined = nextKeys.findIndex(isUndefined) > -1;   const hasNextNonZeroIndices =     nextKeys.filter(nextKey => Number.isInteger(nextKey) && nextKey != 0)       .length > 0;    return (hasNextUndefined && hasMultiple) || hasNextNonZeroIndices; }  /**  * Solves possible key collisions and normalizes array indices (order from 0)  * @hasSideEffects  * @param {ObjectPartial[]} partials  * @param {Array} pathIndex  * @param {Object} indices.nextKeys possible colision key map  */ function solveKeyCollision(partials, pathIndex, { indices, nextKeys }) {   if (!hasPossibleKeyCollision(indices, nextKeys)) {     return;   }   const indicesTransform = [];   let curNextIndex = 0;    for (let i = 0; i < indices.length; i++) {     const partialIndex = indices[i];     const partialNextKey = nextKeys[i];     const partialPath = partials[partialIndex].getPath();      if (isUndefined(partialNextKey)) {       partialPath.splice(pathIndex + 1, 0, curNextIndex);       curNextIndex += 1;     } else if (Number.isInteger(partialNextKey)) {       let newIndex = indicesTransform[partialNextKey];        if (isUndefined(newIndex)) {         newIndex = curNextIndex;         curNextIndex += 1;         indicesTransform[partialNextKey] = newIndex;       }       partialPath[pathIndex + 1] = newIndex;     }   } }  /**  * Handles the possible collisions in current section of the object partials  * @param {ObjectPartial[]} partials  * @param {*} scanMaps (curLevel, partialIndices[])  * @returns (hasNextLevel, uniqueKeys)  */ function handlePathColisions(partials, { curLevel, partialIndices = [] }) {   const thesePartials = partials.filter((_partial, index) =>     partialIndices.includes(index)   );   const { possibleColisionKeys, hasNextLevel } = createPossibleCollisionsKeyMap(     thesePartials,     curLevel   );   const uniqueKeys = [];    Object.keys(possibleColisionKeys).forEach(key => {     solveKeyCollision(thesePartials, curLevel, possibleColisionKeys[key]);     uniqueKeys.push(possibleColisionKeys[key].key);   });    return { hasNextLevel, uniqueKeys }; }  function createDummyScanMap(partials, level) {   return {     curLevel: level,     partialIndices: partials.map((partial, index) => {       return {         path: partial.getPath(),         index       };     })   }; }  /**  * Gets a part of the ObjectPartials to be checked for possible path collisions  * @param {ObjectPartial[]} partials  * @param {Number} level  * @param {string} parentKey  * @returns {Object} Object composed of (curLevel,parentKey,partialIndices[] (path,index))  */ function getScanMap(partials, level, parentKey) {   const result = createDummyScanMap(partials, level);    if (isUndefined(parentKey)) {     result.partialIndices = result.partialIndices       .filter(value => !isUndefined(value.path[0]))       .map(value => value.index);   } else {     result.partialIndices = result.partialIndices       .filter(         value =>           value.path[level - 1] === parentKey && !isUndefined(value.path[level])       )       .map(value => value.index);   }    return result; }  /**  * Prepares object partials to merge contents and normalize array indexes  * @hasSideEffects  * @param {ObjectPartial[]} partials  */ /* eslint-disable no-loop-func */ function mergePartials(partials) {   if (!partials || !Array.isArray(partials) || !partials.length) {     throw new InvalidArgError('Missing ObjectPartial list for mergePartials()');   }   let hasNextLevel = true;   let curLevel = 0;   let parentKeys;   let scanMaps;   let tempResult;    while (hasNextLevel) {     if (curLevel === 0) {       scanMaps = [getScanMap(partials, 0)];     } else {       scanMaps = parentKeys.map(pKey => getScanMap(partials, curLevel, pKey));     }     parentKeys = [];      for (let i = 0; i < scanMaps.length; i++) {       tempResult = handlePathColisions(partials, scanMaps[i]);       pushAllUnique(parentKeys, tempResult.uniqueKeys);       hasNextLevel = false || tempResult.hasNextLevel;     }     curLevel += 1;   } }  module.exports = mergePartials; 

I did do some tests for this that are passing and here is an example of input and output ObjectPartials. Hope the purpose of the whole thing is clear, let me know if I missed something.

  mergePartials([     new ObjectPartial(['A', 3, 'BA'], 'example'),     new ObjectPartial(['A', 3, 'B'], 123),     new ObjectPartial(['A', 4, 'B'], 456),     new ObjectPartial(['D'], 'yay'),     new ObjectPartial(['D', 0], 'nay')   ]);   /* --> [     new ObjectPartial(['A', 0, 'BA'], 'example'),     new ObjectPartial(['A', 0, 'B'], 123),     new ObjectPartial(['A', 1, 'B'], 456),     new ObjectPartial(['D', 0], 'yay'),     new ObjectPartial(['D', 1], 'nay')   ] */