WooCommerce API nodes for Node-RED
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
# Secrets & credentials
|
||||
.env
|
||||
.env.*
|
||||
*.pem
|
||||
*.key
|
||||
credentials*
|
||||
credentials.json
|
||||
consumer*secret*
|
||||
consumer*key*
|
||||
secrets*
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Temp
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM nodered/node-red:latest
|
||||
|
||||
USER root
|
||||
|
||||
COPY nodes/ /opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,182 @@
|
||||
# nextcloud-nodered-woocommerce
|
||||
|
||||
Node-RED nodes for the [WooCommerce](https://woocommerce.com/) REST API. Part of the
|
||||
[nextcloud-nodered](https://kode.hannesahlin.se/n3wk/) suite.
|
||||
|
||||
## Package
|
||||
|
||||
`node-red-contrib-nextcloud-woocommerce`
|
||||
|
||||
## Nodes
|
||||
|
||||
| Node | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `woocommerce-config` | config | Base URL + Consumer Key + Consumer Secret |
|
||||
| `woocommerce-order` | function | list (with filters), get, updateStatus, addNote, getNotes |
|
||||
| `woocommerce-customer` | function | search (by email), get, create |
|
||||
| `woocommerce-product` | function | list, get (for enriching order data) |
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- An existing [nextcloud-node-red](https://kode.hannesahlin.se/n3wk/nextcloud-nodered-ocs-api) installation.
|
||||
- WooCommerce API key with Read permission (preferably Read/Write), bound to an Administrator user.
|
||||
|
||||
### Quick install (recommended)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kode-tjenester/nextcloud-nodered-woocommerce.git
|
||||
cd nextcloud-nodered-woocommerce
|
||||
export NODERED_DIR=/path/to/your/nextcloud-node-red
|
||||
bash bootstrap-install.sh
|
||||
cd $NODERED_DIR
|
||||
docker compose build --no-cache
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Manual install
|
||||
|
||||
```bash
|
||||
export NODERED_DIR=/path/to/your/nextcloud-node-red
|
||||
bash integrate-into-nodered.sh
|
||||
```
|
||||
|
||||
The integrate script:
|
||||
1. Copies all `.js` and `.html` files into `$NODERED_DIR/nodes/`.
|
||||
2. Copies the package to `$NODERED_DIR/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce/`.
|
||||
3. Patches `$NODERED_DIR/entrypoint.sh` to install the nodes on container start.
|
||||
4. Is idempotent — safe to run multiple times.
|
||||
|
||||
## Authentication
|
||||
|
||||
| Method | Details |
|
||||
|--------|---------|
|
||||
| Consumer Key | From WooCommerce → Settings → Advanced → REST API |
|
||||
| Consumer Secret | Paired with Consumer Key |
|
||||
|
||||
**Important:** Base URL must be `https://shop.hanssonit.se/wp-json/wc/v3/`.
|
||||
The API key must have at least Read permission. The user the key is bound to
|
||||
must be an Administrator or have equivalent capabilities.
|
||||
|
||||
## Node Details
|
||||
|
||||
### woocommerce-order
|
||||
|
||||
Operations: `list`, `get`, `updateStatus`, `addNote`, `getNotes`.
|
||||
|
||||
**List with filters:**
|
||||
```
|
||||
msg.status = "processing" | "completed" | "on-hold" | ...
|
||||
msg.after = "2026-10-01T00:00:00" (ISO 8601)
|
||||
msg.before = "2026-10-31T23:59:59"
|
||||
msg.customer = customer ID
|
||||
msg.product = product ID
|
||||
msg.limit = per_page (default 10)
|
||||
msg.page = page number
|
||||
```
|
||||
|
||||
**Update status:**
|
||||
```
|
||||
msg.orderId = 31921
|
||||
msg.status = "completed"
|
||||
```
|
||||
|
||||
**Add note:**
|
||||
```
|
||||
msg.orderId = 31921
|
||||
msg.note = "Customer contacted via phone"
|
||||
msg.customerNote = true (optional — visible to customer)
|
||||
```
|
||||
|
||||
### woocommerce-customer
|
||||
|
||||
Operations: `search` (by email), `get`, `create`.
|
||||
|
||||
**Search by email:**
|
||||
```
|
||||
msg.email = "lars@lrginvest.se"
|
||||
```
|
||||
|
||||
### woocommerce-product
|
||||
|
||||
Operations: `list`, `get`.
|
||||
|
||||
Useful for enriching order `line_items` with product name, SKU, images.
|
||||
|
||||
## Vertical Integration Flow: WooCommerce → Zammad → Nextcloud
|
||||
|
||||
This is the trigger half of the vertical flow. Combined with
|
||||
[nextcloud-nodered-zammad](https://github.com/kode-tjenester/nextcloud-nodered-zammad)
|
||||
and the existing Nextcloud OCS nodes:
|
||||
|
||||
### Step 1: Poll for new orders (or webhook)
|
||||
|
||||
Use `woocommerce-order` with operation `list` and `msg.status = "processing"`
|
||||
on a schedule (inject node with cron), or trigger via a WooCommerce webhook.
|
||||
|
||||
### Step 2: Extract order info in a Function node
|
||||
|
||||
```javascript
|
||||
msg.title = "Order #" + msg.payload.number + " – " +
|
||||
msg.payload.billing.first_name + " " + msg.payload.billing.last_name +
|
||||
(msg.payload.billing.company ? " (" + msg.payload.billing.company + ")" : "");
|
||||
|
||||
msg.customer = msg.payload.billing.email;
|
||||
|
||||
msg.body = "Products:\n";
|
||||
for (let item of msg.payload.line_items) {
|
||||
msg.body += "- " + item.name + " x" + item.quantity +
|
||||
" (" + msg.payload.currency + " " + item.total + ")\n";
|
||||
}
|
||||
msg.body += "\nTotal: " + msg.payload.currency + " " + msg.payload.total + "\n";
|
||||
msg.body += "Address: " + msg.payload.billing.address_1 + ", " +
|
||||
msg.payload.billing.city + " " + msg.payload.billing.postcode + "\n";
|
||||
msg.body += "Phone: " + msg.payload.billing.phone + "\n";
|
||||
msg.body += "Email: " + msg.payload.billing.email;
|
||||
|
||||
msg.ticketId = msg.payload.id;
|
||||
msg.tag = "woocommerce-order";
|
||||
|
||||
return msg;
|
||||
```
|
||||
|
||||
### Step 3: Create Zammad ticket
|
||||
|
||||
`zammad-ticket` node (operation: create) receives the Function output.
|
||||
Title: `Order #31921 – Lars Gardelin (LRG Invest AB)`
|
||||
Customer: billing email
|
||||
Body: product list + amount + address + phone
|
||||
|
||||
### Step 4: Tag the ticket
|
||||
|
||||
`zammad-ticket` (operation: addTag) with `msg.tag = "woocommerce-order"`.
|
||||
|
||||
### Step 5: Organize in Nextcloud
|
||||
|
||||
Existing Nextcloud OCS nodes:
|
||||
- Create/update customer folder
|
||||
- Create note or Deck card with order details
|
||||
- Optional: calendar event for follow-up
|
||||
- Link back to Zammad ticket in note/card
|
||||
|
||||
### Example Flow JSON
|
||||
|
||||
See `flows/woocommerce-to-zammad.json` for a complete example flow
|
||||
(no real credentials included).
|
||||
|
||||
## Integration with Other Packages
|
||||
|
||||
- **nextcloud-nodered-zammad**: Creates and manages Zammad tickets from WooCommerce orders.
|
||||
- **nextcloud-nodered-ocs-api**: Provides Nextcloud OCS nodes for creating folders, notes, deck cards, etc.
|
||||
- **nextcloud-nodered-agent**: Agent-based nodes for complex orchestration.
|
||||
|
||||
## Credentials Security
|
||||
|
||||
- Consumer Key and Secret are stored **only** in the Node-RED config node (encrypted at rest).
|
||||
- Never commit credentials to git (`.gitignore` excludes them).
|
||||
- The config node uses Node-RED's built-in credential encryption.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Bootstrap installer for node-red-contrib-nextcloud-woocommerce
|
||||
# Clones repo and integrates into existing nextcloud-node-red
|
||||
|
||||
REPO_URL="https://kode.hannesahlin.se/n3wk/nextcloud-nodered-woocommerce.git"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/node-red-contrib-nextcloud-woocommerce}"
|
||||
NODERED_DIR="${NODERED_DIR:-}"
|
||||
|
||||
echo "=== Nextcloud WooCommerce Node-RED Bootstrap Installer ==="
|
||||
|
||||
# Auto-detect nextcloud-node-red directory if not set
|
||||
if [[ -z "$NODERED_DIR" ]]; then
|
||||
for dir in \
|
||||
"/opt/nextcloud-node-red" \
|
||||
"/home/ncdeploy/nextcloud-node-red" \
|
||||
"/root/nextcloud-node-red" \
|
||||
"$(docker inspect -f '{{.Config.Labels}}' nextcloud-node-red 2>/dev/null | grep -o 'com.docker.compose.project.working_dir=[^ ]*' | cut -d= -f2)"; do
|
||||
if [[ -n "$dir" && -d "$dir" && -f "$dir/Dockerfile" ]]; then
|
||||
NODERED_DIR="$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$NODERED_DIR" ]]; then
|
||||
echo "ERROR: Could not auto-detect nextcloud-node-red directory."
|
||||
echo "Set NODERED_DIR environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Target Node-RED: $NODERED_DIR"
|
||||
|
||||
# Clone or update repo
|
||||
if [[ -d "$INSTALL_DIR/.git" ]]; then
|
||||
echo "Updating existing repo..."
|
||||
git -C "$INSTALL_DIR" pull --ff-only || echo "Pull failed, continuing with existing code..."
|
||||
else
|
||||
echo "Cloning repo..."
|
||||
git clone --depth 1 "$REPO_URL" "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# Run integration
|
||||
export NODERED_DIR
|
||||
"$INSTALL_DIR/integrate-into-nodered.sh"
|
||||
|
||||
echo ""
|
||||
echo "=== Bootstrap Complete ==="
|
||||
echo "Rebuild with: cd $NODERED_DIR && docker compose build --no-cache && docker compose up -d"
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
node-red:
|
||||
build: .
|
||||
image: nextcloud-node-red-woocommerce:latest
|
||||
container_name: nextcloud-node-red-woocommerce
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- node-red-data:/data
|
||||
|
||||
volumes:
|
||||
node-red-data:
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# Install woocommerce nodes into /data at runtime.
|
||||
# /data is a Docker volume that masks build-time COPY.
|
||||
|
||||
set -eu
|
||||
|
||||
SRC="/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce"
|
||||
DST_DIR="/data/node_modules"
|
||||
DST="$DST_DIR/node-red-contrib-nextcloud-woocommerce"
|
||||
|
||||
if [ ! -d "$SRC" ]; then
|
||||
echo "[entrypoint] ERROR: source directory missing: $SRC" >&2
|
||||
ls -la /opt || true
|
||||
ls -la /opt/nextcloud-nodes || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_DIR"
|
||||
rm -rf "$DST"
|
||||
cp -a "$SRC" "$DST_DIR/"
|
||||
|
||||
if [ ! -f "$DST/package.json" ]; then
|
||||
echo "[entrypoint] ERROR: install did not produce $DST/package.json" >&2
|
||||
ls -la /data || true
|
||||
ls -la "$DST_DIR" || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chown -R node-red:node-red /data "$DST_DIR" "$DST" || true
|
||||
chmod -R a+rX "$DST" || true
|
||||
|
||||
export NODE_RED_USER_DIR=/data
|
||||
|
||||
exec node-red --userDir /data
|
||||
@@ -0,0 +1,57 @@
|
||||
[
|
||||
{
|
||||
"id": "wc-poll-trigger",
|
||||
"type": "inject",
|
||||
"name": "Poll Processing Orders",
|
||||
"topic": "woocommerce/poll",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"repeat": "900",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": 0.1,
|
||||
"x": 150,
|
||||
"y": 100,
|
||||
"wires": [["wc-list-orders"]]
|
||||
},
|
||||
{
|
||||
"id": "wc-list-orders",
|
||||
"type": "woocommerce-order",
|
||||
"name": "List Processing Orders",
|
||||
"config": "woocommerce-config-placeholder",
|
||||
"operation": "list",
|
||||
"x": 350,
|
||||
"y": 100,
|
||||
"wires": [["wc-extract-info"]]
|
||||
},
|
||||
{
|
||||
"id": "wc-extract-info",
|
||||
"type": "function",
|
||||
"name": "Extract Order Info",
|
||||
"func": "var order = Array.isArray(msg.payload) ? msg.payload[0] : msg.payload;\nif (!order || !order.billing) return null;\nmsg.title = 'Order #' + order.number + ' – ' + order.billing.first_name + ' ' + order.billing.last_name;\nif (order.billing.company) msg.title += ' (' + order.billing.company + ')';\nmsg.customer = order.billing.email;\nmsg.body = 'Products:\\n';\nif (order.line_items) {\n for (var i = 0; i < order.line_items.length; i++) {\n var item = order.line_items[i];\n msg.body += '- ' + item.name + ' x' + item.quantity + ' (' + order.currency + ' ' + item.total + ')\\n';\n }\n}\nmsg.body += '\\nTotal: ' + order.currency + ' ' + order.total;\nmsg.body += '\\nAddress: ' + order.billing.address_1 + ', ' + order.billing.city + ' ' + order.billing.postcode;\nmsg.body += '\\nPhone: ' + order.billing.phone;\nmsg.body += '\\nEmail: ' + order.billing.email;\nmsg.ticketId = order.id;\nmsg.tag = 'woocommerce-order';\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 550,
|
||||
"y": 100,
|
||||
"wires": [["zammad-create-ticket"]]
|
||||
},
|
||||
{
|
||||
"id": "zammad-create-ticket",
|
||||
"type": "zammad-ticket",
|
||||
"name": "Create Ticket",
|
||||
"config": "zammad-config-placeholder",
|
||||
"operation": "create",
|
||||
"x": 750,
|
||||
"y": 100,
|
||||
"wires": [["zammad-add-tag"]]
|
||||
},
|
||||
{
|
||||
"id": "zammad-add-tag",
|
||||
"type": "zammad-ticket",
|
||||
"name": "Add WooCommerce Tag",
|
||||
"config": "zammad-config-placeholder",
|
||||
"operation": "addTag",
|
||||
"x": 950,
|
||||
"y": 100,
|
||||
"wires": [[]]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Integration script for node-red-contrib-nextcloud-woocommerce
|
||||
# Patches woocommerce nodes into existing nextcloud-node-red Docker image
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PACKAGE_NAME="node-red-contrib-nextcloud-woocommerce"
|
||||
|
||||
# Auto-detect Node-RED directory
|
||||
if [[ -z "${NODERED_DIR:-}" ]]; then
|
||||
for dir in \
|
||||
"/opt/nextcloud-node-red" \
|
||||
"/home/ncdeploy/nextcloud-node-red" \
|
||||
"/root/nextcloud-node-red" \
|
||||
"$(dirname "$SCRIPT_DIR")/nextcloud-node-red" \
|
||||
"$(docker inspect -f '{{.Config.Labels}}' nextcloud-node-red 2>/dev/null | grep -o 'com.docker.compose.project.working_dir=[^ ]*' | cut -d= -f2)"; do
|
||||
if [[ -n "$dir" && -d "$dir" && -f "$dir/Dockerfile" ]]; then
|
||||
NODERED_DIR="$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "${NODERED_DIR:-}" ]] || [[ ! -d "$NODERED_DIR" ]]; then
|
||||
echo "ERROR: Could not auto-detect nextcloud-node-red directory."
|
||||
echo "Set NODERED_DIR environment variable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$NODERED_DIR/Dockerfile" ]]; then
|
||||
echo "ERROR: No Dockerfile found in $NODERED_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Nextcloud WooCommerce Node-RED Integration ==="
|
||||
echo "Source: $SCRIPT_DIR"
|
||||
echo "Target: $NODERED_DIR"
|
||||
|
||||
# 1. Copy nodes to nextcloud-node-red nodes subdirectory
|
||||
NODES_TARGET="$NODERED_DIR/nodes/$PACKAGE_NAME"
|
||||
echo "[integrate] Copying woocommerce nodes to $NODES_TARGET..."
|
||||
mkdir -p "$NODES_TARGET"
|
||||
cp "$SCRIPT_DIR/nodes"/*.js "$NODES_TARGET/"
|
||||
cp "$SCRIPT_DIR/nodes"/*.html "$NODES_TARGET/"
|
||||
cp "$SCRIPT_DIR/nodes/package.json" "$NODES_TARGET/"
|
||||
echo " Nodes copied."
|
||||
|
||||
# 2. Patch Dockerfile to COPY woocommerce nodes into /opt
|
||||
DOCKERFILE="$NODERED_DIR/Dockerfile"
|
||||
echo "[integrate] Patching Dockerfile..."
|
||||
|
||||
if grep -q "$PACKAGE_NAME" "$DOCKERFILE"; then
|
||||
echo " Dockerfile already contains $PACKAGE_NAME"
|
||||
else
|
||||
cp "$DOCKERFILE" "$DOCKERFILE.bak.$(date +%s)"
|
||||
|
||||
sed -i '/COPY nodes\//a\
|
||||
COPY nodes/ '"$PACKAGE_NAME"' /opt/nextcloud-nodes/'"$PACKAGE_NAME"'\
|
||||
' "$DOCKERFILE"
|
||||
|
||||
echo " Dockerfile patched with COPY for $PACKAGE_NAME."
|
||||
fi
|
||||
|
||||
# 3. Patch entrypoint.sh to install woocommerce nodes at container start
|
||||
ENTRYPOINT="$NODERED_DIR/entrypoint.sh"
|
||||
echo "[integrate] Patching entrypoint.sh..."
|
||||
|
||||
if [[ -f "$ENTRYPOINT" ]]; then
|
||||
cp "$ENTRYPOINT" "$ENTRYPOINT.bak.$(date +%s)"
|
||||
|
||||
if ! grep -q "$PACKAGE_NAME" "$ENTRYPOINT"; then
|
||||
cat >> "$ENTRYPOINT" << 'PATCH_EOF'
|
||||
|
||||
# --- node-red-contrib-nextcloud-woocommerce ---
|
||||
SRC_WC="/opt/nextcloud-nodes/node-red-contrib-nextcloud-woocommerce"
|
||||
DST_WC="/data/node_modules/node-red-contrib-nextcloud-woocommerce"
|
||||
if [ -d "$SRC_WC" ]; then
|
||||
echo "[entrypoint] Installing woocommerce nodes into /data..."
|
||||
mkdir -p /data/node_modules
|
||||
rm -rf "$DST_WC"
|
||||
cp -a "$SRC_WC" /data/node_modules/
|
||||
chown -R node-red:node-red "$DST_WC" 2>/dev/null || true
|
||||
chmod -R a+rX "$DST_WC" 2>/dev/null || true
|
||||
echo "[entrypoint] WooCommerce nodes installed."
|
||||
fi
|
||||
PATCH_EOF
|
||||
echo " entrypoint.sh patched."
|
||||
else
|
||||
echo " entrypoint.sh already contains $PACKAGE_NAME."
|
||||
fi
|
||||
else
|
||||
echo " WARNING: entrypoint.sh not found at $ENTRYPOINT"
|
||||
fi
|
||||
|
||||
# 4. Create woocommerce nodes package in /opt for entrypoint to copy
|
||||
OPT_NODES_DIR="$NODERED_DIR/opt/nextcloud-nodes"
|
||||
echo "[integrate] Creating woocommerce nodes package in $OPT_NODES_DIR..."
|
||||
mkdir -p "$OPT_NODES_DIR/$PACKAGE_NAME/nodes"
|
||||
cp "$SCRIPT_DIR/nodes"/*.js "$OPT_NODES_DIR/$PACKAGE_NAME/nodes/"
|
||||
cp "$SCRIPT_DIR/nodes"/*.html "$OPT_NODES_DIR/$PACKAGE_NAME/nodes/"
|
||||
cp "$SCRIPT_DIR/nodes/package.json" "$OPT_NODES_DIR/$PACKAGE_NAME/package.json"
|
||||
echo " Package created."
|
||||
|
||||
# 5. Update docker-compose.yml to mount /opt/nextcloud-nodes
|
||||
COMPOSE="$NODERED_DIR/docker-compose.yml"
|
||||
if [[ -f "$COMPOSE" ]]; then
|
||||
echo "[integrate] Checking docker-compose.yml for woocommerce volume..."
|
||||
if ! grep -q "nextcloud-nodes" "$COMPOSE"; then
|
||||
cp "$COMPOSE" "$COMPOSE.bak.$(date +%s)"
|
||||
|
||||
sed -i '/- node-red-data:\/data/a\
|
||||
- nextcloud-nodes:/opt/nextcloud-nodes' "$COMPOSE"
|
||||
|
||||
sed -i '/^volumes:/a\
|
||||
nextcloud-nodes:' "$COMPOSE"
|
||||
|
||||
echo " docker-compose.yml updated with nextcloud-nodes volume."
|
||||
else
|
||||
echo " docker-compose.yml already has nextcloud-nodes volume."
|
||||
fi
|
||||
else
|
||||
echo " WARNING: docker-compose.yml not found at $COMPOSE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Integration Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Rebuild the Docker image:"
|
||||
echo " cd $NODERED_DIR && docker compose build --no-cache"
|
||||
echo ""
|
||||
echo "2. Restart the container:"
|
||||
echo " docker compose up -d"
|
||||
echo ""
|
||||
echo "3. In Node-RED, configure a 'woocommerce config' node with your Consumer Key + Secret."
|
||||
echo ""
|
||||
echo "4. Import example flows from: $SCRIPT_DIR/flows/"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user