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 ADDED
@@ -0,0 +1,522 @@
1
+ # n8n-nodes-excel-api
2
+
3
+ [![npm version](https://badge.fury.io/js/n8n-nodes-excel-api.svg)](https://badge.fury.io/js/n8n-nodes-excel-api)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,7 @@
1
+ import { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class ExcelApiAuth implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -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
+ }