@accede/node-red-contrib-odbcwritenow 1.0.3 → 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 CHANGED
@@ -1,52 +1,87 @@
1
1
  # node-red-odbcwritenow
2
2
 
3
- Node-RED node for accessing MYOB data via **ODBCWriteNow**. Useful when you want AccountRight-style ODBC-style reads/writes in Node-RED without wrangling the raw HTTP yourself.
3
+ Node-RED node for downloading MYOB data from ODBCWriteNow / MYOBSync.
4
4
 
5
- ## Features
6
- - Node-RED palette node that talks to ODBC WriteNow
7
- - Read/write operations against MYOB AccountRight via the ODBC WriteNow API
8
- - Simple config for credentials and endpoint base URL
5
+ This package provides one node type: `odbcwritenow-get`.
6
+
7
+ ## What It Does
8
+
9
+ The node builds and calls:
10
+
11
+ ```text
12
+ https://myobsync.accede.com.au/download/{what}/json/{page}?apikey={apikey}[&filters=...][&datefrom=...][&dateto=...][&orderby=...]
13
+ ```
14
+
15
+ It then:
16
+
17
+ - sends parsed JSON rows to output 1
18
+ - sends "no data" completion messages to output 2
9
19
 
10
20
  ## Prerequisites
11
- - Node-RED ≥ 4.x installed and running
12
- - An active **ODBC WriteNow** account + API key
21
+
22
+ - Node-RED `>= 4.0.0`
23
+ - Valid ODBCWriteNow / MYOBSync API key
13
24
 
14
25
  ## Install
15
26
 
27
+ Install in your Node-RED user directory:
28
+
16
29
  ```bash
17
- # install into your Node-RED user dir
18
30
  cd ~/.node-red
19
- npm install DarkAxi0m/node-red-odbcwritenow
31
+ npm install @accede/node-red-contrib-odbcwritenow
20
32
  ```
21
33
 
22
- Restart Node-RED.
34
+ Restart Node-RED after installation.
23
35
 
24
- If you manage Node-RED as a service:
25
- ```bash
26
- sudo systemctl restart nodered
27
- ```
36
+ ## Node Configuration
28
37
 
29
- ## Usage
38
+ Editor fields:
30
39
 
31
- 1. In the Node-RED editor, open the palette and drag **ODBC WriteNow** onto your flow.
32
- 2. Double-click the node and set:
33
- - **Base URL**: your ODBC WriteNow endpoint (e.g. `https://myobsync.accede.com.au/`)
34
- - **API Key**: your issued key
35
- - **Operation/Path**: API route you need (e.g. download/upload endpoints per docs)
36
- - **Params/Body**: any query/body fields required for your action
40
+ - `Name` (optional)
41
+ - `What` (required): dataset/resource name, for example `sales_invoice_item`
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`)
45
+ - `APIKey` (required unless provided in `msg.apikey`)
37
46
 
38
- Refer to the ODBC WriteNow developer docs for the exact routes and parameters.
47
+ ## Runtime Inputs (`msg`)
39
48
 
40
- Example (generic pattern):
41
- ```text
42
- GET /api/download?table=Customers&updatedSince=2024-01-01
43
- POST /api/upload (JSON body with rows)
44
- ```
49
+ You can override behavior per message:
50
+
51
+ - `msg.page` (default `0`)
52
+ - `msg.apikey` (overrides configured API key)
53
+ - `msg.orderby` (overrides configured order by)
54
+ - `msg.filters`
55
+ - `msg.datefrom`
56
+ - `msg.dateto`
57
+
58
+ ## Outputs
59
+
60
+ `odbcwritenow-get` has 2 outputs:
61
+
62
+ 1. Data output
63
+ - `msg.payload`: parsed JSON array returned by the API
64
+ - `msg.rows`: number of rows in `payload`
65
+ - `msg.page`, `msg.what`, `msg.retry`
66
+
67
+ 2. No-data output
68
+ - emitted when response contains `"no data found"`
69
+ - `msg.payload = []`
70
+ - `msg.nodata = true`
71
+ - `msg.complete = true`
72
+
73
+ ## Status Behavior
74
+
75
+ - Blue ring: currently fetching
76
+ - Green dot: rows returned
77
+ - Green ring: no data found
78
+ - Red ring: timeout/token error retries or request/parsing error
79
+
80
+ ## Notes
81
+
82
+ - Base URL is currently fixed to `https://myobsync.accede.com.au`.
83
+ - Timeout and token error responses are retried using bounded exponential backoff.
45
84
 
