Parse through an objects and all it's descendents to output split up data structure. Might involce recursive?

Viewed 58

I am dealing with a complicated NoSQL database, and my goal is to parse the data within it, separate each level of childrens keys, and then use these keys to label columns on a grid. In this way I can visually show a documents Parent -> Child -> Child -> Child relationships.

It might be easier for my to describe the data and the task by showing the data and the expected output.

Please keep in mind this is mock data but the structure and the problem will be seen here.

[
    {
        "customer_id": 1,
        "customer_name": "John",
        "customer_phone": "720-222-1111",
        "orders": [
            {
                "order_id": 1,
                "total": 500,
                "ordered_from": "website",
                "products": [
                    {
                        "product_id": 1,
                        "product_price": 400,
                        "product_name": "The Blaster",
                        "product_description": "Blasts everyone away! Fun in the pool"
                    },
                    {
                        "product_id": 2,
                        "product_price": 100,
                        "product_name": "Water",
                        "product_description": "Average H20, delivered to your doorstep",
                        "product_attributes": [
                            {
                                "name": "Addon",
                                "color": "Blue"
                            }
                        ]
                    }
                ]
            },
            {
                "order_id": 2,
                "total": 240,
                "ordered_from": "app",
                "geolocation": "California",
                "coupon_code": "5X23A",
                "products": [
                    {
                        "product_id": 2
                    }
                ]
            }
        ]
    },
    {
        "customer_id": 1,
        "customer_name": "Alice",
        "customer_address": "23 Main Street",
        "customer_zipcode": "15234",
        "orders": [
            {
                "order_id": 4,
                "total": 100,
                "ordered_from": "website",
                "products": [
                    {
                        "product_id": 1,
                        "product_price": 100,
                        "product_name": "Fins",
                        "category": "Water"
                    }
                ]
            },
            {
                "order_id": 2,
                "total": 240,
                "ordered_from": "app",
                "geolocation": "California",
                "coupon_code": "5X23A",
                "products": [
                    {
                        "product_id": 2
                    }
                ]
            }
        ]
    },
    {
        "customer_id": 1,
        "customer_name": "Colin",
        "customer_gender": "Male",

        "orders": [
            {
                "order_id": 1,
                "total": 500,
                "ordered_from": "website",
                "products": [
                    {
                        "product_id": 1,
                        "product_price": 400,
                        "product_name": "The Blaster",
                        "product_description": "Blasts everyone away! Fun in the pool"
                    },
                    {
                        "product_id": 2,
                        "product_price": 100,
                        "product_name": "Water",
                        "product_description": "Average H20, delivered to your doorstep",
                        "product_attributes": [
                            {
                                "name": "Addon",
                                "color": "Blue"
                            }
                        ]
                    }
                ]
            },
            {
                "order_id": 2,
                "total": 240,
                "ordered_from": "app",
                "geolocation": "California",
                "coupon_code": "5X23A",
                "products": [
                    {
                        "product_id": 2
                    }
                ]
            }
        ]
    }
]

Notice:

  • Here we have three NoSql Documents, the parent level objects are customers
  • the data within these objects are not standardized. One customer has a "customer_phone" key, while another doesnt. One customer has a "customer_address" key, while the other doesnt.
  • The same point continues onto the next key of "orders" which is an array of also none-standardized objects. One order has a "geolocation" key, while the other doesn't.
  • This non-standardization of data carries all the way down to the third child, "product_attributes", some products have this key, some don't.

My objects is to get every single possible key in every level of children.

So my expected output would be something like this.

Note: the value in this key:value pair doesn't matter at all. I am only using this object for the key, the value could be literally anything.

[
    {
        "KEY":"parent",
        "customer_id":true,
        "customer_phone":true,
        "customer_zipcode":true,
        "customer_address":true,
        "customer_gender":true,
        "customer_name":true,
        "orders":true,
    },
    {
        "KEY":"parent.orders",
        "order_id":true,
        "total":true,
        "ordered_from":true,
        "geolocation":true,
        "coupon_code":true,
        "products":true,
    },
    {
        "KEY":"parent.orders.products",
        "product_id":true,
        "product_price":true,
        "product_name":true,
        "product_description":true,
        "product_attributes":true,
        "category":true,
    },
    {
        "KEY":"parent.orders.products.attributes",
        "name":true,
        "color":true
    }
]

