WooCommerce API nodes for Node-RED

This commit is contained in:
2026-10-14 23:18:22 +02:00
commit e3b89fc74c
17 changed files with 1049 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"name": "node-red-contrib-nextcloud-woocommerce",
"version": "1.0.0",
"description": "Node-RED nodes for WooCommerce REST API integration",
"keywords": ["node-red", "woocommerce", "ecommerce", "orders"],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://kode.hannesahlin.se/n3wk/nextcloud-nodered-woocommerce"
},
"node-red": {
"nodes": {
"woocommerce-config": "woocommerce-config.js",
"woocommerce-order": "woocommerce-order.js",
"woocommerce-customer": "woocommerce-customer.js",
"woocommerce-product": "woocommerce-product.js"
}
},
"engines": {
"node": ">=14.0.0"
}
}
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-config", {
category: "config",
defaults: {
name: { value: "" },
baseUrl: { value: "https://shop.hanssonit.se/wp-json/wc/v3", required: true }
},
credentials: {
consumerKey: { type: "text" },
consumerSecret: { type: "password" }
},
label: function () {
return this.name || this.baseUrl || "WooCommerce";
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-config">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="WooCommerce Config">
</div>
<div class="form-row">
<label for="node-config-input-baseUrl"><i class="fa fa-server"></i> Base URL</label>
<input type="text" id="node-config-input-baseUrl" placeholder="https://shop.hanssonit.se/wp-json/wc/v3">
</div>
<div class="form-row">
<label for="node-config-input-consumerKey"><i class="fa fa-key"></i> Consumer Key</label>
<input type="text" id="node-config-input-consumerKey" autocomplete="off">
</div>
<div class="form-row">
<label for="node-config-input-consumerSecret"><i class="fa fa-lock"></i> Consumer Secret</label>
<input type="password" id="node-config-input-consumerSecret" autocomplete="off">
<div class="form-tips" style="color:#888; font-size:11px; margin-top:4px;">
Create API key at: WooCommerce → Settings → Advanced → REST API.<br>
Key must have <b>Read</b> permission (preferably Read/Write).<br>
User bound to key must be <b>Administrator</b> or equivalent.
</div>
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-config">
<p>WooCommerce REST API configuration node.</p>
<h3>Details</h3>
<p>Configure your WooCommerce store connection.</p>
<ul>
<li><b>Base URL</b> – Must be <code>https://shop.hanssonit.se/wp-json/wc/v3</code></li>
<li><b>Consumer Key</b> – From WooCommerce → Settings → Advanced → REST API</li>
<li><b>Consumer Secret</b> – Paired with Consumer Key</li>
</ul>
</script>
+52
View File
@@ -0,0 +1,52 @@
module.exports = function (RED) {
"use strict";
const https = require("https");
const http = require("http");
function WooCommerceConfigNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.baseUrl = (config.baseUrl || "https://shop.hanssonit.se/wp-json/wc/v3").replace(/\/+$/, "");
node.consumerKey = config.consumerKey || "";
node.consumerSecret = config.consumerSecret || "";
node.sendRequest = function (method, path, body, msgHeaders, callback) {
const url = new URL(node.baseUrl + path);
const parsedBody = body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined;
const headers = Object.assign({ "Accept": "application/json", "Content-Type": "application/json" }, msgHeaders || {});
if (parsedBody !== undefined) headers["Content-Length"] = Buffer.byteLength(parsedBody);
headers["Authorization"] = "Basic " + Buffer.from(node.consumerKey + ":" + node.consumerSecret).toString("base64");
const options = {
method, hostname: url.hostname,
port: url.port || (url.protocol === "https:" ? 443 : 80),
path: url.pathname + url.search, headers, rejectUnauthorized: false
};
const req = (url.protocol === "https:" ? https : http).request(options, function (res) {
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => {
let parsed = null;
if (data) { try { parsed = JSON.parse(data); } catch (e) { parsed = data; } }
if (res.statusCode >= 200 && res.statusCode < 300) {
callback(null, { statusCode: res.statusCode, body: parsed, headers: res.headers });
} else {
const err = new Error("WooCommerce API returned status " + res.statusCode + ": " + data);
err.statusCode = res.statusCode;
err.body = parsed;
callback(err);
}
});
});
req.on("error", callback);
if (parsedBody !== undefined) req.write(parsedBody);
req.end();
};
}
RED.nodes.registerType("woocommerce-config", WooCommerceConfigNode);
};
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-customer", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "search" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-users",
label: function () {
return this.name || ("WC Customer " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-customer">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Customer">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="search">Search by Email</option>
<option value="get">Get Customer by ID</option>
<option value="create">Create Customer</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>Search</b>: msg.email – exact match on customer email<br>
<b>Get</b>: msg.customerId – WooCommerce user ID<br>
<b>Create</b>: msg.email, msg.firstname, msg.lastname, msg.billing, msg.shipping
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-customer">
<p>WooCommerce customer operations: search (by email), get, create.</p>
</script>
+76
View File
@@ -0,0 +1,76 @@
module.exports = function (RED) {
"use strict";
function WooCommerceCustomerNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.configNode = RED.nodes.getNode(config.config);
if (!node.configNode) {
node.error("No woocommerce-config node configured");
return;
}
node.operation = config.operation || "search";
node.status({ fill: "blue", shape: "ring", text: "ready" });
function doRequest(method, path, body, msg, done) {
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
if (err) {
node.status({ fill: "red", shape: "ring", text: "error" });
if (done) done(err);
else node.error(err, msg);
return;
}
msg.payload = result.body;
msg.statusCode = result.statusCode;
msg.woocommerce = result;
node.status({ fill: "green", shape: "dot", text: result.statusCode });
done();
});
}
node.on("input", function (msg, send, done) {
const op = msg.operation || node.operation;
switch (op) {
case "search": {
const params = new URLSearchParams();
if (msg.email) params.append("email", msg.email);
if (msg.limit) params.append("per_page", msg.limit);
if (msg.page) params.append("page", msg.page);
const qs = params.toString();
doRequest("GET", "/customers" + (qs ? "?" + qs : ""), undefined, msg, done);
break;
}
case "get": {
const id = msg.customerId || msg.payload;
if (!id) { node.error("Missing customerId"); if (done) done(new Error("Missing customerId")); return; }
doRequest("GET", "/customers/" + encodeURIComponent(id), undefined, msg, done);
break;
}
case "create": {
const body = msg.payload && typeof msg.payload === "object" ? msg.payload : {
email: msg.email || "",
first_name: msg.firstname || msg.firstName || "",
last_name: msg.lastname || msg.lastName || "",
username: msg.username || "",
billing: msg.billing || {},
shipping: msg.shipping || {}
};
doRequest("POST", "/customers", body, msg, done);
break;
}
default: {
const err = new Error("Unknown operation: " + op);
node.error(err);
if (done) done(err);
}
}
});
}
RED.nodes.registerType("woocommerce-customer", WooCommerceCustomerNode);
};
+70
View File
@@ -0,0 +1,70 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-order", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "list" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-shopping-cart",
label: function () {
return this.name || ("WC Order " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-order">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Order">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="list">List Orders</option>
<option value="get">Get Order</option>
<option value="updateStatus">Update Status</option>
<option value="addNote">Add Note</option>
<option value="getNotes">Get Notes</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>List filters</b>: msg.status (processing, completed, on-hold, etc.),
msg.after (ISO 8601 date), msg.before, msg.customer (ID), msg.product (ID),
msg.limit (per_page), msg.page<br>
<b>Update Status</b>: msg.orderId + msg.status<br>
<b>Add Note</b>: msg.orderId + msg.note (+ optional msg.customerNote=true)
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-order">
<p>WooCommerce order operations: list, get, updateStatus, addNote, getNotes.</p>
<h3>Operations</h3>
<ul>
<li><b>list</b> – List orders with filters (status, after, before, customer, product)</li>
<li><b>get</b> – Get single order by msg.orderId</li>
<li><b>updateStatus</b> – Update order status (msg.orderId + msg.status)</li>
<li><b>addNote</b> – Add order note (msg.orderId + msg.note)</li>
<li><b>getNotes</b> – Get order notes (msg.orderId)</li>
</ul>
<h3>Order Object Fields</h3>
<p><code>billing</code> (first_name, last_name, email, company, address, phone),
<code>line_items</code>, <code>total</code>, <code>currency</code>,
<code>number</code>, <code>date_created</code>, <code>meta_data</code></p>
</script>
+83
View File
@@ -0,0 +1,83 @@
module.exports = function (RED) {
"use strict";
function WooCommerceOrderNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.configNode = RED.nodes.getNode(config.config);
if (!node.configNode) { node.error("No woocommerce-config node configured"); return; }
node.operation = config.operation || "list";
node.status({ fill: "blue", shape: "ring", text: "ready" });
function doRequest(method, path, body, msg, done) {
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
if (err) { node.status({ fill: "red", shape: "ring", text: "error" }); done ? done(err) : node.error(err, msg); return; }
msg.payload = result.body;
msg.statusCode = result.statusCode;
msg.woocommerce = result;
node.status({ fill: "green", shape: "dot", text: result.statusCode });
done();
});
}
function buildListParams(msg) {
const p = new URLSearchParams();
if (msg.status) p.append("status", msg.status);
if (msg.after) p.append("after", msg.after);
if (msg.before) p.append("before", msg.before);
if (msg.customer) p.append("customer", msg.customer);
if (msg.product) p.append("product", msg.product);
if (msg.limit) p.append("per_page", msg.limit);
if (msg.page) p.append("page", msg.page);
if (msg.order) p.append("order", msg.order);
if (msg.orderby) p.append("orderby", msg.orderby);
return p.toString() ? "?" + p.toString() : "";
}
node.on("input", function (msg, send, done) {
const op = msg.operation || node.operation;
switch (op) {
case "list": {
doRequest("GET", "/orders" + buildListParams(msg), undefined, msg, done);
break;
}
case "get": {
const id = msg.orderId || msg.payload;
if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; }
doRequest("GET", "/orders/" + encodeURIComponent(id), undefined, msg, done);
break;
}
case "updateStatus": {
const id = msg.orderId || (msg.payload && msg.payload.id);
const status = msg.status || msg.newStatus;
if (!id || !status) { node.error("Missing orderId or status"); done && done(new Error("Missing orderId or status")); return; }
doRequest("PUT", "/orders/" + encodeURIComponent(id), { status }, msg, done);
break;
}
case "addNote": {
const id = msg.orderId || (msg.payload && msg.payload.id);
const note = msg.note || msg.noteText || (msg.payload && msg.payload.note);
if (!id || !note) { node.error("Missing orderId or note"); done && done(new Error("Missing orderId or note")); return; }
doRequest("POST", "/orders/" + encodeURIComponent(id) + "/notes", { note, customer_note: msg.customerNote || false }, msg, done);
break;
}
case "getNotes": {
const id = msg.orderId || (msg.payload && msg.payload.id);
if (!id) { node.error("Missing orderId"); done && done(new Error("Missing orderId")); return; }
doRequest("GET", "/orders/" + encodeURIComponent(id) + "/notes", undefined, msg, done);
break;
}
default: {
const err = new Error("Unknown operation: " + op);
node.error(err);
if (done) done(err);
}
}
});
}
RED.nodes.registerType("woocommerce-order", WooCommerceOrderNode);
};
+54
View File
@@ -0,0 +1,54 @@
<script type="text/javascript">
RED.nodes.registerType("woocommerce-product", {
category: "WooCommerce",
color: "#7f54b3",
defaults: {
name: { value: "" },
config: { type: "woocommerce-config", required: true },
operation: { value: "list" }
},
inputs: 1,
outputs: 1,
icon: "font-awesome/fa-cube",
label: function () {
return this.name || ("WC Product " + this.operation);
},
labelStyle: function () {
return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
$("#node-input-config").val(this.config ? this.config.id : "");
},
oneditsave: function () {
this.operation = $("#node-select-operation").val();
}
});
</script>
<script type="text/x-red" data-template-name="woocommerce-product">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name" placeholder="WooCommerce Product">
</div>
<div class="form-row">
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
<input type="text" id="node-input-config">
</div>
<div class="form-row">
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
<select id="node-select-operation">
<option value="list">List Products</option>
<option value="get">Get Product by ID</option>
</select>
</div>
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
<b>List</b>: msg.limit, msg.page, msg.category (ID), msg.search<br>
<b>Get</b>: msg.productId – WooCommerce product ID<br>
Useful for enriching order data with product details.
</div>
</script>
<script type="text/x-red" data-help-name="woocommerce-product">
<p>WooCommerce product operations: list, get.</p>
<p>Useful for enriching order line_items with product details (name, SKU, image).</p>
</script>
+65
View File
@@ -0,0 +1,65 @@
module.exports = function (RED) {
"use strict";
function WooCommerceProductNode(config) {
RED.nodes.createNode(this, config);
const node = this;
node.configNode = RED.nodes.getNode(config.config);
if (!node.configNode) {
node.error("No woocommerce-config node configured");
return;
}
node.operation = config.operation || "list";
node.status({ fill: "blue", shape: "ring", text: "ready" });
function doRequest(method, path, body, msg, done) {
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
if (err) {
node.status({ fill: "red", shape: "ring", text: "error" });
if (done) done(err);
else node.error(err, msg);
return;
}
msg.payload = result.body;
msg.statusCode = result.statusCode;
msg.woocommerce = result;
node.status({ fill: "green", shape: "dot", text: result.statusCode });
done();
});
}
node.on("input", function (msg, send, done) {
const op = msg.operation || node.operation;
switch (op) {
case "list": {
const params = new URLSearchParams();
if (msg.limit) params.append("per_page", msg.limit);
if (msg.page) params.append("page", msg.page);
if (msg.category) params.append("category", msg.category);
if (msg.search) params.append("search", msg.search);
const qs = params.toString();
doRequest("GET", "/products" + (qs ? "?" + qs : ""), undefined, msg, done);
break;
}
case "get": {
const id = msg.productId || msg.payload;
if (!id) { node.error("Missing productId"); if (done) done(new Error("Missing productId")); return; }
doRequest("GET", "/products/" + encodeURIComponent(id), undefined, msg, done);
break;
}
default: {
const err = new Error("Unknown operation: " + op);
node.error(err);
if (done) done(err);
}
}
});
}
RED.nodes.registerType("woocommerce-product", WooCommerceProductNode);
};