@accede/node-red-contrib-odbcwritenow 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/odbcwritenow.html +11 -2
- package/odbcwritenow.js +92 -47
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,6 +40,8 @@ Editor fields:
|
|
|
40
40
|
- `Name` (optional)
|
|
41
41
|
- `What` (required): dataset/resource name, for example `sales_invoice_item`
|
|
42
42
|
- `OrderBy` (optional): default sort expression
|
|
43
|
+
- `MaxRetries` (optional, default `3`): number of retries for timeout/token errors
|
|
44
|
+
- `RetryBackoffMs` (optional, default `500`): base delay in milliseconds; each retry uses exponential backoff (`base * 2^attempt`)
|
|
43
45
|
- `APIKey` (required unless provided in `msg.apikey`)
|
|
44
46
|
|
|
45
47
|
## Runtime Inputs (`msg`)
|
|
@@ -78,7 +80,7 @@ You can override behavior per message:
|
|
|
78
80
|
## Notes
|
|
79
81
|
|
|
80
82
|
- Base URL is currently fixed to `https://myobsync.accede.com.au`.
|
|
81
|
-
- Timeout and token error responses are retried
|
|
83
|
+
- Timeout and token error responses are retried using bounded exponential backoff.
|
|
82
84
|
|
|
83
85
|
## License
|
|
84
86
|
|
package/odbcwritenow.html
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
name: {value:""},
|
|
7
7
|
apikey: {value:""},
|
|
8
8
|
what: {value:""},
|
|
9
|
-
orderby: {value:""}
|
|
9
|
+
orderby: {value:""},
|
|
10
|
+
maxRetries: {value:3},
|
|
11
|
+
retryBackoffMs: {value:500}
|
|
10
12
|
},
|
|
11
13
|
inputs: 1,
|
|
12
14
|
outputs: 2,
|
|
@@ -30,6 +32,14 @@
|
|
|
30
32
|
<label for="node-input-orderby"><i class="fa fa-lock"></i> OrderBy</label>
|
|
31
33
|
<input type="text" id="node-input-orderby" placeholder="OrderBy">
|
|
32
34
|
</div>
|
|
35
|
+
<div class="form-row">
|
|
36
|
+
<label for="node-input-maxRetries"><i class="fa fa-repeat"></i> MaxRetries</label>
|
|
37
|
+
<input type="number" id="node-input-maxRetries" min="0" placeholder="3">
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-retryBackoffMs"><i class="fa fa-clock-o"></i> RetryBackoffMs</label>
|
|
41
|
+
<input type="number" id="node-input-retryBackoffMs" min="0" placeholder="500">
|
|
42
|
+
</div>
|
|
33
43
|
<div class="form-row">
|
|
34
44
|
<label for="node-input-apikey"><i class="fa fa-lock"></i> APIKey</label>
|
|
35
45
|
<input type="text" id="node-input-apikey" placeholder="APIKey">
|
|
@@ -39,4 +49,3 @@
|
|
|
39
49
|
<script type="text/html" data-help-name="odbcwritenow-get">
|
|
40
50
|
<p>odbcwritenow downloader</p>
|
|
41
51
|
</script>
|
|
42
|
-
|
package/odbcwritenow.js
CHANGED
|
@@ -1,48 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
function sleep(ms) {
|
|
2
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
async function DoImport(msg, url, node, maxRetries, baseBackoffMs) {
|
|
2
6
|
console.log('>', url)
|
|
3
7
|
msg.nodata = false
|
|
4
8
|
delete msg.complete
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const response = await fetch(url);
|
|
9
|
-
|
|
10
|
-
datastr = await response.text();
|
|
11
|
-
if (datastr.toLowerCase().includes("no data found")) {
|
|
12
|
-
console.log("no data found");
|
|
13
|
-
msg.nodata = true
|
|
14
|
-
msg.complete = true
|
|
15
|
-
msg.payload = []
|
|
16
|
-
node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
|
|
17
|
-
return node.send([null, msg]);
|
|
18
|
-
}
|
|
9
|
+
let datastr = ""
|
|
10
|
+
let attempt = 0
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
while (attempt <= maxRetries) {
|
|
13
|
+
msg.retry = attempt
|
|
14
|
+
node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what} (try ${attempt + 1}/${maxRetries + 1})` })
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`)
|
|
20
|
+
}
|
|
21
|
+
datastr = await response.text();
|
|
22
|
+
|
|
23
|
+
if (datastr.toLowerCase().includes("no data found")) {
|
|
24
|
+
console.log("no data found");
|
|
25
|
+
msg.nodata = true
|
|
26
|
+
msg.complete = true
|
|
27
|
+
msg.payload = []
|
|
28
|
+
node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
|
|
29
|
+
return node.send([null, msg]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Retry only for gateway timeout and token error responses.
|
|
33
|
+
if (datastr.toLowerCase().includes("timeout") || datastr.toLowerCase().includes("token error")) {
|
|
34
|
+
const isLastTry = attempt >= maxRetries
|
|
35
|
+
const kind = datastr.toLowerCase().includes("timeout") ? "MYOB Gateway Timeout" : "MYOB Token Error"
|
|
36
|
+
|
|
37
|
+
if (isLastTry) {
|
|
38
|
+
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind} (max retries reached)` })
|
|
39
|
+
return node.error(`${kind} after ${maxRetries + 1} attempts`, msg)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const delayMs = Math.max(0, baseBackoffMs) * Math.pow(2, attempt)
|
|
43
|
+
node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind}, retrying in ${delayMs}ms` })
|
|
44
|
+
console.error("*******", kind, attempt, msg.page, `retry in ${delayMs}ms`)
|
|
45
|
+
attempt += 1
|
|
46
|
+
await sleep(delayMs)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//---------------------------------
|
|
51
|
+
// Everything looks good.
|
|
52
|
+
const data = JSON.parse(datastr);
|
|
53
|
+
msg.rows = data.length
|
|
54
|
+
console.log("Page:", msg.page, "Rows:", data.length)
|
|
55
|
+
|
|
56
|
+
node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
|
|
57
|
+
msg.payload = data;
|
|
58
|
+
return node.send([msg, null]);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
node.status({ fill: "red", shape: "ring", text: error.message })
|
|
61
|
+
console.error(error.message)
|
|
62
|
+
console.log(datastr)
|
|
63
|
+
return
|
|
32
64
|
}
|
|
33
|
-
//---------------------------------
|
|
34
|
-
//Everything looks good
|
|
35
|
-
const data = JSON.parse(datastr);
|
|
36
|
-
msg.rows = data.length
|
|
37
|
-
console.log("Page:", data.page, "Rows:", data.length)
|
|
38
|
-
|
|
39
|
-
node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
|
|
40
|
-
msg.payload = data;
|
|
41
|
-
node.send([msg, null]);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
node.status({ fill: "red", shape: "ring", text: error.message })
|
|
44
|
-
console.error(error.message)
|
|
45
|
-
console.log(datastr)
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
}
|
|
@@ -54,16 +73,43 @@ module.exports = function(RED) {
|
|
|
54
73
|
var node = this;
|
|
55
74
|
node.status({ text: `` })
|
|
56
75
|
node.on('input', async function(msg) {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
76
|
+
const pageRaw = msg.page === undefined || msg.page === null || msg.page === "" ? 0 : msg.page
|
|
77
|
+
const page = parseInt(pageRaw, 10)
|
|
78
|
+
const apikeyRaw = msg.apikey || config.apikey
|
|
79
|
+
const whatRaw = config.what
|
|
60
80
|
const orderby = encodeURIComponent(msg.orderby || config.orderby);
|
|
61
81
|
|
|
82
|
+
if (!whatRaw || String(whatRaw).trim().length === 0) {
|
|
83
|
+
node.status({ fill: "red", shape: "ring", text: "Invalid config: 'what' is required" })
|
|
84
|
+
node.error("Invalid config: 'what' is required", msg)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!apikeyRaw || String(apikeyRaw).trim().length === 0) {
|
|
89
|
+
node.status({ fill: "red", shape: "ring", text: "Missing API key" })
|
|
90
|
+
node.error("Missing API key: set config.apikey or msg.apikey", msg)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (Number.isNaN(page) || page < 0) {
|
|
95
|
+
node.status({ fill: "red", shape: "ring", text: "Invalid page: must be >= 0" })
|
|
96
|
+
node.error(`Invalid page '${pageRaw}': must be a non-negative integer`, msg)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const apikey = encodeURIComponent(apikeyRaw);
|
|
101
|
+
const what = encodeURIComponent(whatRaw)
|
|
102
|
+
|
|
62
103
|
|
|
63
104
|
|
|
64
105
|
msg.page = page
|
|
65
|
-
msg.what =
|
|
66
|
-
|
|
106
|
+
msg.what = whatRaw
|
|
107
|
+
const maxRetries = Number.isInteger(parseInt(config.maxRetries, 10))
|
|
108
|
+
? Math.max(0, parseInt(config.maxRetries, 10))
|
|
109
|
+
: 3
|
|
110
|
+
const backoffMs = Number.isInteger(parseInt(config.retryBackoffMs, 10))
|
|
111
|
+
? Math.max(0, parseInt(config.retryBackoffMs, 10))
|
|
112
|
+
: 500
|
|
67
113
|
|
|
68
114
|
var orderbystr = "";
|
|
69
115
|
var filtersstr = "";
|
|
@@ -87,11 +133,10 @@ module.exports = function(RED) {
|
|
|
87
133
|
}
|
|
88
134
|
|
|
89
135
|
const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`;
|
|
90
|
-
DoImport(msg, url, node)
|
|
136
|
+
DoImport(msg, url, node, maxRetries, backoffMs)
|
|
91
137
|
|
|
92
138
|
});
|
|
93
139
|
}
|
|
94
140
|
RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet);
|
|
95
141
|
|
|
96
142
|
}
|
|
97
|
-
|
package/package.json
CHANGED