Once I have this output, I could easily loop through it and use the keys to create columns on a grid component.

My attempt at a solution:

Pass the first shared JSON into this function

    traverseTree(rawData: any, assembled: any[]): any {
        let AllKeys: any = {};
        let results = [];
        rawData.forEach((question: any) => {
            for (const [key, value] of Object.entries(question)) {
                AllKeys[key] = value;

                if (
                    Array.isArray(value) &&
                    value.length > 0 &&
                    typeof value[0] === 'object'
                ) {
                    console.log('Count ', value);
                    assembled = this.traverseTree(mockJSON.questionnaire, []);
                }
            }
        });

        results.push(AllKeys);
        return [results, ...assembled];
    }

If I remove the inner most if statement, I will get the expected results but ONLY for the parent, so i'll get the first object that I shared in my last JSON.

Another issue I have is that the data structure might change, and so I cannot predict how many levels of children there would be.

Thank you so much for anyone reading this, it's a real head splitter for me.


## Edit #2

New Output Data Structure

[
    {
        KEY: 'parent',
        keys: [
            'customer_id',
            'customer_phone',
            'customer_zipcode',
            'customer_address',
            'customer_gender',
            'customer_name',
            'orders',
        ],
        data: [
            {
                'parent.primary_second': 'uuid-0001',
                customer_id: 1,
                customer_name: 'John',
                customer_phone: '720-222-1111',
                orders: [],
            },
            {
                'parent.primary_second': 'uuid-1001',
                customer_id: 1,
                customer_name: 'Alice',
                customer_address: '23 Main Street',
                customer_zipcode: '15234',
            },
            {
                'parent.primary_second': 'uuid-2001',
                customer_id: 1,
                customer_name: 'Colin',
                customer_gender: 'Male',
                orders: [],
            },
        ],
    },
    {
        KEY: 'parent.orders',
        keys: [
            'order_id',
            'total',
            'ordered_from',
            'geolocation',
            'coupon_code',
            'products',
        ],
        data: [
            {
                'sibling.primary_second': 'uuid-0001',
                order_id: 1,
                total: 500,
                ordered_from: 'website',
                'parent.orders.primary_second': 'uuid-0002',
            },
            {
                'sibling.primary_second': 'uuid-0001',
                order_id: 2,
                total: 240,
                ordered_from: 'app',
                geolocation: 'California',
                coupon_code: '5X23A',
                'parent.orders.primary_second': 'uuid-0003',
            },
            {
                'sibling.primary_second': 'uuid-1001',
                order_id: 4,
                total: 100,
                ordered_from: 'website',
                'parent.orders.primary_second': 'uuid-1002',
            },
            {
                'sibling.primary_second': 'uuid-1001',
                order_id: 2,
                total: 240,
                ordered_from: 'app',
                geolocation: 'California',
                coupon_code: '5X23A',
                'parent.orders.primary_second': 'uuid-1003',
            },
            {
                'sibling.primary_second': 'uuid-2001',
                order_id: 1,
                total: 500,
                ordered_from: 'website',
                'parent.orders.primary_second': 'uuid-2002',
                products: [],
            },
            {
                'sibling.primary_second': 'uuid-2001',
                order_id: 2,
                total: 240,
                ordered_from: 'app',
                geolocation: 'California',
                coupon_code: '5X23A',
                'parent.orders.primary_second': 'uuid-2003',
                products: [
                    {
                        product_id: 2,
                    },
                ],
            },
        ],
    },
    {
        KEY: 'parent.orders.products',
        keys: [
            'product_id',
            'product_price',
            'product_name',
            'product_description',
            'product_attributes',
            'category',
        ],
        data: [
            {
                'sibling.orders.primary_second': 'uuid-0002',
                product_id: 1,
                product_price: 400,
                product_name: 'The Blaster',
                product_description: 'Blasts everyone away! Fun in the pool',
            },
            {
                'sibling.orders.primary_second': 'uuid-0002',
                product_id: 2,
                product_price: 100,
                product_name: 'Water',
                product_description: 'Average H20, delivered to your doorstep',
                'parent.orders.products.primary_second': 'uuid-0004',
            },
            {
                'sibling.orders.primary_second': 'uuid-0003',
                product_id: 2,
            },
            {
                'sibling.orders.primary_second': 'uuid-1002',
                product_id: 1,
                product_price: 100,
                product_name: 'Fins',
                category: 'Water',
            },
            {
                'sibling.orders.primary_second': 'uuid-1003',
                product_id: 2,
            },
            {
                'sibling.orders.primary_second': 'uuid-2002',
                product_id: 1,
                product_price: 400,
                product_name: 'The Blaster',
                product_description: 'Blasts everyone away! Fun in the pool',
            },
            {
                'sibling.orders.primary_second': 'uuid-2002',
                product_id: 2,
                product_price: 100,
                product_name: 'Water',
                product_description: 'Average H20, delivered to your doorstep',
                'parent.orders.products.primary_second': 'uuid-2004',
                product_attributes: [],
            },
        ],
    },
    {
        KEY: 'parent.orders.products.attributes',
        keys: ['name', 'color'],
        data: [
            {
                'sibling.orders.primary_second': 'uuid-0004',
                name: 'Addon',
                color: 'Blue',
            },
            {
                'sibling.orders.primary_second': 'uuid-2004',
                name: 'Addon',
                color: 'Blue',
            },
        ],
    },
];

