-1

I am getting my ahead around a problem.

I have a JS object which as following:

{
  obj:
    {
      o1:
        {
          id: "o1",
          name: "o1",
          fields:
            {
              f1: { id: "o1:f1", type: { id: "o5" } },
              f2: { id: "o1:f2", type: { id: "o2" } },
            },
        },
      o2:
        {
          id: "o2",
          name: "o2",
          fields: { f1: { id: "o2:f1", type: { id: "o5" } } },
        },
      o5: { id: "o5", name: "o5" },
    },
}

Within the obj, I have a set of properties deeply nested, each one with an ID which is unique. I want to add, under each parent object (o1, o2, etc), a property from which contains who is referencing the object. A result should look like this:

{
  obj:
    {
      o1:
        {
          id: "o1",
          name: "o1",
          fields:
            {
              f1: { id: "o1:f1", type: { id: "o5" } },
              f2: { id: "o1:f2", type: { id: "o2" } },
            },
        },
      o2:
        {
          id: "o2",
          name: "o2",
          fields: { f1: { id: "o2:f1", type: { id: "o5" } } },
          from: ["o1:f2"],
        },
      o5: { id: "o5", name: "o5", from: ["o1:f1", "o2:f1"] },
    },
}

The from is populated with the id of any fields within each object.

I considered using Ramda which can offer a path or Lodash with a find but I can't solve anyway.

Ste
  • 283
  • 1
  • 15

1 Answers1

0

One way to do this is to do a first pass at the object to create something like this:

{o5: ["o1:f1","o2:f1"], o2: ["o1:f2"]}

Then we can simply recursively visit the tree, adding the appropriate from node where we have an element.

My version creates this in a round-about way, and I'm quite sure we can improve it. Essentially, we scan the input object with gather, and create a version that looks like this:

[{o5: "o1:f1"}, {o2: "o1:f2"}, {o5: "o2:f1"}]

Then using a simple groupBy function and an Object .entries / Object .fromEntries dance, we turn this into the above. Then we pass this along with the input to a rebuild function which immutably adds this to the input object.

This version looks like this:

const groupBy = (fn) => (xs) => xs .reduce (
  (a, x, _, __, k = fn (x)) => ((a[k] = a[k] || []), a[k] .push(x), a), {}
)

const gather = (o, res = [], {fields = {}, id = '', type = {}, ...rest} = o) =>
  Object (o) !== o
    ? res
  : type .id 
    ? res .concat ({[type.id]: id})
  : Object .values (fields) .length
    ? Object .values (fields) .flatMap (fld => gather (fld, res))
  : Object .values (rest) . length
    ? Object .values (rest) .flatMap (fld => gather (fld, res))
  : res

const rebuild = (o, from) => // `from` looks like {o5: ["o1:f1","o2:f1"], o2: ["o1:f2"]}
  Object (o) !== o
    ? o
  : 'id' in o && o ['id'] in from 
    ? {...o, from: from [o ['id']]}
  : Object .fromEntries (Object .entries (o) 
      .filter (([k]) => k !== 'type') .map (([k, v]) => [k, rebuild (v, from)]
    ))

const addFrom = (o) => rebuild (
  o,
  Object .fromEntries (Object .entries (
    groupBy (x => x .at (0)) (gather (input) .flatMap (Object .entries)))
      .map (([k, vs]) => [k, vs.map(x => x .at(-1))]
  ))
)

const input = {obj: {o1: {id: "o1", name: "o1", fields: {f1: {id: "o1:f1", type: {id: "o5" }}, f2: {id: "o1:f2", type: {id: "o2" }}}}, o2: {id: "o2", name: "o2", fields: {f1: {id: "o2:f1", type: {id: "o5"}}}}, o5: {id: "o5", name: "o5"}}}

console .log (addFrom (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

This is working code, but I think it should be updated by fixing gather to directly collect the format needed by rebuild. If I had more time, I might try that, but for the moment, I leave this as an exercise for the reader.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103