n8n-nodes-excel-api 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.md +522 -0
- package/dist/credentials/ExcelApiAuth.credentials.d.ts +7 -0
- package/dist/credentials/ExcelApiAuth.credentials.js +33 -0
- package/dist/nodes/ExcelApi/ExcelApi.node.d.ts +11 -0
- package/dist/nodes/ExcelApi/ExcelApi.node.js +505 -0
- package/dist/nodes/ExcelApi/excelapi.svg +9 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# n8n-nodes-excel-api
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/n8n-nodes-excel-api)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
n8n 社群節點,透過 API 存取 Excel 檔案,具備**並行安全保護**。完美適用於多使用者同時透過 n8n 工作流程存取相同 Excel 檔案的場景。
|
|
7
|
+
|
|
8
|
+
## 🎯 為什麼需要這個節點?
|
|
9
|
+
|
|
10
|
+
### 問題所在
|
|
11
|
+
直接在 n8n 中使用 Excel 檔案時:
|
|
12
|
+
- ❌ 多個工作流程同時存取同一檔案會導致檔案損毀
|
|
13
|
+
- ❌ 並行寫入時會發生資料覆蓋與遺失
|
|
14
|
+
- ❌ 缺乏檔案鎖定機制
|
|
15
|
+
- ❌ 難以處理多人同時提交的 Webhook 表單
|
|
16
|
+
|
|
17
|
+
### 解決方案
|
|
18
|
+
本節點搭配 [Excel API Server](https://github.com/code4Copilot/excel-api-server) 提供:
|
|
19
|
+
- ✅ **檔案鎖定** - 自動佇列管理並行請求
|
|
20
|
+
- ✅ **資料完整性** - 無資料遺失或損毀
|
|
21
|
+
- ✅ **多使用者支援** - 完美適用於多人提交的 HTML 表單
|
|
22
|
+
- ✅ **類似 Google Sheets 的介面** - 在 n8n 中熟悉的操作方式
|
|
23
|
+
- ✅ **批次操作** - 高效的大量更新
|
|
24
|
+
|
|
25
|
+
## 📦 安裝方式
|
|
26
|
+
|
|
27
|
+
### 方法 1:npm(推薦)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install n8n-nodes-excel-api
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 方法 2:手動安裝
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 1. 複製儲存庫
|
|
37
|
+
git clone https://github.com/code4Copilot/n8n-nodes-excel-api.git
|
|
38
|
+
cd n8n-nodes-excel-api
|
|
39
|
+
|
|
40
|
+
# 2. 安裝相依套件
|
|
41
|
+
npm install
|
|
42
|
+
|
|
43
|
+
# 3. 建置
|
|
44
|
+
npm run build
|
|
45
|
+
|
|
46
|
+
# 4. 連結到 n8n
|
|
47
|
+
npm link
|
|
48
|
+
cd ~/.n8n
|
|
49
|
+
npm link n8n-nodes-excel-api
|
|
50
|
+
|
|
51
|
+
# 5. 重新啟動 n8n
|
|
52
|
+
n8n start
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 方法 3:社群套件(發布後)
|
|
56
|
+
|
|
57
|
+
在 n8n 中:
|
|
58
|
+
1. 前往 **設定** → **社群節點**
|
|
59
|
+
2. 點擊 **安裝**
|
|
60
|
+
3. 輸入:`n8n-nodes-excel-api`
|
|
61
|
+
4. 點擊 **安裝**
|
|
62
|
+
|
|
63
|
+
## 🚀 前置需求
|
|
64
|
+
|
|
65
|
+
**必須先執行 Excel API Server!**
|
|
66
|
+
|
|
67
|
+
安裝並啟動 [Excel API Server](https://github.com/code4Copilot/excel-api-server):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# 使用 Docker 快速啟動
|
|
71
|
+
docker run -d \
|
|
72
|
+
-p 8000:8000 \
|
|
73
|
+
-v $(pwd)/data:/app/data \
|
|
74
|
+
-e API_TOKEN=your-secret-token \
|
|
75
|
+
yourusername/excel-api-server
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
詳細資訊請參閱 [Excel API Server 文件](https://github.com/code4Copilot/excel-api-server)。
|
|
79
|
+
|
|
80
|
+
## 🔧 設定
|
|
81
|
+
|
|
82
|
+
### 1. 設定憑證
|
|
83
|
+
|
|
84
|
+
在 n8n 中:
|
|
85
|
+
1. 前往 **憑證** → **新增**
|
|
86
|
+
2. 搜尋「Excel API」
|
|
87
|
+
3. 填寫:
|
|
88
|
+
- **API URL**:`http://localhost:8000`(您的 API 伺服器位址)
|
|
89
|
+
- **API Token**:`your-secret-token`(來自 Excel API Server)
|
|
90
|
+
4. 點擊 **儲存**
|
|
91
|
+
|
|
92
|
+
### 2. 將節點加入工作流程
|
|
93
|
+
|
|
94
|
+
1. 建立或開啟工作流程
|
|
95
|
+
2. 點擊 **新增節點**
|
|
96
|
+
3. 搜尋「Excel API」
|
|
97
|
+
4. 選擇節點
|
|
98
|
+
5. 選擇您的憑證
|
|
99
|
+
6. 設定操作
|
|
100
|
+
|
|
101
|
+
## 📚 操作說明
|
|
102
|
+
|
|
103
|
+
### 1. Append(附加)
|
|
104
|
+
在工作表末端新增一列資料。
|
|
105
|
+
|
|
106
|
+
**兩種模式:**
|
|
107
|
+
|
|
108
|
+
#### Object Mode(物件模式)- 推薦
|
|
109
|
+
使用欄位名稱對應,更安全且易於維護。
|
|
110
|
+
|
|
111
|
+
**範例:**
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"員工編號": "{{ $json.body.employeeId }}",
|
|
115
|
+
"姓名": "{{ $json.body.name }}",
|
|
116
|
+
"部門": "{{ $json.body.department }}",
|
|
117
|
+
"職位": "{{ $json.body.position }}",
|
|
118
|
+
"薪資": "{{ $json.body.salary }}"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**特色:**
|
|
123
|
+
- ✅ 自動讀取 Excel 表頭(第一列)
|
|
124
|
+
- ✅ 按照欄位名稱智能對應
|
|
125
|
+
- ✅ 忽略未知欄位,並在回應中提示
|
|
126
|
+
- ✅ 欄位順序可任意調整
|
|
127
|
+
- ✅ 缺少的欄位會自動填入空值
|
|
128
|
+
|
|
129
|
+
#### Array Mode(陣列模式)
|
|
130
|
+
依照精確的欄位順序指定值。
|
|
131
|
+
|
|
132
|
+
**範例:**
|
|
133
|
+
```json
|
|
134
|
+
["E100", "江小魚", "人資部", "經理", "70000"]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**注意:** 值的順序必須與 Excel 欄位順序完全對應。
|
|
138
|
+
|
|
139
|
+
### 2. Read(讀取)
|
|
140
|
+
從 Excel 檔案讀取資料。
|
|
141
|
+
|
|
142
|
+
**參數:**
|
|
143
|
+
- `file`:檔案名稱(例如:`employees.xlsx`)
|
|
144
|
+
- `sheet`:工作表名稱(預設:`Sheet1`)
|
|
145
|
+
- `range`:儲存格範圍(例如:`A1:D10`,留空讀取全部資料)
|
|
146
|
+
|
|
147
|
+
**輸出:**
|
|
148
|
+
- 若偵測到表頭,自動將第一列轉換為欄位名稱
|
|
149
|
+
- 回傳物件陣列,以表頭作為鍵值
|
|
150
|
+
- 若無表頭則回傳原始資料陣列
|
|
151
|
+
|
|
152
|
+
### 3. Update(更新)
|
|
153
|
+
更新現有列的資料。
|
|
154
|
+
|
|
155
|
+
**識別方式:**
|
|
156
|
+
|
|
157
|
+
#### 依列號(Row Number)
|
|
158
|
+
直接指定要更新的列號(從 2 開始,第 1 列為表頭)。
|
|
159
|
+
|
|
160
|
+
**範例:**
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"operation": "update",
|
|
164
|
+
"identifyBy": "rowNumber",
|
|
165
|
+
"rowNumber": 5,
|
|
166
|
+
"valuesToSet": {
|
|
167
|
+
"狀態": "已完成",
|
|
168
|
+
"更新日期": "2025-12-21"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### 依查找(Lookup)
|
|
174
|
+
透過查找特定欄位的值來找到要更新的列。
|
|
175
|
+
|
|
176
|
+
**範例:**
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"operation": "update",
|
|
180
|
+
"identifyBy": "lookup",
|
|
181
|
+
"lookupColumn": "員工編號",
|
|
182
|
+
"lookupValue": "E100",
|
|
183
|
+
"valuesToSet": {
|
|
184
|
+
"薪資": "80000",
|
|
185
|
+
"職位": "資深經理"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 4. Delete(刪除)
|
|
191
|
+
從工作表中刪除一列。
|
|
192
|
+
|
|
193
|
+
**識別方式:**
|
|
194
|
+
|
|
195
|
+
#### 依列號
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"operation": "delete",
|
|
199
|
+
"identifyBy": "rowNumber",
|
|
200
|
+
"rowNumber": 5
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### 依查找
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"operation": "delete",
|
|
208
|
+
"identifyBy": "lookup",
|
|
209
|
+
"lookupColumn": "員工編號",
|
|
210
|
+
"lookupValue": "E100"
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 5. Batch(批次)
|
|
215
|
+
一次執行多個操作(更有效率)。
|
|
216
|
+
|
|
217
|
+
**範例:**
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"operations": [
|
|
221
|
+
{
|
|
222
|
+
"type": "append",
|
|
223
|
+
"values": ["E010", "Alice", "行銷部", "專員", "65000"]
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"type": "update",
|
|
227
|
+
"row": 5,
|
|
228
|
+
"values": ["E005", "Updated Name", "IT部", "經理", "90000"]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"type": "delete",
|
|
232
|
+
"row": 10
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 🎨 使用範例
|
|
239
|
+
|
|
240
|
+
## 🎨 使用範例
|
|
241
|
+
|
|
242
|
+
### 範例 1:Webhook 表單寫入 Excel
|
|
243
|
+
|
|
244
|
+
完美適用於多人同時提交表單的場景!
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
┌──────────────────┐
|
|
248
|
+
│ Webhook │ 接收表單提交
|
|
249
|
+
│ POST /submit │
|
|
250
|
+
└────────┬─────────┘
|
|
251
|
+
│
|
|
252
|
+
▼
|
|
253
|
+
┌──────────────────┐
|
|
254
|
+
│ Excel API │ 操作:Append(物件模式)
|
|
255
|
+
│ │ 檔案:registrations.xlsx
|
|
256
|
+
│ │ 值:{
|
|
257
|
+
│ │ "姓名": "{{ $json.body.name }}",
|
|
258
|
+
│ │ "Email": "{{ $json.body.email }}",
|
|
259
|
+
│ │ "電話": "{{ $json.body.phone }}",
|
|
260
|
+
│ │ "提交時間": "{{ $now }}"
|
|
261
|
+
│ │ }
|
|
262
|
+
└────────┬─────────┘
|
|
263
|
+
│
|
|
264
|
+
▼
|
|
265
|
+
┌──────────────────┐
|
|
266
|
+
│ Respond Webhook │ 回傳成功訊息
|
|
267
|
+
└──────────────────┘
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**HTML 表單:**
|
|
271
|
+
```html
|
|
272
|
+
<form id="registrationForm">
|
|
273
|
+
<input type="text" name="name" placeholder="姓名" required>
|
|
274
|
+
<input type="email" name="email" placeholder="Email" required>
|
|
275
|
+
<input type="tel" name="phone" placeholder="電話" required>
|
|
276
|
+
<button type="submit">提交</button>
|
|
277
|
+
</form>
|
|
278
|
+
|
|
279
|
+
<script>
|
|
280
|
+
document.getElementById('registrationForm').addEventListener('submit', async (e) => {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
const formData = new FormData(e.target);
|
|
283
|
+
await fetch('YOUR_WEBHOOK_URL', {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: {'Content-Type': 'application/json'},
|
|
286
|
+
body: JSON.stringify(Object.fromEntries(formData))
|
|
287
|
+
});
|
|
288
|
+
alert('提交成功!');
|
|
289
|
+
});
|
|
290
|
+
</script>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 範例 2:每日報表產生
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
┌──────────────────┐
|
|
297
|
+
│ Schedule │ 每天早上 9:00
|
|
298
|
+
│ 0 9 * * * │
|
|
299
|
+
└────────┬─────────┘
|
|
300
|
+
│
|
|
301
|
+
▼
|
|
302
|
+
┌──────────────────┐
|
|
303
|
+
│ Excel API │ 操作:Read
|
|
304
|
+
│ (讀取) │ 檔案:sales.xlsx
|
|
305
|
+
└────────┬─────────┘
|
|
306
|
+
│
|
|
307
|
+
▼
|
|
308
|
+
┌──────────────────┐
|
|
309
|
+
│ Filter │ 篩選今日記錄
|
|
310
|
+
└────────┬─────────┘
|
|
311
|
+
│
|
|
312
|
+
▼
|
|
313
|
+
┌──────────────────┐
|
|
314
|
+
│ Send Email │ 發送每日報表
|
|
315
|
+
└──────────────────┘
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 範例 3:批次更新
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
┌──────────────────┐
|
|
322
|
+
│ Code │ 準備操作陣列
|
|
323
|
+
│ │ operations = [...]
|
|
324
|
+
└────────┬─────────┘
|
|
325
|
+
│
|
|
326
|
+
▼
|
|
327
|
+
┌──────────────────┐
|
|
328
|
+
│ Excel API │ 操作:Batch
|
|
329
|
+
│ (批次) │ 檔案:data.xlsx
|
|
330
|
+
│ │ 操作:{{ $json.operations }}
|
|
331
|
+
└──────────────────┘
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 範例 4:透過員工編號更新薪資
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
┌──────────────────┐
|
|
338
|
+
│ Webhook │ 接收更新請求
|
|
339
|
+
│ POST /update │ { "employeeId": "E100", "salary": 85000 }
|
|
340
|
+
└────────┬─────────┘
|
|
341
|
+
│
|
|
342
|
+
▼
|
|
343
|
+
┌──────────────────┐
|
|
344
|
+
│ Excel API │ 操作:Update
|
|
345
|
+
│ │ 識別方式:Lookup
|
|
346
|
+
│ │ 查找欄位:員工編號
|
|
347
|
+
│ │ 查找值:{{ $json.body.employeeId }}
|
|
348
|
+
│ │ 設定值:{ "薪資": "{{ $json.body.salary }}" }
|
|
349
|
+
└────────┬─────────┘
|
|
350
|
+
│
|
|
351
|
+
▼
|
|
352
|
+
┌──────────────────┐
|
|
353
|
+
│ Respond Webhook │ 回傳更新結果
|
|
354
|
+
└──────────────────┘
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## 🧪 並行測試
|
|
358
|
+
|
|
359
|
+
測試 10 個同時提交:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
// concurrent_test.js
|
|
363
|
+
const promises = [];
|
|
364
|
+
for (let i = 0; i < 10; i++) {
|
|
365
|
+
promises.push(
|
|
366
|
+
fetch('YOUR_WEBHOOK_URL', {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
headers: {'Content-Type': 'application/json'},
|
|
369
|
+
body: JSON.stringify({
|
|
370
|
+
員工編號: `E${String(i).padStart(3, '0')}`,
|
|
371
|
+
姓名: `測試使用者 ${i}`,
|
|
372
|
+
時間戳記: new Date().toISOString()
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
await Promise.all(promises);
|
|
379
|
+
console.log('所有請求完成!');
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**結果:** 所有 10 筆記錄都會安全地寫入 Excel,不會有資料遺失或損毀!
|
|
383
|
+
|
|
384
|
+
## ⚠️ 常見問題
|
|
385
|
+
|
|
386
|
+
### 問題 1:節點未顯示在 n8n 中
|
|
387
|
+
|
|
388
|
+
**解決方法:**
|
|
389
|
+
```bash
|
|
390
|
+
# 重新啟動 n8n
|
|
391
|
+
pkill -f n8n
|
|
392
|
+
n8n start
|
|
393
|
+
|
|
394
|
+
# 或使用 pm2
|
|
395
|
+
pm2 restart n8n
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 問題 2:API 連線失敗
|
|
399
|
+
|
|
400
|
+
**解決方法:**
|
|
401
|
+
- 檢查 Excel API Server 是否正在執行:`curl http://localhost:8000/`
|
|
402
|
+
- 驗證憑證中的 API URL 是否正確
|
|
403
|
+
- 檢查 API Token 是否正確
|
|
404
|
+
- 檢查防火牆設定
|
|
405
|
+
|
|
406
|
+
### 問題 3:「找不到參數」錯誤
|
|
407
|
+
|
|
408
|
+
**原因:** 參數名稱設定錯誤
|
|
409
|
+
|
|
410
|
+
**解決方法:**
|
|
411
|
+
- 確認選擇了正確的 Append Mode(Object 或 Array)
|
|
412
|
+
- Object Mode:使用 `appendValuesObject` 參數
|
|
413
|
+
- Array Mode:使用 `appendValuesArray` 參數
|
|
414
|
+
- 檢查 JSON 格式是否正確
|
|
415
|
+
|
|
416
|
+
### 問題 4:「檔案鎖定」錯誤
|
|
417
|
+
|
|
418
|
+
**原因:** 並行請求過多或 API 伺服器問題
|
|
419
|
+
|
|
420
|
+
**解決方法:**
|
|
421
|
+
- 稍等片刻後重試
|
|
422
|
+
- 檢查 API 伺服器狀態
|
|
423
|
+
- 必要時重新啟動 Excel API Server
|
|
424
|
+
|
|
425
|
+
## 🔐 安全性
|
|
426
|
+
|
|
427
|
+
### 最佳實踐
|
|
428
|
+
|
|
429
|
+
1. **使用強式 API Token**
|
|
430
|
+
```bash
|
|
431
|
+
# 產生安全的 token
|
|
432
|
+
openssl rand -hex 32
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
2. **在正式環境使用 HTTPS**
|
|
436
|
+
- 設定反向代理(Nginx)
|
|
437
|
+
- 使用 SSL 憑證
|
|
438
|
+
|
|
439
|
+
3. **限制存取**
|
|
440
|
+
- 僅允許信任的網路存取 API URL
|
|
441
|
+
- 遠端存取時使用 VPN
|
|
442
|
+
|
|
443
|
+
4. **定期備份**
|
|
444
|
+
- 設定 Excel 檔案自動備份
|
|
445
|
+
- 將備份儲存在安全位置
|
|
446
|
+
|
|
447
|
+
## 📊 效能優化建議
|
|
448
|
+
|
|
449
|
+
### 1. 使用批次操作
|
|
450
|
+
```javascript
|
|
451
|
+
// ❌ 不好:多次單一操作
|
|
452
|
+
for (item of items) {
|
|
453
|
+
await appendRow(item);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ✅ 好:一次批次操作
|
|
457
|
+
await batchOperations(items.map(item => ({
|
|
458
|
+
type: "append",
|
|
459
|
+
values: item.values
|
|
460
|
+
})));
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 2. 讀取時指定範圍
|
|
464
|
+
```javascript
|
|
465
|
+
// ❌ 不好:讀取整個檔案
|
|
466
|
+
range: ""
|
|
467
|
+
|
|
468
|
+
// ✅ 好:只讀取需要的範圍
|
|
469
|
+
range: "A1:D100"
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 3. 使用高效的工作流程
|
|
473
|
+
- 在一個工作流程中組合相關操作
|
|
474
|
+
- 減少 API 呼叫次數
|
|
475
|
+
- 適當使用快取
|
|
476
|
+
|
|
477
|
+
## 🆕 最新功能
|
|
478
|
+
|
|
479
|
+
### Object Mode(物件模式)
|
|
480
|
+
- ✅ 使用 `/api/excel/append_object` API
|
|
481
|
+
- ✅ 自動讀取 Excel 表頭(第一列)
|
|
482
|
+
- ✅ 按照欄位名稱智能對應
|
|
483
|
+
- ✅ 忽略未知欄位,並在回應中提示
|
|
484
|
+
- ✅ 不需要記住欄位順序
|
|
485
|
+
|
|
486
|
+
### 進階更新與刪除
|
|
487
|
+
- ✅ 支援依列號直接操作
|
|
488
|
+
- ✅ 支援依查找欄位值來操作
|
|
489
|
+
- ✅ 可更新特定欄位而不影響其他欄位
|
|
490
|
+
|
|
491
|
+
## 🤝 貢獻
|
|
492
|
+
|
|
493
|
+
歡迎貢獻!
|
|
494
|
+
|
|
495
|
+
1. Fork 此儲存庫
|
|
496
|
+
2. 建立您的功能分支:`git checkout -b feature/AmazingFeature`
|
|
497
|
+
3. 提交您的變更:`git commit -m 'Add some AmazingFeature'`
|
|
498
|
+
4. 推送到分支:`git push origin feature/AmazingFeature`
|
|
499
|
+
5. 開啟 Pull Request
|
|
500
|
+
|
|
501
|
+
## 📄 授權
|
|
502
|
+
|
|
503
|
+
MIT 授權 - 詳見 [LICENSE](LICENSE) 檔案
|
|
504
|
+
|
|
505
|
+
## 🔗 相關專案
|
|
506
|
+
|
|
507
|
+
- [Excel API Server](https://github.com/code4Copilot/excel-api-server) - 後端 API 伺服器(必要)
|
|
508
|
+
- [n8n](https://github.com/n8n-io/n8n) - 工作流程自動化工具
|
|
509
|
+
|
|
510
|
+
## 📧 支援
|
|
511
|
+
|
|
512
|
+
- GitHub Issues:[回報問題](https://github.com/code4Copilot/n8n-nodes-excel-api/issues)
|
|
513
|
+
- Email:your.email@example.com
|
|
514
|
+
- n8n 社群:[n8n 論壇](https://community.n8n.io)
|
|
515
|
+
|
|
516
|
+
## ⭐ Star 歷史
|
|
517
|
+
|
|
518
|
+
如果這個專案對您有幫助,請給它一個 ⭐!
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
**用 ❤️ 為 n8n 社群打造**
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExcelApiAuth = void 0;
|
|
4
|
+
class ExcelApiAuth {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'excelApiAuth';
|
|
7
|
+
this.displayName = 'Excel API Auth';
|
|
8
|
+
this.documentationUrl = 'https://your-docs-url.com';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'API URL',
|
|
12
|
+
name: 'url',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: 'http://localhost:8000',
|
|
15
|
+
required: true,
|
|
16
|
+
description: 'The base URL of your Excel API server',
|
|
17
|
+
placeholder: 'http://localhost:8000',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: 'API Token',
|
|
21
|
+
name: 'token',
|
|
22
|
+
type: 'string',
|
|
23
|
+
typeOptions: {
|
|
24
|
+
password: true,
|
|
25
|
+
},
|
|
26
|
+
default: '',
|
|
27
|
+
required: true,
|
|
28
|
+
description: 'The API token for authentication',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.ExcelApiAuth = ExcelApiAuth;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ExcelApi implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getExcelFiles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
getExcelSheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExcelApi = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
class ExcelApi {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Excel API',
|
|
9
|
+
name: 'excelApi',
|
|
10
|
+
icon: 'file:excelapi.svg',
|
|
11
|
+
group: ['transform'],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
14
|
+
description: 'Access Excel files via API with concurrent safety',
|
|
15
|
+
defaults: {
|
|
16
|
+
name: 'Excel API',
|
|
17
|
+
},
|
|
18
|
+
inputs: ['main'],
|
|
19
|
+
outputs: ['main'],
|
|
20
|
+
credentials: [
|
|
21
|
+
{
|
|
22
|
+
name: 'excelApiAuth',
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
properties: [
|
|
27
|
+
{
|
|
28
|
+
displayName: 'Operation',
|
|
29
|
+
name: 'operation',
|
|
30
|
+
type: 'options',
|
|
31
|
+
noDataExpression: true,
|
|
32
|
+
options: [
|
|
33
|
+
{ name: 'Append', value: 'append', action: 'Append row to Excel file', description: 'Add a new row to the end of the sheet' },
|
|
34
|
+
{ name: 'Read', value: 'read', action: 'Read Excel file', description: 'Read data from Excel file' },
|
|
35
|
+
{ name: 'Update', value: 'update', action: 'Update row', description: 'Update an existing row' },
|
|
36
|
+
{ name: 'Delete', value: 'delete', action: 'Delete row', description: 'Delete a row' },
|
|
37
|
+
{ name: 'Batch', value: 'batch', action: 'Batch operations', description: 'Execute multiple operations at once' },
|
|
38
|
+
],
|
|
39
|
+
default: 'append',
|
|
40
|
+
},
|
|
41
|
+
// File selection
|
|
42
|
+
{
|
|
43
|
+
displayName: 'File Name',
|
|
44
|
+
name: 'fileName',
|
|
45
|
+
type: 'options',
|
|
46
|
+
required: true,
|
|
47
|
+
typeOptions: {
|
|
48
|
+
loadOptionsMethod: 'getExcelFiles',
|
|
49
|
+
},
|
|
50
|
+
default: '',
|
|
51
|
+
description: 'Select an Excel file from the server',
|
|
52
|
+
},
|
|
53
|
+
// Sheet selection
|
|
54
|
+
{
|
|
55
|
+
displayName: 'Sheet Name',
|
|
56
|
+
name: 'sheetName',
|
|
57
|
+
type: 'options',
|
|
58
|
+
typeOptions: {
|
|
59
|
+
loadOptionsMethod: 'getExcelSheets',
|
|
60
|
+
loadOptionsDependsOn: ['fileName'],
|
|
61
|
+
},
|
|
62
|
+
default: 'Sheet1',
|
|
63
|
+
description: 'Select a worksheet from the file',
|
|
64
|
+
},
|
|
65
|
+
// Append operation - Mode selection
|
|
66
|
+
{
|
|
67
|
+
displayName: 'Append Mode',
|
|
68
|
+
name: 'appendMode',
|
|
69
|
+
type: 'options',
|
|
70
|
+
displayOptions: {
|
|
71
|
+
show: {
|
|
72
|
+
operation: ['append']
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
options: [
|
|
76
|
+
{ name: 'Object (By Column Names)', value: 'object', description: 'Map values by column names - easier and safer' },
|
|
77
|
+
{ name: 'Array (By Position)', value: 'array', description: 'Specify values in exact column order' },
|
|
78
|
+
],
|
|
79
|
+
default: 'object',
|
|
80
|
+
description: 'How to specify values to append',
|
|
81
|
+
},
|
|
82
|
+
// Append - Object Mode
|
|
83
|
+
{
|
|
84
|
+
displayName: 'Values to Append',
|
|
85
|
+
name: 'appendValuesObject',
|
|
86
|
+
type: 'json',
|
|
87
|
+
displayOptions: {
|
|
88
|
+
show: {
|
|
89
|
+
operation: ['append'],
|
|
90
|
+
appendMode: ['object']
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
default: '{\n "Column1": "{{ $json.field1 }}",\n "Column2": "{{ $json.field2 }}"\n}',
|
|
94
|
+
required: true,
|
|
95
|
+
description: 'Object with column names as keys. Column names must match Excel headers exactly.',
|
|
96
|
+
hint: 'Example: {"員工編號": "{{ $json.body.employeeId }}", "姓名": "{{ $json.body.name }}"}',
|
|
97
|
+
},
|
|
98
|
+
// Append - Array Mode
|
|
99
|
+
{
|
|
100
|
+
displayName: 'Values to Append',
|
|
101
|
+
name: 'appendValuesArray',
|
|
102
|
+
type: 'json',
|
|
103
|
+
displayOptions: {
|
|
104
|
+
show: {
|
|
105
|
+
operation: ['append'],
|
|
106
|
+
appendMode: ['array']
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
default: '["value1", "value2", "value3"]',
|
|
110
|
+
required: true,
|
|
111
|
+
description: 'Array of values to append. Can use expressions like {{ $json.fieldName }}',
|
|
112
|
+
hint: 'Example: ["{{ $json.name }}", "{{ $json.email }}", "{{ $json.age }}"]',
|
|
113
|
+
},
|
|
114
|
+
// Read operation
|
|
115
|
+
{
|
|
116
|
+
displayName: 'Range',
|
|
117
|
+
name: 'range',
|
|
118
|
+
type: 'string',
|
|
119
|
+
displayOptions: { show: { operation: ['read'] } },
|
|
120
|
+
default: '',
|
|
121
|
+
description: 'Cell range to read (e.g., A1:D10). Leave empty to read all data',
|
|
122
|
+
placeholder: 'A1:D10',
|
|
123
|
+
},
|
|
124
|
+
// Update & Delete: Row Identification Method
|
|
125
|
+
{
|
|
126
|
+
displayName: 'Identify Row By',
|
|
127
|
+
name: 'identifyBy',
|
|
128
|
+
type: 'options',
|
|
129
|
+
displayOptions: { show: { operation: ['update', 'delete'] } },
|
|
130
|
+
options: [
|
|
131
|
+
{ name: 'Row Number', value: 'rowNumber', description: 'Specify the exact row number' },
|
|
132
|
+
{ name: 'Lookup', value: 'lookup', description: 'Find row by matching a column value' },
|
|
133
|
+
],
|
|
134
|
+
default: 'rowNumber',
|
|
135
|
+
description: 'How to identify the row to update/delete',
|
|
136
|
+
},
|
|
137
|
+
// Row Number (for direct specification)
|
|
138
|
+
{
|
|
139
|
+
displayName: 'Row Number',
|
|
140
|
+
name: 'rowNumber',
|
|
141
|
+
type: 'number',
|
|
142
|
+
displayOptions: {
|
|
143
|
+
show: {
|
|
144
|
+
operation: ['update', 'delete'],
|
|
145
|
+
identifyBy: ['rowNumber']
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
required: true,
|
|
149
|
+
default: 2,
|
|
150
|
+
description: 'Row number to update/delete (1-based, row 1 is header)',
|
|
151
|
+
hint: '⚠️ Row 1 is protected (header row). Data rows start from row 2.',
|
|
152
|
+
},
|
|
153
|
+
// Lookup Column (for lookup method)
|
|
154
|
+
{
|
|
155
|
+
displayName: 'Lookup Column',
|
|
156
|
+
name: 'lookupColumn',
|
|
157
|
+
type: 'string',
|
|
158
|
+
displayOptions: {
|
|
159
|
+
show: {
|
|
160
|
+
operation: ['update', 'delete'],
|
|
161
|
+
identifyBy: ['lookup']
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
required: true,
|
|
165
|
+
default: '',
|
|
166
|
+
placeholder: 'e.g., 員工編號, Email, ID',
|
|
167
|
+
description: 'Column name to search in (must match header exactly)',
|
|
168
|
+
hint: 'The first row is treated as headers',
|
|
169
|
+
},
|
|
170
|
+
// Lookup Value (for lookup method)
|
|
171
|
+
{
|
|
172
|
+
displayName: 'Lookup Value',
|
|
173
|
+
name: 'lookupValue',
|
|
174
|
+
type: 'string',
|
|
175
|
+
displayOptions: {
|
|
176
|
+
show: {
|
|
177
|
+
operation: ['update', 'delete'],
|
|
178
|
+
identifyBy: ['lookup']
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
required: true,
|
|
182
|
+
default: '',
|
|
183
|
+
placeholder: 'e.g., E001, john@example.com',
|
|
184
|
+
description: 'Value to search for in the lookup column',
|
|
185
|
+
hint: 'Can use expressions like {{ $json.id }}',
|
|
186
|
+
},
|
|
187
|
+
// Update operation: Values to Set
|
|
188
|
+
{
|
|
189
|
+
displayName: 'Values to Set',
|
|
190
|
+
name: 'valuesToSet',
|
|
191
|
+
type: 'json',
|
|
192
|
+
displayOptions: { show: { operation: ['update'] } },
|
|
193
|
+
default: '{\n "Status": "Done",\n "UpdatedDate": "2024-01-01"\n}',
|
|
194
|
+
required: true,
|
|
195
|
+
description: 'Object with column names as keys and new values',
|
|
196
|
+
hint: 'Example: {"Status": "{{ $json.status }}", "Salary": {{ $json.salary }}}',
|
|
197
|
+
},
|
|
198
|
+
// Batch operation
|
|
199
|
+
{
|
|
200
|
+
displayName: 'Operations',
|
|
201
|
+
name: 'batchOperations',
|
|
202
|
+
type: 'json',
|
|
203
|
+
displayOptions: { show: { operation: ['batch'] } },
|
|
204
|
+
default: `[
|
|
205
|
+
{
|
|
206
|
+
"type": "append",
|
|
207
|
+
"values": ["value1", "value2"]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"type": "update",
|
|
211
|
+
"row": 5,
|
|
212
|
+
"values": ["new1", "new2"]
|
|
213
|
+
}
|
|
214
|
+
]`,
|
|
215
|
+
required: true,
|
|
216
|
+
description: 'Array of operations to execute',
|
|
217
|
+
hint: 'Each operation should have "type" (append/update/delete) and related fields',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
this.methods = {
|
|
222
|
+
loadOptions: {
|
|
223
|
+
async getExcelFiles() {
|
|
224
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
225
|
+
const apiUrl = credentials.url;
|
|
226
|
+
const apiToken = credentials.token;
|
|
227
|
+
try {
|
|
228
|
+
const response = await this.helpers.request({
|
|
229
|
+
method: 'GET',
|
|
230
|
+
url: `${apiUrl}/api/excel/files`,
|
|
231
|
+
headers: {
|
|
232
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
233
|
+
},
|
|
234
|
+
json: true,
|
|
235
|
+
});
|
|
236
|
+
if (response.success && response.files) {
|
|
237
|
+
return response.files.map((file) => ({
|
|
238
|
+
name: file,
|
|
239
|
+
value: file,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async getExcelSheets() {
|
|
249
|
+
const fileName = this.getNodeParameter('fileName');
|
|
250
|
+
if (!fileName) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
254
|
+
const apiUrl = credentials.url;
|
|
255
|
+
const apiToken = credentials.token;
|
|
256
|
+
try {
|
|
257
|
+
const response = await this.helpers.request({
|
|
258
|
+
method: 'GET',
|
|
259
|
+
url: `${apiUrl}/api/excel/sheets?file=${encodeURIComponent(fileName)}`,
|
|
260
|
+
headers: {
|
|
261
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
262
|
+
},
|
|
263
|
+
json: true,
|
|
264
|
+
});
|
|
265
|
+
if (response.success && response.sheets) {
|
|
266
|
+
return response.sheets.map((sheet) => ({
|
|
267
|
+
name: sheet,
|
|
268
|
+
value: sheet,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async execute() {
|
|
281
|
+
var _a;
|
|
282
|
+
const items = this.getInputData();
|
|
283
|
+
const returnData = [];
|
|
284
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
285
|
+
// Get credentials
|
|
286
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
287
|
+
const apiUrl = credentials.url;
|
|
288
|
+
const apiToken = credentials.token;
|
|
289
|
+
// Common parameters
|
|
290
|
+
const fileName = this.getNodeParameter('fileName', 0);
|
|
291
|
+
const sheetName = this.getNodeParameter('sheetName', 0) || 'Sheet1';
|
|
292
|
+
if (!fileName) {
|
|
293
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'File Name is required. Please select an Excel file.');
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
for (let i = 0; i < items.length; i++) {
|
|
297
|
+
let responseData;
|
|
298
|
+
if (operation === 'append') {
|
|
299
|
+
// Get append mode to determine which parameter to use
|
|
300
|
+
const appendMode = this.getNodeParameter('appendMode', i);
|
|
301
|
+
const parameterName = appendMode === 'object' ? 'appendValuesObject' : 'appendValuesArray';
|
|
302
|
+
const appendValuesRaw = this.getNodeParameter(parameterName, i);
|
|
303
|
+
let appendValues;
|
|
304
|
+
if (typeof appendValuesRaw === 'string') {
|
|
305
|
+
try {
|
|
306
|
+
appendValues = JSON.parse(appendValuesRaw);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Values to Append must be a valid JSON ${appendMode === 'object' ? 'object' : 'array'}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
appendValues = appendValuesRaw;
|
|
314
|
+
}
|
|
315
|
+
// Use different API endpoint based on append mode
|
|
316
|
+
if (appendMode === 'object') {
|
|
317
|
+
// Object Mode: Use append_object API
|
|
318
|
+
// This API automatically reads headers and maps values by column names
|
|
319
|
+
responseData = await this.helpers.request({
|
|
320
|
+
method: 'POST',
|
|
321
|
+
url: `${apiUrl}/api/excel/append_object`,
|
|
322
|
+
headers: {
|
|
323
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
324
|
+
'Content-Type': 'application/json',
|
|
325
|
+
},
|
|
326
|
+
body: {
|
|
327
|
+
file: fileName,
|
|
328
|
+
sheet: sheetName,
|
|
329
|
+
values: appendValues,
|
|
330
|
+
},
|
|
331
|
+
json: true,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// Array Mode: Use standard append API
|
|
336
|
+
responseData = await this.helpers.request({
|
|
337
|
+
method: 'POST',
|
|
338
|
+
url: `${apiUrl}/api/excel/append`,
|
|
339
|
+
headers: {
|
|
340
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
341
|
+
'Content-Type': 'application/json',
|
|
342
|
+
},
|
|
343
|
+
body: {
|
|
344
|
+
file: fileName,
|
|
345
|
+
sheet: sheetName,
|
|
346
|
+
values: appendValues,
|
|
347
|
+
},
|
|
348
|
+
json: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (operation === 'read') {
|
|
353
|
+
const range = this.getNodeParameter('range', i);
|
|
354
|
+
responseData = await this.helpers.request({
|
|
355
|
+
method: 'POST',
|
|
356
|
+
url: `${apiUrl}/api/excel/read`,
|
|
357
|
+
headers: {
|
|
358
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
359
|
+
'Content-Type': 'application/json',
|
|
360
|
+
},
|
|
361
|
+
body: {
|
|
362
|
+
file: fileName,
|
|
363
|
+
sheet: sheetName,
|
|
364
|
+
range: range || undefined,
|
|
365
|
+
},
|
|
366
|
+
json: true,
|
|
367
|
+
});
|
|
368
|
+
if (responseData.success && responseData.data) {
|
|
369
|
+
const data = responseData.data;
|
|
370
|
+
if (data.length > 1) {
|
|
371
|
+
const headers = data[0];
|
|
372
|
+
const hasHeaders = headers.every((h) => typeof h === 'string' && h.length > 0);
|
|
373
|
+
if (hasHeaders) {
|
|
374
|
+
for (let rowIdx = 1; rowIdx < data.length; rowIdx++) {
|
|
375
|
+
const rowData = {};
|
|
376
|
+
const row = data[rowIdx];
|
|
377
|
+
headers.forEach((header, colIdx) => {
|
|
378
|
+
rowData[header] = row[colIdx];
|
|
379
|
+
});
|
|
380
|
+
returnData.push({ json: rowData });
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
returnData.push({ json: responseData });
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else if (operation === 'update') {
|
|
390
|
+
// Get identification method
|
|
391
|
+
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
392
|
+
const valuesToSetRaw = this.getNodeParameter('valuesToSet', i);
|
|
393
|
+
let valuesToSet;
|
|
394
|
+
if (typeof valuesToSetRaw === 'string') {
|
|
395
|
+
try {
|
|
396
|
+
valuesToSet = JSON.parse(valuesToSetRaw);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Values to Set must be a valid JSON object');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
valuesToSet = valuesToSetRaw;
|
|
404
|
+
}
|
|
405
|
+
// Build request body
|
|
406
|
+
const requestBody = {
|
|
407
|
+
file: fileName,
|
|
408
|
+
sheet: sheetName,
|
|
409
|
+
values_to_set: valuesToSet,
|
|
410
|
+
};
|
|
411
|
+
if (identifyBy === 'rowNumber') {
|
|
412
|
+
const rowNumber = this.getNodeParameter('rowNumber', i);
|
|
413
|
+
requestBody.row = rowNumber;
|
|
414
|
+
}
|
|
415
|
+
else if (identifyBy === 'lookup') {
|
|
416
|
+
const lookupColumn = this.getNodeParameter('lookupColumn', i);
|
|
417
|
+
const lookupValue = this.getNodeParameter('lookupValue', i);
|
|
418
|
+
requestBody.lookup_column = lookupColumn;
|
|
419
|
+
requestBody.lookup_value = lookupValue;
|
|
420
|
+
}
|
|
421
|
+
responseData = await this.helpers.request({
|
|
422
|
+
method: 'PUT',
|
|
423
|
+
url: `${apiUrl}/api/excel/update_advanced`,
|
|
424
|
+
headers: {
|
|
425
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
426
|
+
'Content-Type': 'application/json',
|
|
427
|
+
},
|
|
428
|
+
body: requestBody,
|
|
429
|
+
json: true,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
else if (operation === 'delete') {
|
|
433
|
+
// Get identification method
|
|
434
|
+
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
435
|
+
// Build request body
|
|
436
|
+
const requestBody = {
|
|
437
|
+
file: fileName,
|
|
438
|
+
sheet: sheetName,
|
|
439
|
+
};
|
|
440
|
+
if (identifyBy === 'rowNumber') {
|
|
441
|
+
const rowNumber = this.getNodeParameter('rowNumber', i);
|
|
442
|
+
requestBody.row = rowNumber;
|
|
443
|
+
}
|
|
444
|
+
else if (identifyBy === 'lookup') {
|
|
445
|
+
const lookupColumn = this.getNodeParameter('lookupColumn', i);
|
|
446
|
+
const lookupValue = this.getNodeParameter('lookupValue', i);
|
|
447
|
+
requestBody.lookup_column = lookupColumn;
|
|
448
|
+
requestBody.lookup_value = lookupValue;
|
|
449
|
+
}
|
|
450
|
+
responseData = await this.helpers.request({
|
|
451
|
+
method: 'DELETE',
|
|
452
|
+
url: `${apiUrl}/api/excel/delete_advanced`,
|
|
453
|
+
headers: {
|
|
454
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
455
|
+
'Content-Type': 'application/json',
|
|
456
|
+
},
|
|
457
|
+
body: requestBody,
|
|
458
|
+
json: true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else if (operation === 'batch') {
|
|
462
|
+
const batchOperationsRaw = this.getNodeParameter('batchOperations', i);
|
|
463
|
+
let batchOperations;
|
|
464
|
+
if (typeof batchOperationsRaw === 'string') {
|
|
465
|
+
try {
|
|
466
|
+
batchOperations = JSON.parse(batchOperationsRaw);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Batch Operations must be a valid JSON array');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
batchOperations = batchOperationsRaw;
|
|
474
|
+
}
|
|
475
|
+
responseData = await this.helpers.request({
|
|
476
|
+
method: 'POST',
|
|
477
|
+
url: `${apiUrl}/api/excel/batch`,
|
|
478
|
+
headers: {
|
|
479
|
+
'Authorization': `Bearer ${apiToken}`,
|
|
480
|
+
'Content-Type': 'application/json',
|
|
481
|
+
},
|
|
482
|
+
body: {
|
|
483
|
+
file: fileName,
|
|
484
|
+
sheet: sheetName,
|
|
485
|
+
operations: batchOperations,
|
|
486
|
+
},
|
|
487
|
+
json: true,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported operation: ${operation}`);
|
|
492
|
+
}
|
|
493
|
+
returnData.push({ json: responseData });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
if ((_a = error.response) === null || _a === void 0 ? void 0 : _a.body) {
|
|
498
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Excel API Error: ${JSON.stringify(error.response.body)}`);
|
|
499
|
+
}
|
|
500
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
501
|
+
}
|
|
502
|
+
return [returnData];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
exports.ExcelApi = ExcelApi;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg width="60px" height="60px" viewBox="0 0 60 60" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
4
|
+
<rect fill="#217346" x="0" y="0" width="60" height="60" rx="8"/>
|
|
5
|
+
<text font-family="Arial-BoldMT, Arial" font-size="36" font-weight="bold" fill="#FFFFFF">
|
|
6
|
+
<tspan x="13" y="43">X</tspan>
|
|
7
|
+
</text>
|
|
8
|
+
</g>
|
|
9
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-excel-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "n8n node for accessing Excel files via API with concurrent safety",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"excel",
|
|
9
|
+
"api",
|
|
10
|
+
"spreadsheet"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://github.com/code4Copilot/n8n-nodes-excel-api",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Hueyan Chen",
|
|
16
|
+
"email": "hueyan.chen@gmail.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/code4Copilot/n8n-nodes-excel-api.git"
|
|
21
|
+
},
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && gulp build:icons",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"format": "prettier nodes --write",
|
|
27
|
+
"lint": "eslint -c .eslintrc.prepublish.js --ext .ts,.js nodes package.json",
|
|
28
|
+
"lintfix": "eslint -c .eslintrc.prepublish.js --ext .ts,.js nodes package.json --fix",
|
|
29
|
+
"test": "jest",
|
|
30
|
+
"test:watch": "jest --watch",
|
|
31
|
+
"test:coverage": "jest --coverage",
|
|
32
|
+
"prepublishOnly": "npm run build && npm run lint"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"n8n": {
|
|
38
|
+
"n8nNodesApiVersion": 1,
|
|
39
|
+
"credentials": [
|
|
40
|
+
"dist/credentials/ExcelApiAuth.credentials.js"
|
|
41
|
+
],
|
|
42
|
+
"nodes": [
|
|
43
|
+
"dist/nodes/ExcelApi/ExcelApi.node.js"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/jest": "^29.5.0",
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"@typescript-eslint/parser": "^5.0.0",
|
|
50
|
+
"eslint": "^8.57.1",
|
|
51
|
+
"eslint-plugin-n8n-nodes-base": "^1.0.0",
|
|
52
|
+
"gulp": "^4.0.2",
|
|
53
|
+
"jest": "^29.5.0",
|
|
54
|
+
"n8n-workflow": "*",
|
|
55
|
+
"prettier": "^2.7.1",
|
|
56
|
+
"ts-jest": "^29.1.0",
|
|
57
|
+
"typescript": "^5.3.3"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"n8n-workflow": "*"
|
|
61
|
+
}
|
|
62
|
+
}
|