What I changed was, now the keys are in an array as you mentioned. But in each object I also have the data associated with that object. And this data is linked Parent <---> Sibling via parent.x.primary_key and sibling.x.primary_key. I need these primary key links so that later I can show a nested view of Customer -> Order Related to Customer -> Product related to Order -> Attributes related to Product

I'm also open to changing up my data structure. This is the most complicated data structure and assignment i've had so I might not be seeing the problem for the whole.

To give a scope of the final expected result.

I'll have a grid showing all the customers.

When I click on one customer, a second grid will show up below the first, showing the selected customers related orders. When clicking an order, a third grid will show up showing the customers related products. And so on.

Visual Representation of Graphs: enter image description here

Current Attempt at solution:

parseNestedData(data: any, parent: string) {
        var result: any = {};

        function do_level(arr: any, name?: any, primary_key?: any) {
            const newUUID = UUID.UUID();
            name = name || parent;
            var level: any = {
                KEY: name,
                allValues: [],
            };
            let tempAllValues: any[] = [];
            if (result[name]) level.allValues = [...result[name].allValues];

            arr.forEach(function (obj: any, index: number) {
                const valueExists = level.allValues.findIndex(
                    (v: any) => v.key === obj.key
                );
                tempAllValues.push({
                    ...obj,
                });

                Object.keys(obj).forEach(function (key) {
                    var value = obj[key];

                    if (
                        Array.isArray(value) &&
                        typeof value === 'object' &&
                        typeof value[0] === 'object' &&
                        value !== null
                    ) {
                        do_level(value, name + '.' + key, primary_key);
                    }
                    level[key] = true;
                });
            });
            level.allValues = [
                ...level.allValues,
                { [name + '.primary_key']: newUUID, data: tempAllValues },
            ];
            result[name] = Object.assign({}, result[name], level);
        }

        do_level(data);
        return Object.values(result);
    }
3 Answers

Since the structure is predictable it's easier to make the recursion. So we are working on arrays of objects with properties. And a name for each "array" (KEY).

So on every level of array, we collect all the keys into an object. Simple enough. if we find an array, we do the same for it (recursion). Only thing is extra parameter is name of KEY but that is simple to add up.

