1

I have "Entities" and "Entity Templates". The top level entity is tied to an Entity Template which is the "templateId1". From there an entity is tied to another entity in a path by using its id. I'm trying to end up with the following data structure in an efficient way:

const entities = [
  {
    id: 'xi32',
    name: 'Some name',
    path: ',templateId1,'
  },
  {
    id: 'x382',
    name: 'Some name 2',
    path: ',templateId1,xi32,'
  },
  {
    id: '2oxwo',
    name: 'Some name 3',
    path: ',templateId1,xi32,x382,'
  },
  {
    id: '2',
    name: '2',
    path: ',templateId1,'
  },
  {
    id: '2-2',
    name: '2-2',
    path: ',templateId1,2,'
  },
  {
    id: '3-3',
    name: '3-3',
    path: ',templateId1,3,'
  },
  {
    id: '3-3-3',
    name: '3-3-3',
    path: ',templateId1,3,3-3,'
  },
  {
    id: '3-3-3-3',
    name: '3-3-3-3',
    path: ',templateId1,3,3-3,3-3-3,'
  },
  {
    id: '3',
    name: '3',
    path: ',templateId1,'
  }
];

const desiredResult = [
      {
        id: 'xi32',
        name: 'Some name',
        path: ',templateId1,',
        children: [
          {
            id: 'x382',
            name: 'Some name 2',
            path: ',templateId1,xi32,',
            children: [
              {
                id: '2oxwo',
                name: 'Some name 3',
                path: ',templateId1,xi32,x382,',
                children: null
              }
            ]
          }
        ]
      },
      {
        id: '2',
        name: '2',
        path: ',templateId1,',
        children: [
          {
            id: '2-2',
            name: '2-2',
            path: ',templateId1,2,',
            children: null
          }
        ]
      },
      {
        id: '3',
        name: '3',
        path: ',templateId1,',
        children: [
          {
            id: '3-3',
            name: '3-3',
            path: ',templateId1,3,',
            children: [
              {
                id: '3-3-3',
                name: '3-3-3',
                path: ',templateId1,3,3-3,',
                children: [
                  {
                    id: '3-3-3-3',
                    name: '3-3-3-3',
                    path: ',templateId1,3,3-3,3-3-3,',
                    children: null
                  }
                ]
              }
            ]
          }
        ]
      }
    ];

Initial inspiration for the structure came from the MongoDB documentation:

https://docs.mongodb.com/manual/tutorial/model-tree-structures-with-materialized-paths/

I had a slightly different use case with the "Entity Template" being the top level parent, but within the "Entities" the use case is the same. Any insight is much appreciated.

