node-red-contrib-my-tools 1.0.0
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.txt +69 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.html +443 -0
- package/node-red-contrib-mysql-extended-test/mysql-extended.js +355 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.html +153 -0
- package/node-red-contrib-rfid-helper/rfid-lazy.js +361 -0
- package/node-red-contrib-s7-plus/LICENSE +235 -0
- package/node-red-contrib-s7-plus/README.md +141 -0
- package/node-red-contrib-s7-plus/examples/read-multiple-values-flow.json +346 -0
- package/node-red-contrib-s7-plus/examples/read-write-test-flow.json +172 -0
- package/node-red-contrib-s7-plus/examples/write-single-values-flow.json +5583 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/README.md +72 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/areas.js +36 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/datatypes.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/flat-browser.js +433 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/index.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/lazy.js +397 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/node-id.js +22 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/resolve-symbolic.js +181 -0
- package/node-red-contrib-s7-plus/lib/s7plus/browse/vte-helpers.js +49 -0
- package/node-red-contrib-s7-plus/lib/s7plus/buffer-stream.js +96 -0
- package/node-red-contrib-s7-plus/lib/s7plus/client.js +1238 -0
- package/node-red-contrib-s7-plus/lib/s7plus/constants.js +155 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/index.js +3 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/s7-crc32.js +70 -0
- package/node-red-contrib-s7-plus/lib/s7plus/crc/symbol-crc.js +271 -0
- package/node-red-contrib-s7-plus/lib/s7plus/debug.js +88 -0
- package/node-red-contrib-s7-plus/lib/s7plus/explore-result.js +41 -0
- package/node-red-contrib-s7-plus/lib/s7plus/item-address.js +55 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-explore.js +77 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pdu-messages.js +359 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pobject.js +197 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue-codec.js +435 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvalue.js +653 -0
- package/node-red-contrib-s7-plus/lib/s7plus/pvar-lists.js +336 -0
- package/node-red-contrib-s7-plus/lib/s7plus/read-result.js +91 -0
- package/node-red-contrib-s7-plus/lib/s7plus/s7p.js +266 -0
- package/node-red-contrib-s7-plus/lib/s7plus/tag-routing.js +30 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/cotp-stream.js +335 -0
- package/node-red-contrib-s7-plus/lib/s7plus/transport/s7-transport.js +335 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.png +0 -0
- package/node-red-contrib-s7-plus/nodes/icons/s7complus.svg +17 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-endpoint.js +825 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-explore.js +91 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-in.js +255 -0
- package/node-red-contrib-s7-plus/nodes/s7complus-out.js +182 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.html +1541 -0
- package/node-red-contrib-s7-plus/nodes/s7complus.js +10 -0
- package/node-red-contrib-s7-plus/package.json +50 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Arrays.db +17 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Binary.db +16 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_BitStrings.db +36 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_CharacterStrings.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_DateAndTime.db +70 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_FloatingPoint.db +42 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Integers.db +72 -0
- package/node-red-contrib-s7-plus/plc/s7-1500/DB_Timers.db +40 -0
- package/node-red-contrib-s7-plus/scripts/example-flow-shared.js +39 -0
- package/node-red-contrib-s7-plus/scripts/generate-read-multiple-flow.js +331 -0
- package/node-red-contrib-s7-plus/scripts/generate-rw-test-flow.js +690 -0
- package/node-red-contrib-s7-plus/scripts/generate-write-flow.js +476 -0
- package/node-red-contrib-s7-plus/scripts/layout-write-flow.js +156 -0
- package/node-red-contrib-s7-plus/scripts/run-tests.js +16 -0
- package/node-red-flowgobal-change/variable-change.html +425 -0
- package/node-red-flowgobal-change/variable-change.js +92 -0
- package/node-red-ui_media_viewer/ui_media_viewer.html +87 -0
- package/node-red-ui_media_viewer/ui_media_viewer.js +136 -0
- package/package.json +12 -0
package/README.txt
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# node-red-contrib-my-tools
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/node-red-contrib-my-tools)
|
|
4
|
+
[](https://www.npmjs.com/package/node-red-contrib-my-tools)
|
|
5
|
+
|
|
6
|
+
An all-in-one Node-RED utility toolkit designed for industrial automation, IoT integration, and system management.
|
|
7
|
+
|
|
8
|
+
All nodes are automatically organized under the custom **`🛠️ my-tools`** palette category upon installation[cite: 1, 2, 3, 4].
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 🚀 Key Features
|
|
13
|
+
|
|
14
|
+
* **Unified Package**: Install a single package to access your entire suite of custom nodes.
|
|
15
|
+
* **Neat Categorization**: Avoids clutter by grouping all custom nodes into the `my-tools` category[cite: 1, 2, 3, 4].
|
|
16
|
+
* **Industrial Grade Reliability**: Built with robust anti-collision RFID handling[cite: 2], self-healing S7 PLC communications[cite: 6], and visual helper nodes[cite: 1, 3, 4].
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🛠️ Included Nodes
|
|
21
|
+
|
|
22
|
+
### 1. 💾 MySQL SQLGen (`mysql-extended`)[cite: 1]
|
|
23
|
+
A visual SQL generator and database schema assistant[cite: 1].
|
|
24
|
+
* **Action Modes**: Easily configure `SELECT`, `INSERT`, `UPDATE`, and `DELETE` queries[cite: 1].
|
|
25
|
+
* **Visual Joins**: Add `INNER JOIN` or `LEFT JOIN` dynamically with automatic alias assignment[cite: 1].
|
|
26
|
+
* **Database Autocomplete**: Automatically fetches your database schema upon deployment to suggest tables and columns[cite: 1].
|
|
27
|
+
* **Manual Override**: Keeps auto-generation active while letting you manually edit the final SQL output[cite: 1].
|
|
28
|
+
|
|
29
|
+
### 2. 🏷️ RFID Smart Module (`rfid-lazy`)[cite: 2]
|
|
30
|
+
An optimized RFID read/write controller designed for maximum stability with USER memory banks[cite: 2].
|
|
31
|
+
* **Stable Reading**: Limits single-read operations to 16 Words to prevent communication dropouts[cite: 2].
|
|
32
|
+
* **Auto-Split Writing**: Automatically splits long string payloads and writes them in batches using a background anti-collision delay[cite: 2].
|
|
33
|
+
* **Departure Alert**: Triggers an output notification when a tag is removed, with a customizable delay timeout[cite: 2].
|
|
34
|
+
|
|
35
|
+
### 3. 📊 Variables Manager (`variable-change`)[cite: 3]
|
|
36
|
+
A visual variable manager supporting Flow and Global contexts with drag-and-drop sorting[cite: 3].
|
|
37
|
+
* **Two Operational Modes**: Run in "Realtime" mode to edit active variables instantly, or "Initialization" mode to set initial values when input arrives[cite: 3].
|
|
38
|
+
* **Relaxed JSON Support**: Accepts lax JSON structures (such as unquoted keys or single quotes) and auto-corrects them to strict JSON[cite: 3].
|
|
39
|
+
* **Drag-and-Drop Sorting**: Reorder your variable definitions effortlessly by dragging the handle[cite: 3].
|
|
40
|
+
|
|
41
|
+
### 4. 📺 Media Viewer (`ui_media_viewer`)[cite: 4]
|
|
42
|
+
*Note: Requires Node-RED Dashboard to be installed[cite: 4].*
|
|
43
|
+
* **Dynamic Media Rendering**: Pass absolute file paths (such as `C:\test.mp4` or image paths) in `msg.payload` or `msg.filepath` to display media dynamically on your dashboard[cite: 4].
|
|
44
|
+
* **Auto Format Detection**: Automatically switches display engines based on file extensions (e.g., handles MP4 files with a video player)[cite: 4].
|
|
45
|
+
* **Reset Trigger**: Clear the rendering frame by sending a `"reset"` or `"clear"` string in `msg.payload`[cite: 4].
|
|
46
|
+
|
|
47
|
+
### 5. 🔌 Siemens S7-Plus Suite (`node-red-contrib-s7-plus`)[cite: 5]
|
|
48
|
+
An optimized, high-performance S7 communication suite for Siemens S7-1200 and S7-1500 PLCs[cite: 7].
|
|
49
|
+
* **S7-Plus Endpoint**[cite: 5]: The communication engine featuring watchdog reconnects[cite: 6], symbol resolution caching[cite: 6], and automatic self-healing on CRC errors when the PLC program changes[cite: 6].
|
|
50
|
+
* **S7-Plus Read**[cite: 8]: Supports reading multiple tags[cite: 8], optional value-only outputs[cite: 8], and differential filtering (outputs only when values change)[cite: 8].
|
|
51
|
+
* **S7-Plus Write**[cite: 9]: Safely writes values back to PLC symbolic or hex paths with CRC verification[cite: 9].
|
|
52
|
+
* **S7-Plus Explore**[cite: 7]: Scans and browses the PLC program hierarchy to output the entire symbolic structure[cite: 7].
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 📦 Installation
|
|
57
|
+
|
|
58
|
+
### Method 1: Via Palette Manager (Recommended)
|
|
59
|
+
1. Open Node-RED, click the top-right menu icon, and select **Manage palette**.
|
|
60
|
+
2. Go to the **Install** tab.
|
|
61
|
+
3. Search for `node-red-contrib-my-tools`.
|
|
62
|
+
4. Click **Install**.
|
|
63
|
+
|
|
64
|
+
### Method 2: Via Terminal
|
|
65
|
+
Navigate to your Node-RED user directory (typically `~/.node-red`) and run the install command:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd ~/.node-red
|
|
69
|
+
npm install node-red-contrib-my-tools
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
var joinsListInitialised = false;
|
|
4
|
+
var suspendPreview = false;
|
|
5
|
+
var debounceTimer = null;
|
|
6
|
+
var joinsModel = []; // source of truth
|
|
7
|
+
var dbSchema = { tables: [], allColumns: [] }; // 存放自動補齊的 Schema 資料
|
|
8
|
+
|
|
9
|
+
function safe(fn){ try { return fn(); } catch(e){ console.error("mysql-extended editor error:", e); } }
|
|
10
|
+
function debounce(fn, wait) { return function(){ clearTimeout(debounceTimer); var a=arguments; debounceTimer=setTimeout(function(){ safe(function(){ fn.apply(null,a); }); }, wait||120); } }
|
|
11
|
+
function normalizeOp(op){ return op === "==" ? "=" : op; }
|
|
12
|
+
function autoAliasFor(table){ if(!table)return""; if(table==="oc_order")return"oo"; if(table==="oc_order_product")return"ooc"; var b=table.replace(/^oc_/,""); var p=b.split("_").filter(Boolean); if(!p.length) return table[0]; return p.map(function(x){return x[0];}).join(""); }
|
|
13
|
+
function qField(a,f){ if(!f)return""; if(f.indexOf(".")!==-1||/\bas\b/i.test(f)||f.indexOf("(")!==-1) return f; if(f==="*") return a+".*"; return a+"."+f; }
|
|
14
|
+
|
|
15
|
+
function formatValForPreview(v, src) {
|
|
16
|
+
if (!v) return "";
|
|
17
|
+
if (src === "flow") return "{{flow." + v + "}}";
|
|
18
|
+
if (src === "global") return "{{global." + v + "}}";
|
|
19
|
+
return v;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function syncHiddenFromModel(){ $("#node-input-joinsJSON").val(JSON.stringify(joinsModel||[])); }
|
|
23
|
+
function renderJoinsFromModel(){ if(!$.fn.editableList){ return; } var el=$("#node-input-joins"); safe(function(){ el.editableList('empty'); }); (joinsModel||[]).forEach(function(j){ safe(function(){ el.editableList('addItem', j); }); }); }
|
|
24
|
+
function rebuildModelFromDOM(){
|
|
25
|
+
if(!$.fn.editableList) return;
|
|
26
|
+
var items=$("#node-input-joins").editableList('items'); var tmp=[];
|
|
27
|
+
items.each(function(){ var r=$(this);
|
|
28
|
+
var type=r.find(".join-type").val();
|
|
29
|
+
var table=(r.find(".join-table").val()||"").trim();
|
|
30
|
+
var alias=(r.find(".join-alias").val()||"").trim()||autoAliasFor(table);
|
|
31
|
+
var key=(r.find(".join-key").val()||"").trim();
|
|
32
|
+
var fields=(r.find(".join-fields").val()||"").trim();
|
|
33
|
+
tmp.push({type:type, table:table, alias:alias, key:key, fields:fields});
|
|
34
|
+
});
|
|
35
|
+
joinsModel=tmp; syncHiddenFromModel();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// === Autocomplete 輔助函數 ===
|
|
39
|
+
function singleAutocomplete(el, sourceData) {
|
|
40
|
+
$(el).autocomplete({ source: sourceData, minLength: 1, select: function() { setTimeout(function(){ $(el).trigger("change"); }, 10); } });
|
|
41
|
+
}
|
|
42
|
+
function bindMultiAutocomplete(el, sourceData) {
|
|
43
|
+
$(el).on("keydown", function (event) {
|
|
44
|
+
if (event.keyCode === $.ui.keyCode.TAB && $(this).data("ui-autocomplete") && $(this).data("ui-autocomplete").menu.active) { event.preventDefault(); }
|
|
45
|
+
}).autocomplete({
|
|
46
|
+
minLength: 1,
|
|
47
|
+
source: function (request, response) {
|
|
48
|
+
var terms = request.term.split(/,\s*/);
|
|
49
|
+
var term = terms.pop();
|
|
50
|
+
if (term.trim().length === 0) { response([]); return; }
|
|
51
|
+
response($.ui.autocomplete.filter(sourceData, term));
|
|
52
|
+
},
|
|
53
|
+
focus: function () { return false; },
|
|
54
|
+
select: function (event, ui) {
|
|
55
|
+
var terms = this.value.split(/,\s*/);
|
|
56
|
+
terms.pop();
|
|
57
|
+
terms.push(ui.item.value);
|
|
58
|
+
terms.push("");
|
|
59
|
+
this.value = terms.join(", ");
|
|
60
|
+
$(this).trigger("change");
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function applyAutocompletes() {
|
|
67
|
+
if (!dbSchema || dbSchema.tables.length === 0) return;
|
|
68
|
+
// 單選 Table
|
|
69
|
+
singleAutocomplete("#node-input-mainTable, #node-input-iTable, #node-input-uTable, #node-input-dTable", dbSchema.tables);
|
|
70
|
+
// 單選 Column
|
|
71
|
+
singleAutocomplete("#node-input-whereField, #node-input-uWhereField, #node-input-dWhereField", dbSchema.allColumns);
|
|
72
|
+
// 多選 Column (支援逗號分隔)
|
|
73
|
+
bindMultiAutocomplete("#node-input-mainFields, #node-input-orderByFields, #node-input-iColumns, #node-input-uSet", dbSchema.allColumns);
|
|
74
|
+
|
|
75
|
+
// 更新現有的 JOIN 表單
|
|
76
|
+
$(".join-table").each(function(){ singleAutocomplete(this, dbSchema.tables); });
|
|
77
|
+
$(".join-fields").each(function(){ bindMultiAutocomplete(this, dbSchema.allColumns); });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fetchDbSchema() {
|
|
81
|
+
var dbId = $("#node-input-mydb").val();
|
|
82
|
+
if (dbId && dbId !== "_ADD_") {
|
|
83
|
+
$.getJSON("mysql-extended/schema/" + dbId, function(data) {
|
|
84
|
+
dbSchema = data;
|
|
85
|
+
applyAutocompletes();
|
|
86
|
+
}).fail(function(){
|
|
87
|
+
console.log("MySQL Schema fetch failed (Deploy node first to enable autocomplete).");
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// === SQL 組合區 ===
|
|
93
|
+
function buildSelectPreview(){
|
|
94
|
+
var mt=$("#node-input-mainTable").val().trim();
|
|
95
|
+
var ma=($("#node-input-mainAlias").val().trim()||autoAliasFor(mt));
|
|
96
|
+
var mf=$("#node-input-mainFields").val().trim();
|
|
97
|
+
var wM=$("#node-input-whereManual").val().trim();
|
|
98
|
+
var wF=$("#node-input-whereField").val().trim();
|
|
99
|
+
var wO=$("#node-input-whereOp").val();
|
|
100
|
+
var wVSrc=$("#node-input-whereVSrc").val();
|
|
101
|
+
var wV=$("#node-input-whereValue").val().trim();
|
|
102
|
+
var wVFormatted = formatValForPreview(wV, wVSrc);
|
|
103
|
+
|
|
104
|
+
var obF=$("#node-input-orderByFields").val().trim();
|
|
105
|
+
var obD=($("#node-input-orderDir").val()||"ASC").toUpperCase()==="DESC"?"DESC":"ASC";
|
|
106
|
+
if(!mt) return "";
|
|
107
|
+
var mainList=mf?mf.split(",").map(function(s){return s.trim();}).filter(Boolean):[];
|
|
108
|
+
var fields=mainList.length?mainList.map(function(f){return qField(ma,f);}):[];
|
|
109
|
+
var from=mt+" AS "+ma;
|
|
110
|
+
(joinsModel||[]).forEach(function(j){
|
|
111
|
+
var table=(j.table||"").trim(); if(!table) return;
|
|
112
|
+
var type=j.type||"JOIN";
|
|
113
|
+
var key=(j.key||"id").trim();
|
|
114
|
+
var alias=(j.alias&&j.alias.trim())||autoAliasFor(table);
|
|
115
|
+
var jf=(j.fields||"");
|
|
116
|
+
from+=" "+type+" "+table+" AS "+alias+" ON "+ma+"."+key+" = "+alias+"."+key;
|
|
117
|
+
if(jf){ jf.split(",").map(function(s){return s.trim();}).filter(Boolean).forEach(function(f){ fields.push(qField(alias,f)); }); }
|
|
118
|
+
});
|
|
119
|
+
if(fields.length===0) fields=[ma+".*"];
|
|
120
|
+
var where=wM;
|
|
121
|
+
if(!where&&wF&&wO&&wV){ var q=(wF.indexOf(".")!==-1)?wF:qField(ma,wF); where=q+" "+normalizeOp(wO)+" "+wVFormatted; }
|
|
122
|
+
var ob="";
|
|
123
|
+
if(obF){ var list=obF.split(",").map(function(s){return s.trim();}).filter(Boolean); if(list.length){ var parts=list.map(function(f){ return (f.indexOf(".")!==-1||/\bas\b/i.test(f))?f:qField(ma,f); }); ob=" ORDER BY "+parts.join(", ")+" "+obD; } }
|
|
124
|
+
var sql="SELECT "+fields.join(", ")+" FROM "+from; if(where) sql+=" WHERE "+where; if(ob) sql+=ob; return sql;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildInsertPreview(){ var t=$("#node-input-iTable").val().trim(); var c=$("#node-input-iColumns").val().trim(); var v=$("#node-input-iValues").val().trim(); if(!t||!c||!v) return ""; return "INSERT INTO "+t+" ("+c+") VALUES ("+v+")"; }
|
|
128
|
+
|
|
129
|
+
function buildUpdatePreview(){
|
|
130
|
+
var t=$("#node-input-uTable").val().trim();
|
|
131
|
+
var s=$("#node-input-uSet").val().trim();
|
|
132
|
+
var wM=$("#node-input-uWhereManual").val().trim();
|
|
133
|
+
var wF=$("#node-input-uWhereField").val().trim();
|
|
134
|
+
var wO=$("#node-input-uWhereOp").val();
|
|
135
|
+
var wVSrc=$("#node-input-uWhereVSrc").val();
|
|
136
|
+
var wV=$("#node-input-uWhereValue").val().trim();
|
|
137
|
+
var wVFormatted = formatValForPreview(wV, wVSrc);
|
|
138
|
+
|
|
139
|
+
if(!t||!s) return "";
|
|
140
|
+
var sql="UPDATE "+t+" SET "+s;
|
|
141
|
+
var w=wM;
|
|
142
|
+
if(!w&&wF&&wO&&wV){ var q=(wF.indexOf(".")!==-1)?wF:(t+"."+wF); w=q+" "+normalizeOp(wO)+" "+wVFormatted; }
|
|
143
|
+
if(w) sql+=" WHERE "+w; return sql;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildDeletePreview(){
|
|
147
|
+
var t=$("#node-input-dTable").val().trim();
|
|
148
|
+
var wM=$("#node-input-dWhereManual").val().trim();
|
|
149
|
+
var wF=$("#node-input-dWhereField").val().trim();
|
|
150
|
+
var wO=$("#node-input-dWhereOp").val();
|
|
151
|
+
var wVSrc=$("#node-input-dWhereVSrc").val();
|
|
152
|
+
var wV=$("#node-input-dWhereValue").val().trim();
|
|
153
|
+
var wVFormatted = formatValForPreview(wV, wVSrc);
|
|
154
|
+
|
|
155
|
+
if(!t) return "";
|
|
156
|
+
var sql="DELETE FROM "+t;
|
|
157
|
+
var w=wM;
|
|
158
|
+
if(!w&&wF&&wO&&wV){ var q=(wF.indexOf(".")!==-1)?wF:(t+"."+wF); w=q+" "+normalizeOp(wO)+" "+wVFormatted; }
|
|
159
|
+
if(w) sql+=" WHERE "+w; return sql;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
var updatePreviewSticky = debounce(function(){
|
|
163
|
+
if(suspendPreview) return;
|
|
164
|
+
var auto=$("#node-input-autoGenerate").is(":checked");
|
|
165
|
+
var sqlEl=$("#node-input-sqlCustom"); var last=$("#node-input-lastAutoSQL");
|
|
166
|
+
if(!auto) return;
|
|
167
|
+
var a=$("#node-input-action").val(); var text="";
|
|
168
|
+
if(a==="SELECT") text=buildSelectPreview();
|
|
169
|
+
else if(a==="INSERT") text=buildInsertPreview();
|
|
170
|
+
else if(a==="UPDATE") text=buildUpdatePreview();
|
|
171
|
+
else if(a==="DELETE") text=buildDeletePreview();
|
|
172
|
+
if(sqlEl.val().trim()===last.val().trim()||sqlEl.val().trim()===""){ sqlEl.val(text); last.val(text); }
|
|
173
|
+
}, 120);
|
|
174
|
+
|
|
175
|
+
function showSectionByAction(){
|
|
176
|
+
var act=$("#node-input-action").val();
|
|
177
|
+
suspendPreview=true;
|
|
178
|
+
$(".section-select").toggle(act==="SELECT");
|
|
179
|
+
$(".section-insert").toggle(act==="INSERT");
|
|
180
|
+
$(".section-update").toggle(act==="UPDATE");
|
|
181
|
+
$(".section-delete").toggle(act==="DELETE");
|
|
182
|
+
if(act==="SELECT") safe(renderJoinsFromModel);
|
|
183
|
+
suspendPreview=false;
|
|
184
|
+
updatePreviewSticky();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensureJoinsList(){
|
|
188
|
+
try { var __el=$('#node-input-joins'); if(__el && __el.data && __el.data('editableList')) __el.editableList('destroy'); } catch(e){} joinsListInitialised=false; if(!$.fn.editableList){ return; }
|
|
189
|
+
$("#node-input-joins").editableList({
|
|
190
|
+
addItem: function(container, i, data){
|
|
191
|
+
var wrap=$('<div class="join-wrap"></div>').appendTo(container);
|
|
192
|
+
var row1=$('<div class="join-grid row-top"></div>').appendTo(wrap);
|
|
193
|
+
var row2=$('<div class="join-grid row-bottom"></div>').appendTo(wrap);
|
|
194
|
+
var typeEl=$('<select class="join-type"><option value="JOIN">INNER JOIN</option><option value="LEFT JOIN">LEFT JOIN</option></select>').appendTo(row1);
|
|
195
|
+
var tableEl=$('<input class="join-table" type="text" placeholder="JOIN table">').appendTo(row1);
|
|
196
|
+
var aliasEl=$('<input class="join-alias" type="text" placeholder="alias (自動可留空)">').appendTo(row1);
|
|
197
|
+
var keyEl=$('<input class="join-key" type="text" placeholder="key (ex: order_id)">').appendTo(row2);
|
|
198
|
+
var fieldsEl=$('<input class="join-fields" type="text" placeholder="fields (ex: product_id, price)">').appendTo(row2);
|
|
199
|
+
|
|
200
|
+
if (data){ if(data.type) typeEl.val(data.type); if(data.table) tableEl.val(data.table); if(data.alias) aliasEl.val(data.alias); if(data.key) keyEl.val(data.key); if(data.fields) fieldsEl.val(data.fields); }
|
|
201
|
+
var updateFromDOM = debounce(function(){ rebuildModelFromDOM(); updatePreviewSticky(); }, 80);
|
|
202
|
+
wrap.find("input,select").on("input change", updateFromDOM);
|
|
203
|
+
|
|
204
|
+
// 為新加入的欄位綁定自動補齊
|
|
205
|
+
if (dbSchema && dbSchema.tables.length > 0) {
|
|
206
|
+
singleAutocomplete(tableEl, dbSchema.tables);
|
|
207
|
+
bindMultiAutocomplete(fieldsEl, dbSchema.allColumns);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
removable:true, sortable:true, height: 320, addButton:true,
|
|
211
|
+
removeItem:function(){ rebuildModelFromDOM(); updatePreviewSticky(); },
|
|
212
|
+
sortItems:function(){ rebuildModelFromDOM(); updatePreviewSticky(); }
|
|
213
|
+
});
|
|
214
|
+
joinsListInitialised=true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
RED.nodes.registerType('mysql-extended', {
|
|
218
|
+
category:'mytools', color:'#e0f0ff',
|
|
219
|
+
defaults:{
|
|
220
|
+
mydb: {type:"MySQLdatabase", required:true}, // 加入資料庫連線選擇
|
|
221
|
+
dWhereVSrc:{value:"value"},
|
|
222
|
+
uWhereVSrc:{value:"value"},
|
|
223
|
+
whereVSrc:{value:"value"},
|
|
224
|
+
name:{value:""}, action:{value:"SELECT"},
|
|
225
|
+
mainTable:{value:""}, mainAlias:{value:""}, mainFields:{value:""},
|
|
226
|
+
joinsJSON:{value:"[]"},
|
|
227
|
+
whereField:{value:""}, whereOp:{value:"="}, whereValue:{value:""}, whereManual:{value:""},
|
|
228
|
+
orderByFields:{value:""}, orderDir:{value:"ASC"},
|
|
229
|
+
iTable:{value:""}, iColumns:{value:""}, iValues:{value:""},
|
|
230
|
+
uTable:{value:""}, uSet:{value:""}, uWhereManual:{value:""}, uWhereField:{value:""}, uWhereOp:{value:"="}, uWhereValue:{value:""},
|
|
231
|
+
dTable:{value:""}, dWhereManual:{value:""}, dWhereField:{value:""}, dWhereOp:{value:"="}, dWhereValue:{value:""},
|
|
232
|
+
autoGenerate:{value:true}, sqlCustom:{value:""}, lastAutoSQL:{value:""}
|
|
233
|
+
},
|
|
234
|
+
inputs:1, outputs:1, icon:"db.png",
|
|
235
|
+
label:function(){ return this.name||"MySQL SQLGen"; },
|
|
236
|
+
oneditprepare:function(){
|
|
237
|
+
safe(function(){
|
|
238
|
+
joinsListInitialised=false; ensureJoinsList();
|
|
239
|
+
var raw=$("#node-input-joinsJSON").val()||"[]"; try{ joinsModel=JSON.parse(raw); if(!Array.isArray(joinsModel)) joinsModel=[]; }catch(e){ joinsModel=[]; }
|
|
240
|
+
renderJoinsFromModel();
|
|
241
|
+
$("#node-input-sqlCustom").on("input.mysqlx", function(){ $("#node-input-autoGenerate").prop("checked", false); });
|
|
242
|
+
$("#node-input-mainTable,#node-input-mainAlias,#node-input-mainFields,#node-input-whereManual,#node-input-whereField,#node-input-whereOp,#node-input-whereValue,#node-input-whereVSrc,#node-input-orderByFields,#node-input-orderDir").on("input.mysqlx change.mysqlx", updatePreviewSticky);
|
|
243
|
+
$("#node-input-iTable,#node-input-iColumns,#node-input-iValues").on("input.mysqlx change.mysqlx", updatePreviewSticky);
|
|
244
|
+
$("#node-input-uTable,#node-input-uSet,#node-input-uWhereManual,#node-input-uWhereField,#node-input-uWhereOp,#node-input-uWhereValue,#node-input-uWhereVSrc").on("input.mysqlx change.mysqlx", updatePreviewSticky);
|
|
245
|
+
$("#node-input-dTable,#node-input-dWhereManual,#node-input-dWhereField,#node-input-dWhereOp,#node-input-dWhereValue,#node-input-dWhereVSrc").on("input.mysqlx change.mysqlx", updatePreviewSticky);
|
|
246
|
+
$("#node-input-action,#node-input-autoGenerate").on("change.mysqlx", function(){ showSectionByAction(); });
|
|
247
|
+
|
|
248
|
+
showSectionByAction();
|
|
249
|
+
updatePreviewSticky();
|
|
250
|
+
|
|
251
|
+
// 資料庫選擇變更時,抓取 Schema 供 AutoComplete 使用
|
|
252
|
+
$("#node-input-mydb").change(fetchDbSchema);
|
|
253
|
+
// 首次開啟節點時主動抓一次
|
|
254
|
+
fetchDbSchema();
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
oneditsave:function(){
|
|
258
|
+
try{ var __el=$('#node-input-joins'); if(__el.data('editableList')) __el.editableList('destroy'); }catch(e){} joinsListInitialised=false;
|
|
259
|
+
safe(function(){ syncHiddenFromModel(); $("#node-input-lastAutoSQL").val($("#node-input-sqlCustom").val()); });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
})();
|
|
263
|
+
</script>
|
|
264
|
+
|
|
265
|
+
<script type="text/html" data-template-name="mysql-extended">
|
|
266
|
+
<style>
|
|
267
|
+
.section { border: 1px solid #ddd; padding: 10px 12px; border-radius: 8px; margin-bottom: 12px; background:#f8f9fb; }
|
|
268
|
+
.section h4 { margin: 0 0 8px 0; font-weight: 600; }
|
|
269
|
+
.row-wide { display:flex; gap:14px; align-items:center; }
|
|
270
|
+
.row-wide > * { flex: 1; }
|
|
271
|
+
#node-input-joins .red-ui-editableList-item-content { padding: 6px; }
|
|
272
|
+
#node-input-joins .join-wrap { display:block; width:100%; }
|
|
273
|
+
#node-input-joins .join-grid { display:grid; grid-template-columns: 180px 1fr 180px; gap:10px; align-items:center; width:100%; }
|
|
274
|
+
#node-input-joins .row-bottom { grid-template-columns: 220px 1fr; }
|
|
275
|
+
#node-input-joins input[type="text"], #node-input-joins select { width:100%; box-sizing:border-box; }
|
|
276
|
+
#node-input-joins { border:1px solid #e3e3e3; border-radius:8px; background:#fafafa; }
|
|
277
|
+
#node-input-joins .red-ui-editableList-container { max-height: 320px; overflow-y:auto; }
|
|
278
|
+
#node-input-joins .red-ui-editableList-item-content { padding:8px 8px; }
|
|
279
|
+
#node-input-joins .join-grid.row-top { display:grid; grid-template-columns: 140px 1fr 160px; gap:10px; align-items:center; }
|
|
280
|
+
#node-input-joins .join-grid.row-bottom { display:grid; grid-template-columns: 220px 1fr; gap:10px; align-items:center; }
|
|
281
|
+
|
|
282
|
+
/* Autocomplete Dropdown Z-Index Fix */
|
|
283
|
+
.ui-autocomplete { max-height: 250px; overflow-y: auto; overflow-x: hidden; z-index: 10000 !important; }
|
|
284
|
+
</style>
|
|
285
|
+
|
|
286
|
+
<div class="form-row">
|
|
287
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
288
|
+
<input type="text" id="node-input-name" placeholder="Node Name" style="width:100%">
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div class="form-row">
|
|
292
|
+
<label for="node-input-mydb"><i class="fa fa-database"></i> Database</label>
|
|
293
|
+
<input type="text" id="node-input-mydb" style="width:100%">
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="form-row">
|
|
297
|
+
<label for="node-input-action"><i class="fa fa-cogs"></i> Action</label>
|
|
298
|
+
<select id="node-input-action" style="width:260px">
|
|
299
|
+
<option value="SELECT">SELECT</option>
|
|
300
|
+
<option value="INSERT">INSERT</option>
|
|
301
|
+
<option value="UPDATE">UPDATE</option>
|
|
302
|
+
<option value="DELETE">DELETE</option>
|
|
303
|
+
</select>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<input type="hidden" id="node-input-joinsJSON">
|
|
307
|
+
|
|
308
|
+
<div class="section section-select">
|
|
309
|
+
<h4>SELECT</h4>
|
|
310
|
+
<div class="form-row row-wide">
|
|
311
|
+
<div>
|
|
312
|
+
<label for="node-input-mainTable"><i class="fa fa-table"></i> Main table</label>
|
|
313
|
+
<input type="text" id="node-input-mainTable" placeholder="例如:oc_order" style="width:100%">
|
|
314
|
+
</div>
|
|
315
|
+
<div style="max-width:240px">
|
|
316
|
+
<label for="node-input-mainAlias"><i class="fa fa-i-cursor"></i> Alias</label>
|
|
317
|
+
<input type="text" id="node-input-mainAlias" placeholder="預設自動(例如 oc_order → oo)" style="width:100%">
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="form-row">
|
|
321
|
+
<label for="node-input-mainFields"><i class="fa fa-columns"></i> Fields from main</label>
|
|
322
|
+
<input type="text" id="node-input-mainFields" placeholder="例如:order_id, total, name(自動加 alias 前綴)" style="width:100%">
|
|
323
|
+
</div>
|
|
324
|
+
<div class="form-row">
|
|
325
|
+
<label><i class="fa fa-link"></i> JOINS</label>
|
|
326
|
+
<ol id="node-input-joins" style="min-height: 36px;; max-height: 340px; overflow-y: auto; display:block; margin-bottom:8px"></ol>
|
|
327
|
+
<p class="form-tips">按 <b>Add</b> 逐筆新增 JOIN。每筆:類型 / JOIN table / alias;第二列:key / fields(欄位會自動加上該 alias 前綴)。</p>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="form-row">
|
|
330
|
+
<label><i class="fa fa-filter"></i> WHERE</label>
|
|
331
|
+
<div class="row-wide">
|
|
332
|
+
<input type="text" id="node-input-whereField" placeholder="欄位(不填別名時自動使用 main alias)">
|
|
333
|
+
<select id="node-input-whereOp" style="max-width:120px">
|
|
334
|
+
<option value=">">></option>
|
|
335
|
+
<option value=">=">≥</option>
|
|
336
|
+
<option value="<"><</option>
|
|
337
|
+
<option value="<=">≤</option>
|
|
338
|
+
<option value="==">==</option>
|
|
339
|
+
<option value="=">=</option>
|
|
340
|
+
</select>
|
|
341
|
+
<input type="text" id="node-input-whereValue" placeholder="值(字串需自行加引號)">
|
|
342
|
+
<select id="node-input-whereVSrc"><option value="value">一般值</option><option value="flow">flow</option><option value="global">global</option></select>
|
|
343
|
+
</div>
|
|
344
|
+
<p class="form-tips">或使用下方「手動 WHERE」覆蓋:</p>
|
|
345
|
+
<input type="text" id="node-input-whereManual" placeholder="手動 WHERE(直接輸入條件,不含 WHERE)" style="width:100%">
|
|
346
|
+
</div>
|
|
347
|
+
<div class="form-row">
|
|
348
|
+
<label><i class="fa fa-sort"></i> ORDER BY</label>
|
|
349
|
+
<div class="row-wide">
|
|
350
|
+
<input type="text" id="node-input-orderByFields" placeholder="欄位(可多個,用逗號分隔)">
|
|
351
|
+
<select id="node-input-orderDir" style="max-width:140px">
|
|
352
|
+
<option value="ASC">ASC</option>
|
|
353
|
+
<option value="DESC">DESC</option>
|
|
354
|
+
</select>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div class="section section-insert">
|
|
360
|
+
<h4>INSERT</h4>
|
|
361
|
+
<div class="form-row">
|
|
362
|
+
<label for="node-input-iTable"><i class="fa fa-table"></i> Table</label>
|
|
363
|
+
<input type="text" id="node-input-iTable" placeholder="例如:oc_order" style="width:100%">
|
|
364
|
+
</div>
|
|
365
|
+
<div class="form-row">
|
|
366
|
+
<label for="node-input-iColumns"><i class="fa fa-columns"></i> Columns</label>
|
|
367
|
+
<input type="text" id="node-input-iColumns" placeholder="例如:order_id, total, status" style="width:100%">
|
|
368
|
+
</div>
|
|
369
|
+
<div class="form-row">
|
|
370
|
+
<label for="node-input-iValues"><i class="fa fa-pencil"></i> Values</label>
|
|
371
|
+
<input type="text" id="node-input-iValues" placeholder="例如:{{msg.order_id}}, {{msg.total}}, 'complete'" style="width:100%">
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div class="section section-update">
|
|
376
|
+
<h4>UPDATE</h4>
|
|
377
|
+
<div class="form-row">
|
|
378
|
+
<label for="node-input-uTable"><i class="fa fa-table"></i> Table</label>
|
|
379
|
+
<input type="text" id="node-input-uTable" placeholder="例如:oc_order" style="width:100%">
|
|
380
|
+
</div>
|
|
381
|
+
<div class="form-row">
|
|
382
|
+
<label for="node-input-uSet"><i class="fa fa-edit"></i> SET</label>
|
|
383
|
+
<input type="text" id="node-input-uSet" placeholder="例如:status='complete', total={{msg.total}}" style="width:100%">
|
|
384
|
+
</div>
|
|
385
|
+
<div class="form-row">
|
|
386
|
+
<label><i class="fa fa-filter"></i> WHERE</label>
|
|
387
|
+
<div class="row-wide">
|
|
388
|
+
<input type="text" id="node-input-uWhereField" placeholder="欄位(會自動補 table.欄位)">
|
|
389
|
+
<select id="node-input-uWhereOp" style="max-width:120px">
|
|
390
|
+
<option value=">">></option>
|
|
391
|
+
<option value=">=">≥</option>
|
|
392
|
+
<option value="<"><</option>
|
|
393
|
+
<option value="<=">≤</option>
|
|
394
|
+
<option value="==">==</option>
|
|
395
|
+
<option value="=">=</option>
|
|
396
|
+
</select>
|
|
397
|
+
<input type="text" id="node-input-uWhereValue" placeholder="值">
|
|
398
|
+
<select id="node-input-uWhereVSrc"><option value="value">一般值</option><option value="flow">flow</option><option value="global">global</option></select>
|
|
399
|
+
</div>
|
|
400
|
+
<p class="form-tips">或使用下方「手動 WHERE」覆蓋:</p>
|
|
401
|
+
<input type="text" id="node-input-uWhereManual" placeholder="手動 WHERE(直接輸入條件,不含 WHERE)" style="width:100%">
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div class="section section-delete">
|
|
406
|
+
<h4>DELETE</h4>
|
|
407
|
+
<div class="form-row">
|
|
408
|
+
<label for="node-input-dTable"><i class="fa fa-table"></i> Table</label>
|
|
409
|
+
<input type="text" id="node-input-dTable" placeholder="例如:oc_order" style="width:100%">
|
|
410
|
+
</div>
|
|
411
|
+
<div class="form-row">
|
|
412
|
+
<label><i class="fa fa-filter"></i> WHERE</label>
|
|
413
|
+
<div class="row-wide">
|
|
414
|
+
<input type="text" id="node-input-dWhereField" placeholder="欄位(會自動補 table.欄位)">
|
|
415
|
+
<select id="node-input-dWhereOp" style="max-width:120px">
|
|
416
|
+
<option value=">">></option>
|
|
417
|
+
<option value=">=">≥</option>
|
|
418
|
+
<option value="<"><</option>
|
|
419
|
+
<option value="<=">≤</option>
|
|
420
|
+
<option value="==">==</option>
|
|
421
|
+
<option value="=">=</option>
|
|
422
|
+
</select>
|
|
423
|
+
<input type="text" id="node-input-dWhereValue" placeholder="值">
|
|
424
|
+
<select id="node-input-dWhereVSrc"><option value="value">一般值</option><option value="flow">flow</option><option value="global">global</option></select>
|
|
425
|
+
</div>
|
|
426
|
+
<p class="form-tips">或使用下方「手動 WHERE」覆蓋:</p>
|
|
427
|
+
<input type="text" id="node-input-dWhereManual" placeholder="手動 WHERE(直接輸入條件,不含 WHERE)" style="width:100%">
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="section">
|
|
432
|
+
<h4>SQL Preview / Editor</h4>
|
|
433
|
+
<div class="form-row">
|
|
434
|
+
<label for="node-input-autoGenerate"><i class="fa fa-bolt"></i> Auto-generate</label>
|
|
435
|
+
<input type="checkbox" id="node-input-autoGenerate" style="vertical-align:middle" checked>
|
|
436
|
+
<span>自動由上方設定產生 SQL;你在下方編輯框輸入時會自動關閉以保留你的修改。</span>
|
|
437
|
+
</div>
|
|
438
|
+
<div class="form-row">
|
|
439
|
+
<textarea id="node-input-sqlCustom" style="width:100%; height:190px; font-family:monospace;" spellcheck="false" placeholder="這裡顯示組合後的 SQL;可手動修改,會被保存(按 Done)"></textarea>
|
|
440
|
+
<input type="hidden" id="node-input-lastAutoSQL">
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</script>
|