如何解析具有复杂嵌套和未命名数组的分页 JSON API 响应?
How to parse paginated JSON API response with complex nesting and unnamed array?
我已经(在@EmielZuurbier 的帮助下)构建了一个发票模板,该模板对 Quickbase 进行了 API 调用。 API 响应是分页的。如何将分页响应解析为单个 table?
- Quickbase API 端点:https://developer.quickbase.com/operation/runQuery
- Quickbase 分页元数据说明:https://developer.quickbase.com/pagination
这是 API 调用的响应(我删除了数据下的大部分项目,否则在 Whosebug 上 post 会很长
{
"data": [
{
"15": {
"value": "F079427"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-01"
},
"8": {
"value": "71 Wauregan Rd, Danielson, Connecticut 06239"
}
},
{
"15": {
"value": "F079430"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-01"
},
"8": {
"value": "7 County Home Road, Thompson, Connecticut 06277"
}
},
{
"15": {
"value": "F079433"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-16"
},
"8": {
"value": "12 Bentwood Street, Foxboro, Massachusetts 02035"
}
}
],
"fields": [
{
"id": 15,
"label": "Project Number",
"type": "text"
},
{
"id": 8,
"label": "Property Adress",
"type": "address"
},
{
"id": 50,
"label": "Date Completed",
"type": "text"
},
{
"id": 48,
"label": "Billing Codes",
"type": "text"
},
{
"id": 19,
"label": "Total Job Price",
"type": "currency"
}
],
"metadata": {
"numFields": 5,
"numRecords": 500,
"skip": 0,
"totalRecords": 766
}
}
下面是我正在使用的完整 javascript 代码
const urlParams = new URLSearchParams(window.location.search);
//const dbid = urlParams.get('dbid');//
//const fids = urlParams.get('fids');//
let rid = urlParams.get('rid');
//const sortLineItems1 = urlParams.get('sortLineItems1');//
//const sortLineItems2 = urlParams.get('sortLineItems2');//
let subtotalAmount = urlParams.get('subtotalAmount');
let discountAmount = urlParams.get('discountAmount');
let creditAmount = urlParams.get('creditAmount');
let paidAmount = urlParams.get('paidAmount');
let balanceAmount = urlParams.get('balanceAmount');
let clientName = urlParams.get('clientName');
let clientStreetAddress = urlParams.get('clientStreetAddress');
let clientCityStatePostal = urlParams.get('clientCityStatePostal');
let clientPhone = urlParams.get('clientPhone');
let invoiceNumber = urlParams.get('invoiceNumber');
let invoiceTerms = urlParams.get('invoiceTerms');
let invoiceDate = urlParams.get('invoiceDate');
let invoiceDueDate = urlParams.get('invoiceDueDate');
let invoiceNotes = urlParams.get('invoiceNotes');
const formatCurrencyUS = function (x) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(x);
}
let subtotalAmountFormatted = formatCurrencyUS(subtotalAmount);
let discountAmountFormatted = formatCurrencyUS(discountAmount);
let creditAmountFormatted = formatCurrencyUS(creditAmount);
let paidAmountFormatted = formatCurrencyUS(paidAmount);
let balanceAmountFormatted = formatCurrencyUS(balanceAmount);
document.getElementById("subtotalAmount").innerHTML = `${subtotalAmountFormatted}`;
document.getElementById("discountAmount").innerHTML = `${discountAmountFormatted}`;
document.getElementById("creditAmount").innerHTML = `${creditAmountFormatted}`;
document.getElementById("paidAmount").innerHTML = `${paidAmountFormatted}`;
document.getElementById("balanceAmount").innerHTML = `${balanceAmountFormatted}`;
document.getElementById("clientName").innerHTML = `${clientName}`;
document.getElementById("clientStreetAddress").innerHTML = `${clientStreetAddress}`;
document.getElementById("clientCityStatePostal").innerHTML = `${clientCityStatePostal}`;
document.getElementById("clientPhone").innerHTML = `${clientPhone}`;
document.getElementById("invoiceNumber").innerHTML = `${invoiceNumber}`;
document.getElementById("invoiceTerms").innerHTML = `${invoiceTerms}`;
document.getElementById("invoiceDate").innerHTML = `${invoiceDate}`;
document.getElementById("invoiceDueDate").innerHTML = `${invoiceDueDate}`;
document.getElementById("invoiceNotes").innerHTML = `${invoiceNotes}`;
let headers = {
'QB-Realm-Hostname': 'XXXXX',
'User-Agent': 'Invoice',
'Authorization': 'XXXXX',
'Content-Type': 'application/json'
}
let body =
{
"from": "bq9dajvu5",
"select": [
15,
8,
50,
48,
19
],
"where": `{25.EX.${rid}}`,
"sortBy": [
{
"fieldId": 50,
"order": "ASC"
},
{
"fieldId": 8,
"order": "ASC"
}
],
"options": {
"skip": 0
}
}
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);
console.log(line_items);
const transformResponseData = (line_items) => {
const { data, fields } = line_items;
//***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 {
...line_items,
data: revivedData
};
};
const createTable = ({ data, fields }) => {
const table = document.getElementById('line_items'); //const table = document.createElement('table');
const tHead = document.getElementById('line_items_thead'); //const tHead = table.createTHead();
const tBody = document.getElementById('line_items_tbody'); //const tBody = table.createTBody();
//***Create a head for each label in the fields array***//
const tHeadRow = tHead.insertRow();
// ***Create the counts cell manually***//
const tHeadRowCountCell = document.createElement('th');
tHeadRowCountCell.textContent = 'Count';
tHeadRow.append(tHeadRowCountCell);
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 === 5 ?
Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) ://value.toFixed(2) :
value;
}
}
return table;
};
const data = transformResponseData(line_items);
const table = createTable(data);
document.getElementById("line_items_div").append(table) //.innerHTML = table <-- this does not work// //document.body.append(table);
console.log(data);
}
};
xmlHttp.send(JSON.stringify(body));
这就是我要实现的目标(地址仅显示为 xxx,因此 table 非常适合 Whosebug)
Count
Project Number
Property Address
Date Completed
Billing Codes
Total Job Price
1
F079427
xxx
2021-03-01
(S1)
.00
2
F079430
xxx
2021-03-01
(S1)
.00
3
F079433
xxx
2021-03-16
(S1)
.00
我对如何完成这个的想法
对于请求公式,我们可能需要一个循环函数,它将跳过一定数量的记录,即 === to the sum of all the numRecords for every request made until skip + numRecords === totalRecords
例如,如果 totalRecords = 1700
- 第一个请求
{"skip": 0}
returns numRecords=500
- 第二个请求
{"skip": 500}
returnsnumRecords=500
- 第三个请求
{"skip": 1000}
returnsnumRecords=500
- 第四次请求
{"skip": 1500}
returnsnumRecords=200
在第四次请求时 skip + numRecords = 1700
等于总记录数,因此循环应该停止。
在我们拥有所有这些数组之后,我们以某种方式将它们合并成一个 table,这比我所熟悉的更高级 javascript。
你的思路是对的。 API 表示根据响应元数据中的 totalRecords
和 numRecords
值在请求中使用 skip
功能。
要进行此设置,您需要三个部分。
首先,你的 headers
和 body
。 headers
将保持不变,因为每个请求都需要相同。
body
会得到跳过值,但是这个值对于每个请求都是不同的,所以我们会在发出请求时添加那部分。
const headers = {
'QB-Realm-Hostname': 'XXXXX',
'User-Agent': 'Invoice',
'Authorization': 'XXXXX',
'Content-Type': 'application/json'
};
const body = {
"from": "bq9dajvu5",
"select": [
15,
8,
50,
48,
19
],
"where": `{25.EX.${rid}}`,
"sortBy": [
{
"fieldId": 50,
"order": "ASC"
},
{
"fieldId": 8,
"order": "ASC"
}
] // options object will be added later.
};
第二部分是重写您的请求脚本,以便我们可以传递一个 skip
值并将其放入请求正文中。我确实看到您使用 XMLHttpRequest()
,但我建议您查看 较新的 Fetch API。它基本上是相同的,但有不同的语法,在我看来,它更具可读性。
因为 skip
值是动态的,我们通过结合 body
对象的属性和 options
[=69= 来构建请求的 body
],其中包含 skip
属性 和值。
/**
* Makes a single request to the records/query endpoint.
* Expects a JSON response.
*
* @param {number} [skip=0] Amount of records to skip in the request.
* @returns {any}
*/
const getRecords = async (skip = 0) => {
const url = 'https://api.quickbase.com/v1/records/query';
// Make the request with the skip value included.
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
...body,
"options": {
"skip": skip
}
})
});
// Check if the response went okay, if not, throw an error.
if (!response.ok) {
throw new Error(`
The getRecords request has failed:
${response.status} - ${response.statusText}
`);
}
// Decode the body of the response
const payload = await response.json();
return payload;
};
最后一部分是关于确保 getRecords
函数在需要来自 API 的更多记录时不断被调用。
为此,我创建了一个递归函数,这意味着它会不断调用自身,直到满足条件。在这种情况下,我们要继续调用该函数,直到没有更多记录可获取。
每当没有更多请求时,它将 return 一个对象,类似于原始响应,但包含所有 data
个数组。
因此,这意味着您将拥有相同的结构,而无需执行任何额外的操作来展平或重组数组来创建 table.
/**
* Recursive function which keeps getting more records if the current amount
* of records is below the total. Then skips the amount already received
* for each new request, collecting all data in a single object.
*
* @param {number} amountToSkip Amount of records to skip.
* @param {object} collection The collection object.
* @returns {object} An object will all data collected.
*/
const collectRecords = async (amountToSkip = 0, collection = { data: [], fields: [] }) => {
try {
const { data, fields, metadata } = await getRecords(amountToSkip);
const { numRecords, totalRecords, skip } = metadata;
// The amount of collected records.
const recordsCollected = numRecords + skip;
// The data array should be merged with the previous ones.
collection.data = [
...collection.data,
...data
];
// Set the fields the first time.
// They'll never change and only need to be set once.
if (!collection.fields.length) {
collection.fields = fields;
}
// The metadata is updated for each request.
// It might be useful to know the state of the last request.
collection.metadata = metadata;
// Get more records if the current amount of records + the skip amount is lower than the total.
if (recordsCollected < totalRecords) {
return collectRecords(recordsCollected, collection);
}
return collection;
} catch (error) {
console.error(error);
}
};
现在要使用它,您调用 collectRecords
函数,该函数将继续发出请求,直到没有更多请求为止。此函数将 return 和 Promise
,因此您必须使用 Promise
的 then
方法来告诉您在检索到所有记录时要执行的操作。
这就像等待一切完成然后然后对数据做一些事情。
// Select the table div element.
const tableDiv = document.getElementById('line_items_div');
// Get the records, collect them in multiple requests, and generate a table from the data.
collectRecords().then(records => {
const data = transformRecordsData(records);
const table = createTable(data);
tableDiv.append(table);
});
我已经(在@EmielZuurbier 的帮助下)构建了一个发票模板,该模板对 Quickbase 进行了 API 调用。 API 响应是分页的。如何将分页响应解析为单个 table?
- Quickbase API 端点:https://developer.quickbase.com/operation/runQuery
- Quickbase 分页元数据说明:https://developer.quickbase.com/pagination
这是 API 调用的响应(我删除了数据下的大部分项目,否则在 Whosebug 上 post 会很长
{
"data": [
{
"15": {
"value": "F079427"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-01"
},
"8": {
"value": "71 Wauregan Rd, Danielson, Connecticut 06239"
}
},
{
"15": {
"value": "F079430"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-01"
},
"8": {
"value": "7 County Home Road, Thompson, Connecticut 06277"
}
},
{
"15": {
"value": "F079433"
},
"19": {
"value": 50.0
},
"48": {
"value": "(S1)"
},
"50": {
"value": "2021-03-16"
},
"8": {
"value": "12 Bentwood Street, Foxboro, Massachusetts 02035"
}
}
],
"fields": [
{
"id": 15,
"label": "Project Number",
"type": "text"
},
{
"id": 8,
"label": "Property Adress",
"type": "address"
},
{
"id": 50,
"label": "Date Completed",
"type": "text"
},
{
"id": 48,
"label": "Billing Codes",
"type": "text"
},
{
"id": 19,
"label": "Total Job Price",
"type": "currency"
}
],
"metadata": {
"numFields": 5,
"numRecords": 500,
"skip": 0,
"totalRecords": 766
}
}
下面是我正在使用的完整 javascript 代码
const urlParams = new URLSearchParams(window.location.search);
//const dbid = urlParams.get('dbid');//
//const fids = urlParams.get('fids');//
let rid = urlParams.get('rid');
//const sortLineItems1 = urlParams.get('sortLineItems1');//
//const sortLineItems2 = urlParams.get('sortLineItems2');//
let subtotalAmount = urlParams.get('subtotalAmount');
let discountAmount = urlParams.get('discountAmount');
let creditAmount = urlParams.get('creditAmount');
let paidAmount = urlParams.get('paidAmount');
let balanceAmount = urlParams.get('balanceAmount');
let clientName = urlParams.get('clientName');
let clientStreetAddress = urlParams.get('clientStreetAddress');
let clientCityStatePostal = urlParams.get('clientCityStatePostal');
let clientPhone = urlParams.get('clientPhone');
let invoiceNumber = urlParams.get('invoiceNumber');
let invoiceTerms = urlParams.get('invoiceTerms');
let invoiceDate = urlParams.get('invoiceDate');
let invoiceDueDate = urlParams.get('invoiceDueDate');
let invoiceNotes = urlParams.get('invoiceNotes');
const formatCurrencyUS = function (x) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(x);
}
let subtotalAmountFormatted = formatCurrencyUS(subtotalAmount);
let discountAmountFormatted = formatCurrencyUS(discountAmount);
let creditAmountFormatted = formatCurrencyUS(creditAmount);
let paidAmountFormatted = formatCurrencyUS(paidAmount);
let balanceAmountFormatted = formatCurrencyUS(balanceAmount);
document.getElementById("subtotalAmount").innerHTML = `${subtotalAmountFormatted}`;
document.getElementById("discountAmount").innerHTML = `${discountAmountFormatted}`;
document.getElementById("creditAmount").innerHTML = `${creditAmountFormatted}`;
document.getElementById("paidAmount").innerHTML = `${paidAmountFormatted}`;
document.getElementById("balanceAmount").innerHTML = `${balanceAmountFormatted}`;
document.getElementById("clientName").innerHTML = `${clientName}`;
document.getElementById("clientStreetAddress").innerHTML = `${clientStreetAddress}`;
document.getElementById("clientCityStatePostal").innerHTML = `${clientCityStatePostal}`;
document.getElementById("clientPhone").innerHTML = `${clientPhone}`;
document.getElementById("invoiceNumber").innerHTML = `${invoiceNumber}`;
document.getElementById("invoiceTerms").innerHTML = `${invoiceTerms}`;
document.getElementById("invoiceDate").innerHTML = `${invoiceDate}`;
document.getElementById("invoiceDueDate").innerHTML = `${invoiceDueDate}`;
document.getElementById("invoiceNotes").innerHTML = `${invoiceNotes}`;
let headers = {
'QB-Realm-Hostname': 'XXXXX',
'User-Agent': 'Invoice',
'Authorization': 'XXXXX',
'Content-Type': 'application/json'
}
let body =
{
"from": "bq9dajvu5",
"select": [
15,
8,
50,
48,
19
],
"where": `{25.EX.${rid}}`,
"sortBy": [
{
"fieldId": 50,
"order": "ASC"
},
{
"fieldId": 8,
"order": "ASC"
}
],
"options": {
"skip": 0
}
}
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);
console.log(line_items);
const transformResponseData = (line_items) => {
const { data, fields } = line_items;
//***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 {
...line_items,
data: revivedData
};
};
const createTable = ({ data, fields }) => {
const table = document.getElementById('line_items'); //const table = document.createElement('table');
const tHead = document.getElementById('line_items_thead'); //const tHead = table.createTHead();
const tBody = document.getElementById('line_items_tbody'); //const tBody = table.createTBody();
//***Create a head for each label in the fields array***//
const tHeadRow = tHead.insertRow();
// ***Create the counts cell manually***//
const tHeadRowCountCell = document.createElement('th');
tHeadRowCountCell.textContent = 'Count';
tHeadRow.append(tHeadRowCountCell);
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 === 5 ?
Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) ://value.toFixed(2) :
value;
}
}
return table;
};
const data = transformResponseData(line_items);
const table = createTable(data);
document.getElementById("line_items_div").append(table) //.innerHTML = table <-- this does not work// //document.body.append(table);
console.log(data);
}
};
xmlHttp.send(JSON.stringify(body));
这就是我要实现的目标(地址仅显示为 xxx,因此 table 非常适合 Whosebug)
Count | Project Number | Property Address | Date Completed | Billing Codes | Total Job Price |
---|---|---|---|---|---|
1 | F079427 | xxx | 2021-03-01 | (S1) | .00 |
2 | F079430 | xxx | 2021-03-01 | (S1) | .00 |
3 | F079433 | xxx | 2021-03-16 | (S1) | .00 |
我对如何完成这个的想法
对于请求公式,我们可能需要一个循环函数,它将跳过一定数量的记录,即 === to the sum of all the numRecords for every request made until skip + numRecords === totalRecords
例如,如果 totalRecords = 1700
- 第一个请求
{"skip": 0}
returns numRecords=500 - 第二个请求
{"skip": 500}
returnsnumRecords=500 - 第三个请求
{"skip": 1000}
returnsnumRecords=500 - 第四次请求
{"skip": 1500}
returnsnumRecords=200
在第四次请求时 skip + numRecords = 1700
等于总记录数,因此循环应该停止。
在我们拥有所有这些数组之后,我们以某种方式将它们合并成一个 table,这比我所熟悉的更高级 javascript。
你的思路是对的。 API 表示根据响应元数据中的 totalRecords
和 numRecords
值在请求中使用 skip
功能。
要进行此设置,您需要三个部分。
首先,你的 headers
和 body
。 headers
将保持不变,因为每个请求都需要相同。
body
会得到跳过值,但是这个值对于每个请求都是不同的,所以我们会在发出请求时添加那部分。
const headers = {
'QB-Realm-Hostname': 'XXXXX',
'User-Agent': 'Invoice',
'Authorization': 'XXXXX',
'Content-Type': 'application/json'
};
const body = {
"from": "bq9dajvu5",
"select": [
15,
8,
50,
48,
19
],
"where": `{25.EX.${rid}}`,
"sortBy": [
{
"fieldId": 50,
"order": "ASC"
},
{
"fieldId": 8,
"order": "ASC"
}
] // options object will be added later.
};
第二部分是重写您的请求脚本,以便我们可以传递一个 skip
值并将其放入请求正文中。我确实看到您使用 XMLHttpRequest()
,但我建议您查看 较新的 Fetch API。它基本上是相同的,但有不同的语法,在我看来,它更具可读性。
因为 skip
值是动态的,我们通过结合 body
对象的属性和 options
[=69= 来构建请求的 body
],其中包含 skip
属性 和值。
/**
* Makes a single request to the records/query endpoint.
* Expects a JSON response.
*
* @param {number} [skip=0] Amount of records to skip in the request.
* @returns {any}
*/
const getRecords = async (skip = 0) => {
const url = 'https://api.quickbase.com/v1/records/query';
// Make the request with the skip value included.
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
...body,
"options": {
"skip": skip
}
})
});
// Check if the response went okay, if not, throw an error.
if (!response.ok) {
throw new Error(`
The getRecords request has failed:
${response.status} - ${response.statusText}
`);
}
// Decode the body of the response
const payload = await response.json();
return payload;
};
最后一部分是关于确保 getRecords
函数在需要来自 API 的更多记录时不断被调用。
为此,我创建了一个递归函数,这意味着它会不断调用自身,直到满足条件。在这种情况下,我们要继续调用该函数,直到没有更多记录可获取。
每当没有更多请求时,它将 return 一个对象,类似于原始响应,但包含所有 data
个数组。
因此,这意味着您将拥有相同的结构,而无需执行任何额外的操作来展平或重组数组来创建 table.
/**
* Recursive function which keeps getting more records if the current amount
* of records is below the total. Then skips the amount already received
* for each new request, collecting all data in a single object.
*
* @param {number} amountToSkip Amount of records to skip.
* @param {object} collection The collection object.
* @returns {object} An object will all data collected.
*/
const collectRecords = async (amountToSkip = 0, collection = { data: [], fields: [] }) => {
try {
const { data, fields, metadata } = await getRecords(amountToSkip);
const { numRecords, totalRecords, skip } = metadata;
// The amount of collected records.
const recordsCollected = numRecords + skip;
// The data array should be merged with the previous ones.
collection.data = [
...collection.data,
...data
];
// Set the fields the first time.
// They'll never change and only need to be set once.
if (!collection.fields.length) {
collection.fields = fields;
}
// The metadata is updated for each request.
// It might be useful to know the state of the last request.
collection.metadata = metadata;
// Get more records if the current amount of records + the skip amount is lower than the total.
if (recordsCollected < totalRecords) {
return collectRecords(recordsCollected, collection);
}
return collection;
} catch (error) {
console.error(error);
}
};
现在要使用它,您调用 collectRecords
函数,该函数将继续发出请求,直到没有更多请求为止。此函数将 return 和 Promise
,因此您必须使用 Promise
的 then
方法来告诉您在检索到所有记录时要执行的操作。
这就像等待一切完成然后然后对数据做一些事情。
// Select the table div element.
const tableDiv = document.getElementById('line_items_div');
// Get the records, collect them in multiple requests, and generate a table from the data.
collectRecords().then(records => {
const data = transformRecordsData(records);
const table = createTable(data);
tableDiv.append(table);
});