sturoid
  • 2,501
  • 4
  • 20
  • 36
  • Is it something you want to accomplish in MongoDB or in JavaScript? – mickl Feb 20 '20 at 18:55
  • @mickl honestly I'm not sure. I like doing things at the DB level if I can because it's usually more performant, but with this in particular it would be nice to be able to do it in both scenarios. That way the API could return it flat if needed, and the tree could be created in code. – sturoid Feb 20 '20 at 19:01
  • @MisterJojo I tried using this https://stackoverflow.com/questions/6232753/convert-delimited-string-into-hierarchical-json-with-jquery but it assumes a different structure and honestly it's hard to parse what's going on in it for me. I'm still toying with that code. – sturoid Feb 20 '20 at 19:03
  • @MisterJojo my bad, that's a mistake. I'll edit it. – sturoid Feb 20 '20 at 19:09
  • @MisterJojo I guess yes it could but I have it sorted by "path" so the shortest paths come first. It would be nice to assume it could be in any order. I'm sorting it again in code just to be safe. – sturoid Feb 20 '20 at 19:15
  • Take a look [this](https://stackoverflow.com/questions/59245431/getting-ancestors-in-mongodb-using-tree-structure/59248036#59248036) – Valijon Feb 20 '20 at 20:00
  • @Valijon thanks I'm looking that over. – sturoid Feb 20 '20 at 20:04
  • @MisterJojo so the path is made up of the parent "ids". That makes it easy to do queries on it from what I can see so far. So if I want all children of a certain "entity" I could just say Entity.find({path: { $regex: `,${myId},`, options: 'i' }}) and it will grab all entities with the id in the path. I'll fix the path on there as well. It should be ',templateId1,'. – sturoid Feb 20 '20 at 20:51
  • if the source arrow is made of data in random order, it really complicates things! + what should you do if there is no parent in a branch of the tree? should we return an error list and ignore these children? – Mister Jojo Feb 20 '20 at 20:53
  • @MisterJojo yes I agree. So the way I get the initial list is I have an "Entity Template". From there I get all "Entities" that have the template id in their path. So all children present will be part of that top level Entity Template. Within the list itself, the top level Entity will be the ones that ONLY have the template id in the path. It's kind of acting as the foreign key to the Entity Template but makes it easier for querying than storing it separately so far. That being said, it's safe to assume that in the initial list, an entity either belongs to the template, or another entity. – sturoid Feb 20 '20 at 20:58

2 Answers2

2

I have done this... I have added a sort to prevent any key disorder.

const data = 
      [ { id: '1',     name: '1',     path: ',templateId1,'      } 
      , { id: '2',     name: '2',     path: ',templateId1,'      } 
      , { id: '2-1',   name: '2-1',   path: ',templateId1,2,'    } 
      , { id: '1-1',   name: '1-1',   path: ',templateId1,1,'    } 
      , { id: '1-1-1', name: '1-1-1', path: ',templateId1,1,1-1,'} 
      ]
let result  = []
  , parents = [ {children: result} ]
  ;
for (let elData of data.sort((a,b)=>a.id.localeCompare(b.id)))
  {
  let newEl = { ...elData, children: null }
    , level = (elData.id.match(/-/g)||'').length // count number of '-' 
    ;
  if (parents[level].children===null)
    { parents[level].children = [] }
  parents[level].children.push( newEl ) 
  parents[++level] = newEl
  }
  
console.log(JSON.stringify(result,0,2 )  )  // for testing
.as-console-wrapper { max-height: 100% !important; top: 0; }
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
  • This looks great. Much shorter than what I currently have. It needs to be tweaked a little because the relationship/depth isn't based on the "id" or "-", it's based on the "path". I'm fiddling with that now. – sturoid Feb 20 '20 at 23:04
  • It would be level = (elData.path.match(/,/g) || '').length - 2; The (- 2) is to get rid of the "," at the beginning and end of the string. – sturoid Feb 20 '20 at 23:05
  • Yeah man works great! Passed tests and everything with that change I put in the comment. Thanks a ton :). – sturoid Feb 20 '20 at 23:10
  • I'm going to finish the solution I have and I'll post it as well but I like yours. – sturoid Feb 20 '20 at 23:11
  • Yeah that's the only way I can see to make it work. An id could be anything. I just used "-" for the example. A real id I have is "5e4dcc5e8b660466ca1fe916". So we can't look to the id for any sort of reliable measure of depth/level, which is what it looks like you are trying to get there right? – sturoid Feb 20 '20 at 23:47
  • The id and the name can be anything. The path is made up of comma separated id's. I added some more data to the example and the tests still pass :). The logic seems to be pretty sound for what you did. I can't get it to break. – sturoid Feb 20 '20 at 23:57
  • Ok now I got it to break and I see why with what you were saying. I added some different data that makes the ids so they can't be relied on. My database just happened to have incrementing ids so somehow it passed with the tests before. With this data the tests break. If you look at the data in the question now, it reflects the changes. – sturoid Feb 21 '20 at 00:43
  • Ok thanks. I have one too I think. I'll throw it up here soon. – sturoid Feb 21 '20 at 02:34
