2

我已经(在@EmielZuurbier 的帮助下)构建了一个发票模板,该模板将 API 调用放置到 Quickbase。API 响应是分页的。如何将分页响应解析为单个表?

这是来自 API 调用的响应的样子(我删除了 data 下的大部分项目,否则在 stackoverflow 上发布会很长时间

{
    "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,因此该表非常适合 stackoverflow)

数数 项目编号 物业地址 完成日期 帐单代码 总工作价格
1 F079427 xxx 2021-03-01 (S1) 50.00 美元
2 F079430 xxx 2021-03-01 (S1) 50.00 美元
3 F079433 xxx 2021-03-16 (S1) 50.00 美元

我对如何实现这一点的想法

对于请求公式,我们可能需要一个循环的函数,它会跳过一定数量的记录=== to the sum of all the numRecords for every request made until skip + numRecords === totalRecords

例如,如果 totalRecords = 1700

  1. 第一个请求{"skip": 0}返回 numRecords=500
  2. 第二个请求{"skip": 500}返回 numRecords=500
  3. 第三个请求{"skip": 1000}返回 numRecords=500
  4. 第四个请求{"skip": 1500}返回 numRecords=200

skip + numRecords = 1700在等于总记录的第四个请求上,因此循环应该停止。

在我们拥有所有这些数组之后,我们以某种方式将它们合并到一个表中,这比我熟悉的更高级的 javascript。

4

1 回答 1

2

你的想法是正确的。API 指示根据响应元数据中的和值skip在请求中使用该功能。totalRecordsnumRecords

要设置它,您需要三个部分。
首先,你的headersand body。将headers保持不变,因为它们对于每个请求都必须相同。

body得到skip值,但是这个值对于每个请求都是不同的,所以我们会在发出请求时添加那个部分。

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请求。bodyoptionsskip

/**
 * 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 的更多记录时继续调用该函数。

为此,我创建了一个递归函数,这意味着它会不断调用自己,直到满足条件。在这种情况下,我们将希望继续调用该函数,直到没有更多记录可以获取。

只要不再发出请求,它就会返回一个对象,该对象类似于原始响应,但将所有data数组组合在一起。

因此,这意味着您将拥有相同的结构,并且不必做任何额外的事情来展平或重组数组来创建表格。

/**
 * 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函数,然后该函数将继续发出请求,直到没有更多请求为止。此函数将返回 a Promise,因此您必须使用thena 的方法Promise来告诉您在检索到所有记录时要执行的操作。

这就像等待一切完成,然后对数据做一些事情。

// 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);
});
于 2021-03-26T20:06:03.700 回答