151 lines
5.8 KiB
JavaScript
151 lines
5.8 KiB
JavaScript
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function DoImport(msg, url, node, maxRetries, baseBackoffMs) {
|
|
console.log('>', url)
|
|
msg.nodata = false
|
|
delete msg.complete
|
|
let datastr = ""
|
|
let attempt = 0
|
|
|
|
while (attempt <= maxRetries) {
|
|
msg.retry = attempt
|
|
node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what} (try ${attempt + 1}/${maxRetries + 1})` })
|
|
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status} ${response.statusText}`)
|
|
}
|
|
datastr = await response.text();
|
|
|
|
if (datastr.toLowerCase().includes("no data found")) {
|
|
console.log("no data found");
|
|
msg.nodata = true
|
|
msg.complete = true
|
|
msg.payload = []
|
|
node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
|
|
return node.send([null, msg]);
|
|
}
|
|
|
|
// Retry only for known transient responses.
|
|
const lowerData = datastr.toLowerCase()
|
|
const hasTimeout = lowerData.includes("timeout")
|
|
const hasTokenError = lowerData.includes("token error")
|
|
const hasInternalError = lowerData.includes("internalerror") || lowerData.includes("internal error")
|
|
|
|
if (hasTimeout || hasTokenError || hasInternalError) {
|
|
const isLastTry = attempt >= maxRetries
|
|
let kind = "MYOB Retryable Error"
|
|
if (hasTimeout) kind = "MYOB Gateway Timeout"
|
|
if (hasTokenError) kind = "MYOB Token Error"
|
|
if (hasInternalError) kind = "MYOB InternalError"
|
|
|
|
if (isLastTry) {
|
|
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind} (max retries reached)` })
|
|
return node.error(`${kind} after ${maxRetries + 1} attempts`, msg)
|
|
}
|
|
|
|
const delayMs = Math.max(0, baseBackoffMs) * Math.pow(2, attempt)
|
|
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind}, retrying in ${delayMs}ms` })
|
|
console.error("*******", kind, attempt, msg.page, `retry in ${delayMs}ms`)
|
|
attempt += 1
|
|
await sleep(delayMs)
|
|
continue
|
|
}
|
|
|
|
//---------------------------------
|
|
// Everything looks good.
|
|
const data = JSON.parse(datastr);
|
|
msg.rows = data.length
|
|
console.log("Page:", msg.page, "Rows:", data.length)
|
|
|
|
node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
|
|
msg.payload = data;
|
|
return node.send([msg, null]);
|
|
} catch (error) {
|
|
node.status({ fill: "red", shape: "ring", text: error.message })
|
|
console.error(error.message)
|
|
console.log(datastr)
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
module.exports = function(RED) {
|
|
function ODBCWriteNowGet(config) {
|
|
RED.nodes.createNode(this, config);
|
|
var node = this;
|
|
node.status({ text: `` })
|
|
node.on('input', async function(msg) {
|
|
const pageRaw = msg.page === undefined || msg.page === null || msg.page === "" ? 0 : msg.page
|
|
const page = parseInt(pageRaw, 10)
|
|
const apikeyRaw = msg.apikey || config.apikey
|
|
const whatRaw = config.what
|
|
const orderby = config.orderby;
|
|
|
|
if (!whatRaw || String(whatRaw).trim().length === 0) {
|
|
node.status({ fill: "red", shape: "ring", text: "Invalid config: 'what' is required" })
|
|
node.error("Invalid config: 'what' is required", msg)
|
|
return
|
|
}
|
|
|
|
if (!apikeyRaw || String(apikeyRaw).trim().length === 0) {
|
|
node.status({ fill: "red", shape: "ring", text: "Missing API key" })
|
|
node.error("Missing API key: set config.apikey or msg.apikey", msg)
|
|
return
|
|
}
|
|
|
|
if (Number.isNaN(page) || page < 0) {
|
|
node.status({ fill: "red", shape: "ring", text: "Invalid page: must be >= 0" })
|
|
node.error(`Invalid page '${pageRaw}': must be a non-negative integer`, msg)
|
|
return
|
|
}
|
|
|
|
const apikey = encodeURIComponent(apikeyRaw);
|
|
const what = encodeURIComponent(whatRaw)
|
|
|
|
|
|
|
|
msg.page = page
|
|
msg.what = whatRaw
|
|
const maxRetries = Number.isInteger(parseInt(config.maxRetries, 10))
|
|
? Math.max(0, parseInt(config.maxRetries, 10))
|
|
: 3
|
|
const backoffMs = Number.isInteger(parseInt(config.retryBackoffMs, 10))
|
|
? Math.max(0, parseInt(config.retryBackoffMs, 10))
|
|
: 500
|
|
|
|
var orderbystr = "";
|
|
var filtersstr = "";
|
|
const filters = encodeURIComponent(msg.filters || "")
|
|
if (filters.length > 0) {
|
|
filtersstr += `&filters=${filters}`
|
|
}
|
|
|
|
const datefrom = encodeURIComponent(msg.datefrom || "")
|
|
if (datefrom.length > 0) {
|
|
filtersstr += `&datefrom=${datefrom}`
|
|
}
|
|
|
|
const dateto = encodeURIComponent(msg.dateto || "")
|
|
if (dateto.length > 0) {
|
|
filtersstr += `&dateto=${dateto}`
|
|
}
|
|
|
|
if (orderby.length > 0) {
|
|
orderbystr += `&orderby=${orderby}`
|
|
}
|
|
|
|
const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`;
|
|
DoImport(msg, url, node, maxRetries, backoffMs)
|
|
|
|
});
|
|
}
|
|
RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet);
|
|
|
|
}
|