46
85
  ## License
47
- ISC © Accede Holdings PTY LTD. See `LICENSE`.
48
86
 
49
- ## Links
50
- - Repo: [DarkAxi0m/node-red-odbcwritenow](https://github.com/DarkAxi0m/node-red-odbcwritenow)
51
- - [ODBC WriteNow – Overview & pricing](https://odbcwritenow.com/)
52
- - [ODBC WriteNow – Developer docs](https://odbcwritenow.com/developers/)
87
+ ISC. See [LICENSE](LICENSE).
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,45 +1,67 @@
1
- async function DoImport(msg, url, node) {
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
- node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what}` })
6
- var datastr = ""
7
- try {
8
- const response = await fetch(url);
9
-
10
- datastr = await response.text();
11
- if (datastr.toLowerCase().includes("no data found")) {
12
- msg.nodata = true
13
- msg.complete = true
14
- msg.payload = []
15
- node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
16
- return node.send([null, msg]);
17
- }
9
+ let datastr = ""
10
+ let attempt = 0
18
11
 
19
- //Errors and retrys etc
20
- if (datastr.toLowerCase().includes("timeout")) {
21
- node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Gateway Timeout` })
22
- console.error("******* MYOB Gateway Timeout", msg.retry, msg.page, url)
23
- msg.retry = msg.retry + 1
24
- return await DoImport(msg, url, node)
25
- }
26
- if (datastr.toLowerCase().includes("token error")) {
27
- node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Token Error` })
28
- console.error("******* MYOB Token Error", msg.retry, msg.page, url)
29
- msg.retry = msg.retry + 1
30
- return await DoImport(msg, url, node)
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
31
64
  }
32
- //---------------------------------
33
- //Everything looks good
34
- const data = JSON.parse(datastr);
35
- msg.rows = data.length
36
- node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
37
- msg.payload = data;
38
- node.send([msg, null]);
39
- } catch (error) {
40
- node.status({ fill: "red", shape: "ring", text: error.message })
41
- console.error(error.message)
42
- console.log(datastr)
43
65
  }
44
66
 
45
67
  }
@@ -51,16 +73,43 @@ module.exports = function(RED) {
51
73
  var node = this;
52
74
  node.status({ text: `` })
53
75
  node.on('input', async function(msg) {
54
- const page = parseInt(encodeURIComponent(msg.page || 0));
55
- const apikey = encodeURIComponent(msg.apikey || config.apikey);
56
- const what = encodeURIComponent(config.what)
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
57
80
  const orderby = encodeURIComponent(msg.orderby || config.orderby);
58
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
+
59
103
 
60
104
 
61
105
  msg.page = page
62
- msg.what = what
63
- msg.retry = 0
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
64
113
 
65
114
  var orderbystr = "";
66
115
  var filtersstr = "";
@@ -84,11 +133,10 @@ module.exports = function(RED) {
84
133
  }
85
134
 
86
135
  const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`;
87
- DoImport(msg, url, node)
136
+ DoImport(msg, url, node, maxRetries, backoffMs)
88
137
 
89
138
  });
90
139
  }
91
140
  RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet);
92
141
 
93
142
  }
94
-
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@accede/node-red-contrib-odbcwritenow",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
- "main": "index.js",
5
+ "main": "odbcwritenow.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },