-1

I am trying to figure out how to parse the JSON response I receive when I make a call to a specific database (JSON response shown below) using vanilla javascript - and so far I have not had any luck. I am placing an API call to the Quickbase database and they have a standard formatting for their JSON response. The API i am calling can be found at this link: https://developer.quickbase.com/operation/runQuery.

Here is what a response from the API call looks like

{
    "data": [
        {
            "6": {
                "value": 11.0
            },
            "69": {
                "value": "A"
            },
            "70": {
                "value": "B"
            }
        },
        {
            "6": {
                "value": 11.0
            },
            "69": {
                "value": "C"
            },
            "70": {
                "value": "D"
            }
        }
    ],
    "fields": [
        {
            "id": 6,
            "label": "Related Invoice",
            "type": "numeric"
        },
        {
            "id": 69,
            "label": "TEST1",
            "type": "text"
        },
        {
            "id": 70,
            "label": "TEST2",
            "type": "text"
        }
    ],
    "metadata": {
        "numFields": 3,
        "numRecords": 2,
        "skip": 0,
        "totalRecords": 2
    }
}

And this is what I want to parse it into (do NOT need to omit the JSON not shown here - I just did that for clarity)

{
    "data": [
        {
            "Related Invoice":11.0,
            "TEST1":"A",
            "TEST2":"B"
        },
        {
            "Related Invoice":11.0,
            "TEST1":"C",
            "TEST2":"D"
        }
    ]
}

Below is the full javascript code i am using

let headers = {
    'QB-Realm-Hostname': 'XXXXXX',
        'User-Agent': 'Invoice',
    'Authorization': 'XXXXXX',
    'Content-Type': 'application/json'
}

let body = 

{
  "from": "bq2paydp2",
  "select": [
    6,
    69,
    70
  ],
  "where": "{6.EX.11}",
  "sortBy": [
    {
      "fieldId": 6,
      "order": "ASC"
    },
    {
      "fieldId": 69,
      "order": "ASC"
    }
  ]
}


const xmlHttp = new XMLHttpRequest();
xmlHttp.open('POST', 'https://api.quickbase.com/v1/records/query', true);
for (const key in headers) {
  xmlHttp.setRequestHeader(key, headers[key]);
}
xmlHttp.onreadystatechange = function() {
  if (xmlHttp.readyState === XMLHttpRequest.DONE) {
    console.log(xmlHttp.responseText);


let line_items = JSON.parse(this.responseText, dataReviver);

console.log(line_items);

//function dataReviver (key, value) {
//if (key = 6)
//  {
//      var newHeaderName = 99;
//      return newHeaderName;
//  }
//
//  return value;
//} 


//document.getElementById('abc').innerHTML =  line_items.data[0][6].value;
   
function generateTableHead(table,tableData) {
  let thead = table.createTHead();
  let row = thead.insertRow();
  for (let key of tableData) {
    let th = document.createElement("th");
    let text = document.createTextNode(key);
    th.appendChild(text);
    row.appendChild(th);
  }
};

function generateTable(table, tableData) {
  for (let element of tableData) {
    let row = table.insertRow();
    for (key in element) {
      let cell = row.insertCell();
      let text = document.createTextNode(element[key]);
      cell.appendChild(text);
    }
  }
};


let table = document.querySelector("table");
let tableData = Object.keys(line_items.data[0]);
generateTableHead(table, tableData);
generateTable(table, line_items.data);

  }
};


xmlHttp.send(JSON.stringify(body));

This is what I am trying to achieve

|-----------------------------------------|
| Count | Related Invoice | TEST1 | TEST2 |
|-------|-----------------|-------|-------|
|   1   |       11.0      |   A   |   B   |
|-------|-----------------|-------|-------|      
|   2   |       11.0      |   C   |   D   |
|-----------------------------------------|

I need to accomplish 3 things:

  • #1 Rename "6", "69 and "70" to the corresponding fields.label ( "Related Invoice", "TEST1" and "TEST2" ).
  • #2 Take the value of the objects nested under the objects shown above in #1 ( 11.0, "A", "B", ... ) and set them as the value of the objects shown in #1 above. This would, for example, make 6 (Related Invoice) the key and 11.0 the value.
  • #3 I ultimately want to display this in a table on a webpage. the html and css I can handle its the Javascript and JSON that I am not that great with.

If you need me to clarify anymore information please let me know.

  • 2
    _"Don't ask about... Questions you haven't tried to find an answer for (show your work!)"_ [tour] – Thomas Sablik Mar 21 '21 at 12:55
  • 1
    The revive function is a simple mapper. You can either parse using a revive function or parse without revive function and map the result. – Thomas Sablik Mar 21 '21 at 13:01
  • 1
    @EmielZuurbier I have added the full javascript code I am using along with what my end goal is - I apologize I should of had that in there in the beginning. The lines with "//" is one of my attempts at the reviver method. I am not an expert in javascript or JSON I am trying to learn it on a project I am working on for my company. – Matt Lysinger Mar 21 '21 at 19:11