var data=[{customer_id:1,customer_name:"John",customer_phone:"720-222-1111",orders:[{order_id:1,total:500,ordered_from:"website",products:[{product_id:1,product_price:400,product_name:"The Blaster",product_description:"Blasts everyone away! Fun in the pool"},{product_id:2,product_price:100,product_name:"Water",product_description:"Average H20, delivered to your doorstep",product_attributes:[{name:"Addon",color:"Blue"}]}]},{order_id:2,total:240,ordered_from:"app",geolocation:"California",coupon_code:"5X23A",products:[{product_id:2}]}]},{customer_id:1,customer_name:"Alice",customer_address:"23 Main Street",customer_zipcode:"15234",orders:[{order_id:4,total:100,ordered_from:"website",products:[{product_id:1,product_price:100,product_name:"Fins",category:"Water"}]},{order_id:2,total:240,ordered_from:"app",geolocation:"California",coupon_code:"5X23A",products:[{product_id:2}]}]},{customer_id:1,customer_name:"Colin",customer_gender:"Male",orders:[{order_id:1,total:500,ordered_from:"website",products:[{product_id:1,product_price:400,product_name:"The Blaster",product_description:"Blasts everyone away! Fun in the pool"},{product_id:2,product_price:100,product_name:"Water",product_description:"Average H20, delivered to your doorstep",product_attributes:[{name:"Addon",color:"Blue"}]}]},{order_id:2,total:240,ordered_from:"app",geolocation:"California",coupon_code:"5X23A",products:[{product_id:2}]}]}]

function parse_obj(data) {

  var result = {}

  function do_level(arr, name) {
    name = name || "PARENT";
    var level = {
      KEY: name
    }
    arr.forEach(function(obj) {
      Object.keys(obj).forEach(function(key) {
        var value = obj[key];
        if (Array.isArray(value)) {
          do_level(value, name + "." + key)
        }
        if (typeof value === 'object' && value !== null) {
          // no need. but could have.
        }
        level[key] = true;
      })
    })
    result[name] = Object.assign({}, result[name], level)
  }

  do_level(data);
  return Object.values(result);
}

console.log(parse_obj(data))
.as-console-wrapper {
  max-height: 100% !important
}

I find your target structure to be fairly odd. Returning objects with keys that matter but values which can be arbitrary feels like it's missing the point. Have you considered an alternative as simple as

[
  "customer_address",
  "customer_gender",
  "customer_id",
  "customer_name",
  "customer_phone",
  "customer_zipcode",
  "orders",
  "orders.coupon_code",
  "orders.geolocation",
  "orders.order_id",
  "orders.ordered_from",
  "orders.products",
  "orders.products.category",
  "orders.products.product_attributes",
  "orders.products.product_attributes.color",
  "orders.products.product_attributes.name",
  "orders.products.product_description",
  "orders.products.product_id",
  "orders.products.product_name",
  "orders.products.product_price",
  "orders.total"
]

or if that's not enough, something like

{
  "parent": [
    "customer_address",
    "customer_gender",
    "customer_id",
    "customer_name",
    "customer_phone",
    "customer_zipcode",
    "orders"
  ],
  "parent.orders": [
    "coupon_code",
    "geolocation",
    "order_id",
    "ordered_from",
    "products",
    "total"
  ],
  "parent.orders.products": [
    "category",
    "product_attributes",
    "product_description",
    "product_id",
    "product_name",
    "product_price"
  ],
  "parent.orders.products.product_attributes": [
    "color",
    "name"
  ]
}

Since the values have no meaning, either of these carries the same information as your requested format, and both are easier to work with.

We can actually create a function that builds my first format simply from the input, and then build another function that uses the first one and transforms its output into the second format, and then do this a third time to create your target structure. While this might not be the most economical way to reach your target, it shows a useful way of working, and it will let you get your target or either of my suggestions.

The code might look like this:

// utility functions
const paths = (o, path = []) => [
  ... (path .length > 0 ? [path] : []),
  ... (Object (o) === o 
         ? Object .entries (o) .flatMap (([k, v]) => paths (v, [...path, ...(Array .isArray (o) ? [] : [k])])) 
         : []
      )
]

const splitAtLast = (c) => (s, _, __, i = s .lastIndexOf (c)) => 
  i < 0 ? [s, ''] : [s .slice (0, i), s .slice ( i + 1)]


// my first format
const collectPaths = (o) => 
  [... new Set (paths (o) .map (ns => ns .join ('.')))] .sort ()