1

Finally I managed to code this, with a lot of debugging because there are a lot of traps.

the idea is to make the json in several passes, counting each time the number of remaining elements.

if this number does not decrease from one pass to another, then the pgm sends an error and stops.

for each element to add we calculate the supposed parent getParentKey() which returns a table of the list of all its parents

You must then find the direct parent in the table, using this list, starting with the root, if the parent does not exist then the element is kept in a table and will be retried next.

const entities = 
      [ { id: 'xi32',    name: 'Some name',   path: ',templateId1,'            } 
      , { id: 'x382',    name: 'Some name 2', path: ',templateId1,xi32,'       } 
      , { id: '2oxwo',   name: 'Some name 3', path: ',templateId1,xi32,x382,'  } 
      , { id: '2',       name: '2',           path: ',templateId1,'            } 
      , { id: '2-2',     name: '2-2',         path: ',templateId1,2,'          } 
      , { id: '3-3',     name: '3-3',         path: ',templateId1,3,'          } 
      , { id: '3-3-3',   name: '3-3-3',       path: ',templateId1,3,3-3,'      } 
      , { id: '3-3-3-3', name: '3-3-3-3',     path: ',templateId1,3,3-3,3-3-3,'} 
      , { id: '3',       name: '3',           path: ',templateId1,'            } 
      ]; 
const Result  = []
  ;
let unDone = []
  , source = entities
  ;
let cpt = 0  // just for stoping infinite loop on error
  ;
do
  {
  unDone = setResult( source, unDone.length )
  source = unDone;
  if (++cpt > 10) throw 'mince! something is rotten in the state of Denmark...'
  }
while
  (unDone.length>0)
  ;   
/* --------------------------------------------------------*/
console.log( 'result===', JSON.stringify(Result,0,2 ) )
/* --------------------------------------------------------*/

function setResult( arrayIn, nb_rej )
  {
  let orphans = [];
  for (let elData of arrayIn)
    {
    let newEl = { ...elData, children: null }  
      , parAr = getParentKey(elData.path)

    if (parAr.length===0)
      { Result.push(newEl) }
    else
      {
      let resParent = Result;
      do
        {
        let rech = parAr.pop()
          , fPar = resParent.find(treeElm=>(treeElm.id===rech.id && treeElm.path===rech.path))
          ;
        if (fPar) 
          {
          if (fPar.children===null)
            fPar.children = []
            ;
          resParent = fPar.children
          }
        else  //  throw `parent element not found : id:'${rech.id}', path:'${rech.path}'`  ;
          {
          orphans.push( { ...elData }   )
          resParent = null
          parAr.length = 0
          }  
        }
      while
        (parAr.length>0)
        ;
      if (resParent) resParent.push(newEl);
      }
    }
  if ( orphans.length>0 && orphans.length == nb_rej )
    throw ` ${nb_rej} children element(s) without parent !'`;

  return orphans
  }

function getParentKey( path )
  {                          // return array of parent element
  let rep = []
    , par = path
    , lev, bKey, xCom, idK;
  do
    {
    bKey = par.substring(0, par.lastIndexOf(','))  // remove last ','
    lev  = bKey.match(/,/g).length -1
    if (lev>0)
      {
      xCom = bKey.lastIndexOf(',')
      par  = bKey.substring(0, xCom) +',' 
      idK  = bKey.substring(++xCom)
      rep.push({ path:par, id:idK })
    } }
  while
    (lev>0)
  return rep
  }
.as-console-wrapper { max-height: 100% !important; top: 0; }
Mister Jojo
  • 20,093
  • 6
  • 21
  • 40
  • This is AWESOME! Thank you :). I'm reading through and trying to understand the code but it passes all tests. I have a solution as well but I was stuck at updating the tree with the children. I might finish that as well and post it but this works so maybe not. Thanks again! – sturoid Feb 21 '20 at 17:41