1 Answers1

1

To transform the data in the way you're looking, you'll need to loop over the data key in the object and create a new array based on the result of the loop.

A way to do this is with Array.prototype.map(). With this you can loop over each item in the array and return a new value.

In this map loop you are looping over each item in the data array. For each item you'll want to get the id and label from the fields array and use that array to create a new object. To create a new object within in a loop, you could use the Array.prototype.reduce() method.

So in this case you'll have a nested loop. The inner loop will loop over the fields array and uses the id to get the correct value from data array. It then returns an object with the label and the value set like you requested. The surrounding map method will then return a new array with objects. Tadaa, magic!

const response = {
  "data": [{
      "6": {
        "value": 11.0
      },
      "69": {
        "value": "A"
      },
      "70": {
        "value": "B"
      }
    },
    {
      "6": {
        "value": 11.0
      },
      "69": {
        "value": "C"
      },
      "70": {
        "value": "D"
      }
    }
  ],
  "fields": [{
      "id": 6,
      "label": "Related Invoice",
      "type": "numeric"
    },
    {
      "id": 69,
      "label": "TEST1",
      "type": "text"
    },
    {
      "id": 70,
      "label": "TEST2",
      "type": "text"
    }
  ],
  "metadata": {
    "numFields": 3,
    "numRecords": 2,
    "skip": 0,
    "totalRecords": 2
  }
};

const transformResponseData = (response) => {
  const { data, fields } = response;

  // Return a new array with objects based on the values
  // of the data and fields arrays.
  const revivedData = data.map(entry =>
    fields.reduce((object, { id, label }) => {
      object[label] = entry[id].value;
      return object;
    }, {})
  );

  // Combine the original object with the new data key.
  return {
    ...response,
    data: revivedData
  };
};

const createTable = ({ data, fields }) => {
  const table = document.createElement('table');
  const tHead = table.createTHead();
  const tBody = table.createTBody();

  
  const tHeadRow = tHead.insertRow();
  
  // Create the counts cell manually.
  const tHeadRowCountCell = document.createElement('th');
  tHeadRowCountCell.textContent = 'Count';
  tHeadRow.append(tHeadRowCountCell);
    
  // Create a head for each label in the fields array.
  for (const { label } of fields) {
    const tHeadRowCell = document.createElement('th');
    tHeadRowCell.textContent = label;
    tHeadRow.append(tHeadRowCell);
  }

  // Output all the values of the new data array.
  for (const [index, entry] of data.entries()) {
    const tBodyRow = tBody.insertRow();
    
    // Create a new array with the index and the
    // values from the object.
    const values = [
      index + 1,
      ...Object.values(entry)
    ];

    // Loop over the combined values array.
    for (const [index, value] of values.entries()) {
      const tBodyCell = tBodyRow.insertCell();
      tBodyCell.textContent = index === 1 ?
        value.toFixed(1) :
        value;
    }
  }

  return table;
};

const data = transformResponseData(response);
const table = createTable(data);
document.body.append(table);
Emiel Zuurbier
  • 19,095
  • 3
  • 17
  • 32
  • I have successfully implemented your code and created a really nice dynamic Invoice with this code and a combination of bootstrap. Could you help me with one more thing. I want to add a count column (as shown in the "This is what I am trying to achieve" section). Could you show me how this would be done. – Matt Lysinger Mar 25 '21 at 17:08
  • I've modified the answer. Since a count is not in the dataset, it should be added manually. Since the value looks like an index count (0, 1, 2, ... etc), the index in the loop can be used. Just add `1` to each count in the loop and you'll get your value. – Emiel Zuurbier Mar 25 '21 at 19:15
  • This worked great. I have learned a lot from this process and I appreciate your help. I just finished building what I was working on and realized the API I am calling paginates their responses and my response includes enough records where they are paginated.I honestly am not sure what to do about that. the meta data for the pagination is shown here --> [link](https://developer.quickbase.com/pagination). Is this something you would be able to show me how to do? – Matt Lysinger Mar 25 '21 at 21:04
  • I'm happy to help out. For your next question, please submit a new question. Otherwise it would pollute this answer. You could send me a link here in the comments and then I'll take a look. – Emiel Zuurbier Mar 26 '21 at 14:35
  • Thank you, See the new post here -->[link](https://stackoverflow.com/questions/66820192/how-to-parse-paginated-json-api-response-with-complex-nesting-and-unnamed-array). Also, I not know if this is appropriate or how you/stack overflow feels about this, if you are open to working with us as a freelancer/contractor we have more projects we plan to do in the future. – Matt Lysinger Mar 26 '21 at 15:54
  • I'm very flattered by your offer. But I'm happy with my current employment. And usually the comment section is not meant for discourse and should be about the nature of the answer / question, but hey, something you just want to say thanks or something off-topic. ;). Thanks again for the offer, really appreciate it. – Emiel Zuurbier Mar 26 '21 at 17:04