// my second format, created by calling `collectPaths` and transforming the result
const collectSchema = (input) => 
  collectPaths ({parent: input}) 
    .map (splitAtLast ('.'))
    .filter (([s, t]) => Boolean (t))
    .reduce ((a, [b, l]) => {a [b] = (a [b] || []) .concat (l); return a}, {})


// your target format, created by calling `collectSchema` and transforming the result
const collectOddStructure = (input) =>
  Object .entries (collectSchema (input)) 
    .map (([k, vs]) => [['KEY', k], ... vs .map (v => ([v, 0]))])
    .map (Object .fromEntries)  

const input = [{customer_id: 1, customer_name: "John", customer_phone: "720-222-1111", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20,  delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Alice", customer_address: "23 Main Street", customer_zipcode: "15234", orders: [{order_id: 4, total: 100, ordered_from: "website", products: [{product_id: 1, product_price: 100, product_name: "Fins", category: "Water"}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Colin", customer_gender: "Male", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20,  delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}]

console .log ('collectPaths:', collectPaths (input))
console .log ('collectSchema: ', collectSchema (input))
console .log ('collectOddStructure: ', collectOddStructure (input))
.as-console-wrapper {max-height: 100% !important; top: 0}

The most interesting function in here is the initial one, paths, which finds the paths to all the nodes in an object, as an array of arrays such as ["orders", "products", "product_name"].

Here's a suggested alternative for normalizing your data. It seems that a format like this should make it relatively easy to build the UI you want:

{
  _guid: "table_1",
  name: "customer",
  headers: ["customer_id", "customer_name", "customer_phone", "customer_address", "customer_zipcode", "customer_gender"],
  rows: [
    {
      fields: {
        customer_id: 1,
        customer_name: "John",
        customer_phone: "720-222-1111",
        customer_address: "",
        customer_zipcode: "",
        customer_gender: ""
      },
      subtables: [
        {
          _guid: "table_2",
          name: "orders",
          headers: ["order_id", "total", "ordered_from", "geolocation", "coupon_code"],
          rows: [
             /* ... */
          ]
        } /* , ... */
      ]
    }, /* , ... */
  ]
}

Here is one way to do the conversion:

const guid = ((n = 1) => () => `table_${n ++}`) ()

const normalize = (
  xs, name, 
  headers = [... new Set (xs .flatMap (
    (x) => Object .entries (x) .filter (([k, v]) => !Array.isArray (v)) .map (([k]) => k)
  ))]
) => ({
  _guid: guid(), name, headers, 
  rows: xs .map ((x) => ({
    fields: Object .fromEntries (headers .map (h => [h, h in x ? x [h] : ''])),
    subtables: Object .entries (x) 
                 .filter (([k, v]) => Array .isArray (v)) 
                 .map (([k, v]) =>  normalize (v, k))
  }))
})

const data = [{customer_id: 1, customer_name: "John", customer_phone: "720-222-1111", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20, delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Alice", customer_address: "23 Main Street", customer_zipcode: "15234", orders: [{order_id: 4, total: 100, ordered_from: "website", products: [{product_id: 1, product_price: 100, product_name: "Fins", category: "Water"}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Colin", customer_gender: "Male", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20, delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}]

console .log (normalize (data, 'customer'))
.as-console-wrapper {max-height: 100% !important; top: 0}

We start with a simple stateful guid function that just creates a sequence of unique table names, table_1, table_2, table_3... We could instead use a true guid-creation function, but we'd still probably need to prefix these if we didn't want to take the chance of DOM nodes ids starting with inappropriate characters.

