angus-c / just

A library of dependency-free JavaScript utilities that do just one thing.
https://anguscroll.com/just
MIT License
6k stars 209 forks source link

just-merge is not working as expected #580

Open R35007 opened 1 month ago

R35007 commented 1 month ago

Sample Input

const obj1 = {
      "appName": "React",
      "tags": ["react", 'mfe'],
      "fields": {
        "appName": {"type": "checkbox"},
        "template": {"type": "textbox","options": [{"label": "opt1"}, {}, {"label": "opt3","value": "value3"}, {"label": "opt4","value": "value4"},{"label": "opt5","value": "value5"}]},
        "projectName": {"type": "textbox"}
      }
    }

const obj2 = {
      "appName": "React",
      "tags": ["frontend"],
      "fields": {
        "appName": {"type": "checkbox","value": true,"label": "Confirm"},
        "template": {"type": "radio","options": [{"value": "value1"},{"label": "opt2","value": "value2"}, {}, {"label": "opt4","value": "value4"}]},
        "skipInstall": {"type": "checkbox","label": "Should Skip Install ?"}
      }
    }

Expected Output

{
  "appName": "React",
  "tags": ["react", "frontend", "mfe"],
  "fields": {
    "appName": { "type": "checkbox", "value": true, "label": "Confirm" },
    "template": {
      "type": "radio",
      "options": [
        { "label": "opt1", "value": "value1" },
        { "label": "opt2", "value": "value2" },
        { "label": "opt3", "value": "value3" },
        { "label": "opt4", "value": "value4" },
        { "label": "opt5", "value": "value5" }
      ]
    },
    "projectName": { "type": "textbox" },
    "skipInstall": { "type": "checkbox", "label": "Should Skip Install ?" }
  }
}

But What I get is

{
  "appName": "React",
  "tags": ["frontend"],
  "fields": {
    "appName": { "type": "checkbox", "value": true, "label": "Confirm" },
    "template": {
      "type": "radio",
      "options": [
            { "value": "value1" }, 
            { "label": "opt2", "value": "value2" }, 
            {}, 
            { "label": "opt4", "value": "value4" }
       ]
    },
    "skipInstall": { "type": "checkbox", "label": "Should Skip Install ?" }
  }
}
uncenter commented 1 month ago

I believe you want to be using just-extend, not just-merge. Try extend(true, obj1, obj2).

R35007 commented 1 month ago

I believe you want to be using just-extend, not just-merge. Try extend(true, obj1, obj2).

Tried that as well. still not getting the expected result

uncenter commented 1 month ago

What is the difference between expected and what you are getting exactly?

R35007 commented 1 month ago
{
  "appName": "React",
  "tags": ["frontend", "mfe"],
  "fields": {
    "appName": { "type": "checkbox", "value": true, "label": "Confirm" },
    "template": {
      "type": "radio",
      "options": [
        { "label": "opt1", "value": "value1" },
        { "label": "opt2", "value": "value2" },
        { "label": "opt3", "value": "value3" },
        { "label": "opt4", "value": "value4" },
        { "label": "opt5", "value": "value5" }
      ]
    },
    "projectName": { "type": "textbox" },
    "skipInstall": { "type": "checkbox", "label": "Should Skip Install ?" }
  }
}

Have already share u what I get and what is expected. the arrays are not merging. its just replacing it on index.

R35007 commented 1 month ago

This is what I get

{
  "appName": "React",
  "tags": ["frontend"],
  "fields": {
    "appName": { "type": "checkbox", "value": true, "label": "Confirm" },
    "template": {
      "type": "radio",
      "options": [{ "value": "value1" }, { "label": "opt2", "value": "value2" }, {}, { "label": "opt4", "value": "value4" }]
    },
    "skipInstall": { "type": "checkbox", "label": "Should Skip Install ?" }
  }
}
uncenter commented 1 month ago

Ah yeah I was also noticing that! But in your "expected" version you left a bit with that behavior so I thought that is what you wanted.

R35007 commented 1 month ago
const isArray = (val: unknown) => !!(val && typeof val === 'object' && Array.isArray(val));
const isPlainObject = (val: unknown) => !!(val && typeof val === 'object' && !isArray(val));
const isCollection = (val: unknown): val is Array<object> => !!(val && isArray(val) && val.length && val.every(isPlainObject));
const isMatrix = (val: unknown): val is Array<unknown[]> => !!(val && isArray(val) && val.length && val.every(isArray));
const isPrimitive = (val: unknown) => !!(val && typeof val !== 'object');
const getValuesByKey = (args: object[], keyName: string) =>
  args
    .map((obj) =>
      Object.entries(obj)
        .filter(([key]) => key === keyName)
        .map(([, value]) => value)
    )
    .flat();
const getValuesByIndex = (args: Array<unknown[]>, index: number) => args.map((arr) => arr[index]).flat();

function mergeDeep(...args: unknown[]): object | unknown[] {
  if (isCollection(args)) {
    const allKeys = args.map((obj) => Object.entries(obj).map(([key]) => key)).flat();
    const entries = allKeys.map((key) => {
      const values = getValuesByKey(args as object[], key);
      return values.every(isPrimitive) ? [key, values[values.length - 1]] : [key, mergeDeep(...values)];
    });
    return Object.fromEntries(entries);
  }

  if (isMatrix(args)) {
    const maxArrayLength = args.reduce((acc, item) => (acc >= item.length ? acc : item.length), 0);
    return [...Array(maxArrayLength).keys()].map((index) => mergeDeep(...getValuesByIndex(args, index))).flat();
  }

  return [...new Set(args)].filter(Boolean);
}

export default mergeDeep;

This is what I have comeup with. Feel free to use it for merge or expand