Our main function takes your input structure and a name (since "customer" doesn't actually appear in the data) and calculates the headers for the rows in the main table by looking at all the root-level records and finding the superset of keys for non-array values. Then we map the records into our rows objects, which each end up with two properties: fields contains values for each header, either the actual value or an empty string if the record doesn't have a value for the header, and subtables which contains the result of recurring on each array property.

It should be straightforward to now build DOM objects and events atop this structure.

Here is an attempt to show this in all its glory, with some DOM processing added on top. I assume the DOM stuff is disposable, but the logic seems relatively sound. (You might want to show this snippet in full screen mode.)

const guid = ((n = 1) => () => `table_${n ++}`) ()
const titleCase = (s) => s .split (/[_\s]+/) .map (([c, ...cs]) => c.toUpperCase() + cs .join ('')) .join (' ')

const normalize = (
  xs, name, 
  headers = [... new Set (xs .flatMap (
    (x) => Object .entries (x) .filter (([k, v]) => !Array.isArray (v)) .map (([k]) => k)
  ))]
) => ({
  _guid: guid(),
  name,
  headers, 
  rows: xs .map ((x) => ({
    fields: Object .fromEntries (headers .map (h => [h, h in x ? x [h] : ''])),
    subtables: Object .entries (x) 
                 .filter (([k, v]) => Array .isArray (v)) 
                 .map (([k, v]) =>  normalize (v, k))
  }))
})

const makeTables = (t) => 
`<div class="table-hierarchy" id="${t._guid}">
  <h3>${titleCase (t.name)}</h3>
  <table>
    <thead>
      <tr>${t .headers .map (h => `<th>${titleCase(h)}</th>`) .join ('')}</tr>
    </thead>
    <tbody>
      ${t.rows. map (r => `<tr data-links="${
        r.subtables.map (s => s._guid).join(',')
      }">${t .headers .map (
        h => `<td>${r.fields[h]}</td>`
      ) .join ('')}</tr>`) .join ('\n      ')}
    </tbody>
  </table>
</div>
${t.rows .flatMap (r => r .subtables) .map (makeTables) .join ('\n')}
` .replace(/\n+/g, '\n')

const showTable = (id) => document .getElementById (id) .style .display = 'block'
const hideTable = (id) => {
  const div = document .getElementById (id)
  div .style .display = 'none';
  div .querySelectorAll ('tr') .forEach (tr => {
    (tr .dataset .links || '').split (',') .filter (Boolean) .forEach (hideTable)
    tr .classList .remove ('selected')
  }) 
}

const content = document .getElementById ('content') 
content .addEventListener ('click', (evt) => {
  if (evt .target .nodeName == 'TD') {
    const tr = evt .target .closest ('TR')
    const table = tr .closest ('table')
    const alreadySelected = tr .classList .contains ('selected')
    table .querySelectorAll ('tr') .forEach (tr => {
      (tr .dataset .links || '').split (',') .filter (Boolean) .forEach (hideTable)
      tr .classList .remove ('selected')
    })
    if (alreadySelected) {
      tr .classList .remove ('selected')
    } else {
      tr .dataset .links .split (',') .filter (Boolean) .forEach (showTable)
      tr .classList .add ('selected')
    }
  }
})

const buildDOM = (data) => {
  const tables = normalize (data, 'customer')
  content .innerHTML = makeTables (tables)
  hideTable (tables ._guid)
  showTable (tables ._guid)
}


const data = [{customer_id: 1, customer_name: "John", customer_phone: "720-222-1111", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20, delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Alice", customer_address: "23 Main Street", customer_zipcode: "15234", orders: [{order_id: 4, total: 100, ordered_from: "website", products: [{product_id: 1, product_price: 100, product_name: "Fins", category: "Water"}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}, {customer_id: 1, customer_name: "Colin", customer_gender: "Male", orders: [{order_id: 1, total: 500, ordered_from: "website", products: [{product_id: 1, product_price: 400, product_name: "The Blaster", product_description: "Blasts everyone away! Fun in the pool"}, {product_id: 2, product_price: 100, product_name: "Water", product_description: "Average H20, delivered to your doorstep", product_attributes: [{name: "Addon", color: "Blue"}]}]}, {order_id: 2, total: 240, ordered_from: "app", geolocation: "California", coupon_code: "5X23A", products: [{product_id: 2}]}]}]

buildDOM (data)
table {border-collapse: collapse; border: 1px solid #999;}
td, th {border: 1px solid #999; padding: .125em .5em;}
th {background: #ccc;}
tr:hover td {background: #ff9; cursor: pointer;}
tr.selected td {background: #666; color: #fff;}
<div id = "content"></div>

Related