n8n-nodes-excel-api 1.0.2 → 1.0.4
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 +36 -110
- package/dist/nodes/ExcelApi/ExcelApi.node.d.ts +6 -2
- package/dist/nodes/ExcelApi/ExcelApi.node.js +270 -175
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,6 @@ This node works with [Excel API Server](https://github.com/code4Copilot/excel-ap
|
|
|
22
22
|
- ✅ **Data Integrity** - No data loss or corruption
|
|
23
23
|
- ✅ **Multi-User Support** - Perfect for multi-user HTML form submissions
|
|
24
24
|
- ✅ **Google Sheets-like Interface** - Familiar operations in n8n
|
|
25
|
-
- ✅ **Batch Operations** - Efficient bulk updates
|
|
26
25
|
|
|
27
26
|
## 📦 Installation
|
|
28
27
|
|
|
@@ -152,39 +151,17 @@ Read data from Excel file.
|
|
|
152
151
|
- Return raw data array if no headers
|
|
153
152
|
|
|
154
153
|
### 3. Update
|
|
155
|
-
Update existing row data.
|
|
156
|
-
|
|
157
|
-
**Identify Methods:**
|
|
158
|
-
|
|
159
|
-
#### By Row Number
|
|
160
|
-
Directly specify row number to update (starts from 2, row 1 is header).
|
|
161
|
-
|
|
162
|
-
**Example:**
|
|
163
|
-
```json
|
|
164
|
-
{
|
|
165
|
-
"operation": "update",
|
|
166
|
-
"identifyBy": "rowNumber",
|
|
167
|
-
"rowNumber": 5,
|
|
168
|
-
"valuesToSet": {
|
|
169
|
-
"Status": "Completed",
|
|
170
|
-
"Update Date": "2025-12-21"
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### By Lookup
|
|
176
|
-
Find rows to update by looking up specific column values.
|
|
154
|
+
Update existing row data by looking up specific column values.
|
|
177
155
|
|
|
178
156
|
**Process Modes:**
|
|
179
157
|
|
|
180
|
-
|
|
181
|
-
Update all matching rows, suitable for
|
|
158
|
+
#### All Matching Records - Default
|
|
159
|
+
Update all matching rows, suitable for scenarios where multiple rows share the same value.
|
|
182
160
|
|
|
183
161
|
**Example: Update all IT department employees**
|
|
184
162
|
```json
|
|
185
163
|
{
|
|
186
164
|
"operation": "update",
|
|
187
|
-
"identifyBy": "lookup",
|
|
188
165
|
"lookupColumn": "Department",
|
|
189
166
|
"lookupValue": "IT",
|
|
190
167
|
"processMode": "all",
|
|
@@ -195,14 +172,13 @@ Update all matching rows, suitable for batch update scenarios.
|
|
|
195
172
|
}
|
|
196
173
|
```
|
|
197
174
|
|
|
198
|
-
|
|
175
|
+
#### First Match Only
|
|
199
176
|
Update only the first matching record, suitable for unique identifier lookups.
|
|
200
177
|
|
|
201
178
|
**Example: Update specific employee data**
|
|
202
179
|
```json
|
|
203
180
|
{
|
|
204
181
|
"operation": "update",
|
|
205
|
-
"identifyBy": "lookup",
|
|
206
182
|
"lookupColumn": "Employee ID",
|
|
207
183
|
"lookupValue": "E100",
|
|
208
184
|
"processMode": "first",
|
|
@@ -215,50 +191,34 @@ Update only the first matching record, suitable for unique identifier lookups.
|
|
|
215
191
|
|
|
216
192
|
**💡 Usage Tips:**
|
|
217
193
|
- When looking up by unique identifiers (Employee ID, Email), use `processMode: "first"` for better performance
|
|
218
|
-
- Use `processMode: "all"` when
|
|
194
|
+
- Use `processMode: "all"` when updating multiple records that share the same value
|
|
219
195
|
- Default is `"all"` to ensure no matching records are missed
|
|
220
196
|
|
|
221
197
|
### 4. Delete
|
|
222
|
-
Delete
|
|
223
|
-
|
|
224
|
-
**Identify Methods:**
|
|
225
|
-
|
|
226
|
-
#### By Row Number
|
|
227
|
-
```json
|
|
228
|
-
{
|
|
229
|
-
"operation": "delete",
|
|
230
|
-
"identifyBy": "rowNumber",
|
|
231
|
-
"rowNumber": 5
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
#### By Lookup
|
|
236
|
-
Find rows to delete by looking up specific column values.
|
|
198
|
+
Delete rows from the sheet by looking up specific column values.
|
|
237
199
|
|
|
238
200
|
**Process Modes:**
|
|
239
201
|
|
|
240
|
-
|
|
202
|
+
#### All Matching Records - Default
|
|
241
203
|
Delete all matching rows.
|
|
242
204
|
|
|
243
205
|
**Example: Delete all terminated employees**
|
|
244
206
|
```json
|
|
245
207
|
{
|
|
246
208
|
"operation": "delete",
|
|
247
|
-
"identifyBy": "lookup",
|
|
248
209
|
"lookupColumn": "Status",
|
|
249
210
|
"lookupValue": "Terminated",
|
|
250
211
|
"processMode": "all"
|
|
251
212
|
}
|
|
252
213
|
```
|
|
253
214
|
|
|
254
|
-
|
|
215
|
+
#### First Match Only
|
|
255
216
|
Delete only the first matching record.
|
|
256
217
|
|
|
257
218
|
**Example: Delete specific employee**
|
|
258
219
|
```json
|
|
259
220
|
{
|
|
260
221
|
"operation": "delete",
|
|
261
|
-
"identifyBy": "lookup",
|
|
262
222
|
"lookupColumn": "Employee ID",
|
|
263
223
|
"lookupValue": "E100",
|
|
264
224
|
"processMode": "first"
|
|
@@ -268,31 +228,7 @@ Delete only the first matching record.
|
|
|
268
228
|
**⚠️ Important:**
|
|
269
229
|
- Delete operations cannot be undone, use with caution
|
|
270
230
|
- When looking up by unique identifiers, use `processMode: "first"`
|
|
271
|
-
- Verify lookup conditions carefully
|
|
272
|
-
|
|
273
|
-
### 5. Batch
|
|
274
|
-
Execute multiple operations at once (more efficient).
|
|
275
|
-
|
|
276
|
-
**Example:**
|
|
277
|
-
```json
|
|
278
|
-
{
|
|
279
|
-
"operations": [
|
|
280
|
-
{
|
|
281
|
-
"type": "append",
|
|
282
|
-
"values": ["E010", "Alice", "Marketing", "Specialist", "65000"]
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
"type": "update",
|
|
286
|
-
"row": 5,
|
|
287
|
-
"values": ["E005", "Updated Name", "IT", "Manager", "90000"]
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
"type": "delete",
|
|
291
|
-
"row": 10
|
|
292
|
-
}
|
|
293
|
-
]
|
|
294
|
-
}
|
|
295
|
-
```
|
|
231
|
+
- Verify lookup conditions carefully to avoid accidental data loss
|
|
296
232
|
|
|
297
233
|
## 🎨 Usage Examples
|
|
298
234
|
|
|
@@ -372,23 +308,7 @@ document.getElementById('registrationForm').addEventListener('submit', async (e)
|
|
|
372
308
|
└──────────────────┘
|
|
373
309
|
```
|
|
374
310
|
|
|
375
|
-
### Example 3:
|
|
376
|
-
|
|
377
|
-
```
|
|
378
|
-
┌──────────────────┐
|
|
379
|
-
│ Code │ Prepare operations array
|
|
380
|
-
│ │ operations = [...]
|
|
381
|
-
└────────┬─────────┘
|
|
382
|
-
│
|
|
383
|
-
▼
|
|
384
|
-
┌──────────────────┐
|
|
385
|
-
│ Excel API │ Operation: Batch
|
|
386
|
-
│ (Batch) │ File: data.xlsx
|
|
387
|
-
│ │ Operations: {{ $json.operations }}
|
|
388
|
-
└──────────────────┘
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Example 4: Update Salary by Employee ID
|
|
311
|
+
### Example 3: Update Salary by Employee ID
|
|
392
312
|
|
|
393
313
|
```
|
|
394
314
|
┌──────────────────┐
|
|
@@ -412,7 +332,7 @@ document.getElementById('registrationForm').addEventListener('submit', async (e)
|
|
|
412
332
|
└──────────────────┘
|
|
413
333
|
```
|
|
414
334
|
|
|
415
|
-
### Example
|
|
335
|
+
### Example 4: Batch Department Status Update
|
|
416
336
|
|
|
417
337
|
**Use Case:** Review all employees in a department at once
|
|
418
338
|
|
|
@@ -442,7 +362,7 @@ document.getElementById('registrationForm').addEventListener('submit', async (e)
|
|
|
442
362
|
└──────────────────┘
|
|
443
363
|
```
|
|
444
364
|
|
|
445
|
-
### Example
|
|
365
|
+
### Example 5: Clean Up Expired Data
|
|
446
366
|
|
|
447
367
|
**Use Case:** Periodically delete employee records terminated over a year ago
|
|
448
368
|
|
|
@@ -559,21 +479,7 @@ pm2 restart n8n
|
|
|
559
479
|
|
|
560
480
|
## 📊 Performance Optimization Tips
|
|
561
481
|
|
|
562
|
-
### 1.
|
|
563
|
-
```javascript
|
|
564
|
-
// ❌ Bad: Multiple single operations
|
|
565
|
-
for (item of items) {
|
|
566
|
-
await appendRow(item);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// ✅ Good: One batch operation
|
|
570
|
-
await batchOperations(items.map(item => ({
|
|
571
|
-
type: "append",
|
|
572
|
-
values: item.values
|
|
573
|
-
})));
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### 2. Specify Range When Reading
|
|
482
|
+
### 1. Specify Range When Reading
|
|
577
483
|
```javascript
|
|
578
484
|
// ❌ Bad: Read entire file
|
|
579
485
|
range: ""
|
|
@@ -582,13 +488,34 @@ range: ""
|
|
|
582
488
|
range: "A1:D100"
|
|
583
489
|
```
|
|
584
490
|
|
|
585
|
-
###
|
|
491
|
+
### 2. Use Efficient Workflows
|
|
586
492
|
- Combine related operations in one workflow
|
|
587
493
|
- Reduce number of API calls
|
|
588
494
|
- Use caching appropriately
|
|
589
495
|
|
|
590
496
|
## 🆕 Latest Features
|
|
591
497
|
|
|
498
|
+
### 🎉 Automatic Type Conversion (v1.0.3)
|
|
499
|
+
- ✅ **Smart Type Detection**: Automatically convert strings to appropriate data types
|
|
500
|
+
- ✅ **Number Conversion**: `"123"` → `123`, `"45.67"` → `45.67`
|
|
501
|
+
- ✅ **Boolean Conversion**: `"true"` → `true`, `"false"` → `false`
|
|
502
|
+
- ✅ **Null Conversion**: `"null"` or empty string → `null`
|
|
503
|
+
- ✅ **Date Conversion**: ISO format date strings auto-convert (`"2024-01-15"`)
|
|
504
|
+
- ✅ **Preserve Typed Values**: Numbers, booleans, etc. remain unchanged
|
|
505
|
+
- ✅ **All Operations**: Supported in both Append and Update operations
|
|
506
|
+
|
|
507
|
+
**Example:**
|
|
508
|
+
```json
|
|
509
|
+
{
|
|
510
|
+
"EmployeeID": "E001", // Remains string
|
|
511
|
+
"Age": "30", // Auto-converts to number 30
|
|
512
|
+
"Salary": "50000.50", // Auto-converts to 50000.50
|
|
513
|
+
"IsActive": "true", // Auto-converts to boolean true
|
|
514
|
+
"TerminationDate": "null", // Auto-converts to null
|
|
515
|
+
"HireDate": "2020-01-15" // Auto-converts to date format
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
592
519
|
### Object Mode
|
|
593
520
|
- ✅ Uses `/api/excel/append_object` API
|
|
594
521
|
- ✅ Automatically reads Excel headers (first row)
|
|
@@ -597,10 +524,9 @@ range: "A1:D100"
|
|
|
597
524
|
- ✅ No need to remember column order
|
|
598
525
|
|
|
599
526
|
### Advanced Update and Delete
|
|
600
|
-
- ✅ Support operations by row number
|
|
601
527
|
- ✅ Support operations by column value lookup
|
|
602
528
|
- ✅ Can update specific columns without affecting others
|
|
603
|
-
- ✅
|
|
529
|
+
- ✅ Process modes: all matching records or first match only
|
|
604
530
|
|
|
605
531
|
### Lookup Column Selection
|
|
606
532
|
- ✅ Dynamic dropdown selection of Excel headers
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
1
|
+
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodeListSearchResult, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
2
|
export declare class ExcelApi implements INodeType {
|
|
3
3
|
description: INodeTypeDescription;
|
|
4
4
|
methods: {
|
|
5
5
|
loadOptions: {
|
|
6
|
-
getExcelFiles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
6
|
getExcelSheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
7
|
getColumnNames(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
9
8
|
};
|
|
9
|
+
listSearch: {
|
|
10
|
+
searchExcelFiles(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult>;
|
|
11
|
+
searchExcelSheets(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult>;
|
|
12
|
+
searchColumnNames(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult>;
|
|
13
|
+
};
|
|
10
14
|
};
|
|
11
15
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
12
16
|
}
|
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ExcelApi = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
/**
|
|
6
|
+
* 自動轉換欄位值的型態
|
|
7
|
+
* 支援:數字、布林值、日期、null
|
|
8
|
+
*/
|
|
9
|
+
function convertValueType(value) {
|
|
10
|
+
// 如果已經是 null、undefined,直接返回
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
// 如果不是字串,保持原樣
|
|
15
|
+
if (typeof value !== 'string') {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
// 處理空字串
|
|
19
|
+
if (value.trim() === '') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// 處理 "null" 字串
|
|
23
|
+
if (value.toLowerCase() === 'null') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// 處理布林值
|
|
27
|
+
if (value.toLowerCase() === 'true') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (value.toLowerCase() === 'false') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// 處理數字(整數和浮點數)
|
|
34
|
+
// 使用正則表達式確保是有效的數字格式
|
|
35
|
+
if (/^-?\d+(\.\d+)?$/.test(value.trim())) {
|
|
36
|
+
const num = Number(value);
|
|
37
|
+
if (!isNaN(num)) {
|
|
38
|
+
return num;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 處理日期格式
|
|
42
|
+
// 只有日期(yyyy-MM-dd),直接回傳字串,不附加時間
|
|
43
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value.trim())) {
|
|
44
|
+
return value.trim();
|
|
45
|
+
}
|
|
46
|
+
// 有時間的 ISO 格式(yyyy-MM-ddTHH:mm:ss...),才轉換為 ISO 字串
|
|
47
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value.trim())) {
|
|
48
|
+
const date = new Date(value);
|
|
49
|
+
if (!isNaN(date.getTime())) {
|
|
50
|
+
return date.toISOString();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 其他情況保持原字串
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 遞迴轉換物件或陣列中的所有值
|
|
58
|
+
*/
|
|
59
|
+
function convertObjectValues(obj) {
|
|
60
|
+
if (obj === null || obj === undefined) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(obj)) {
|
|
64
|
+
return obj.map(item => convertObjectValues(item));
|
|
65
|
+
}
|
|
66
|
+
if (typeof obj === 'object') {
|
|
67
|
+
const converted = {};
|
|
68
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
69
|
+
converted[key] = convertObjectValues(value);
|
|
70
|
+
}
|
|
71
|
+
return converted;
|
|
72
|
+
}
|
|
73
|
+
return convertValueType(obj);
|
|
74
|
+
}
|
|
5
75
|
class ExcelApi {
|
|
6
76
|
constructor() {
|
|
7
77
|
this.description = {
|
|
@@ -34,7 +104,6 @@ class ExcelApi {
|
|
|
34
104
|
{ name: 'Read', value: 'read', action: 'Read Excel file', description: 'Read data from Excel file' },
|
|
35
105
|
{ name: 'Update', value: 'update', action: 'Update row', description: 'Update an existing row' },
|
|
36
106
|
{ 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
107
|
],
|
|
39
108
|
default: 'append',
|
|
40
109
|
},
|
|
@@ -42,25 +111,54 @@ class ExcelApi {
|
|
|
42
111
|
{
|
|
43
112
|
displayName: 'File Name',
|
|
44
113
|
name: 'fileName',
|
|
45
|
-
type: '
|
|
114
|
+
type: 'resourceLocator',
|
|
46
115
|
required: true,
|
|
47
|
-
|
|
48
|
-
loadOptionsMethod: 'getExcelFiles',
|
|
49
|
-
},
|
|
50
|
-
default: '',
|
|
116
|
+
default: { mode: 'list', value: '' },
|
|
51
117
|
description: 'Select an Excel file from the server',
|
|
118
|
+
modes: [
|
|
119
|
+
{
|
|
120
|
+
displayName: 'From List',
|
|
121
|
+
name: 'list',
|
|
122
|
+
type: 'list',
|
|
123
|
+
typeOptions: {
|
|
124
|
+
searchListMethod: 'searchExcelFiles',
|
|
125
|
+
searchable: true,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
displayName: 'By Name',
|
|
130
|
+
name: 'name',
|
|
131
|
+
type: 'string',
|
|
132
|
+
placeholder: 'e.g. report.xlsx',
|
|
133
|
+
hint: 'Enter the exact Excel filename',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
52
136
|
},
|
|
53
137
|
// Sheet selection
|
|
54
138
|
{
|
|
55
139
|
displayName: 'Sheet Name',
|
|
56
140
|
name: 'sheetName',
|
|
57
|
-
type: '
|
|
58
|
-
|
|
59
|
-
loadOptionsMethod: 'getExcelSheets',
|
|
60
|
-
loadOptionsDependsOn: ['fileName'],
|
|
61
|
-
},
|
|
62
|
-
default: 'Sheet1',
|
|
141
|
+
type: 'resourceLocator',
|
|
142
|
+
default: { mode: 'list', value: '' },
|
|
63
143
|
description: 'Select a worksheet from the file',
|
|
144
|
+
modes: [
|
|
145
|
+
{
|
|
146
|
+
displayName: 'From List',
|
|
147
|
+
name: 'list',
|
|
148
|
+
type: 'list',
|
|
149
|
+
typeOptions: {
|
|
150
|
+
searchListMethod: 'searchExcelSheets',
|
|
151
|
+
searchable: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
displayName: 'By Name',
|
|
156
|
+
name: 'name',
|
|
157
|
+
type: 'string',
|
|
158
|
+
placeholder: 'e.g. Sheet1',
|
|
159
|
+
hint: 'Enter the exact worksheet name',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
64
162
|
},
|
|
65
163
|
// Append operation - Mode selection
|
|
66
164
|
{
|
|
@@ -90,10 +188,10 @@ class ExcelApi {
|
|
|
90
188
|
appendMode: ['object']
|
|
91
189
|
}
|
|
92
190
|
},
|
|
93
|
-
default: '{\n "Column1":
|
|
191
|
+
default: '{{ JSON.stringify({\n "Column1": $json["field1"],\n "Column2": $json["field2"]\n}) }}',
|
|
94
192
|
required: true,
|
|
95
193
|
description: 'Object with column names as keys. Column names must match Excel headers exactly.',
|
|
96
|
-
hint: 'Example: {"員工編號":
|
|
194
|
+
hint: 'Example: {{ JSON.stringify({ "員工編號": $json["employeeId"], "姓名": $json["name"] }) }}',
|
|
97
195
|
},
|
|
98
196
|
// Append - Array Mode
|
|
99
197
|
{
|
|
@@ -121,54 +219,38 @@ class ExcelApi {
|
|
|
121
219
|
description: 'Cell range to read (e.g., A1:D10). Leave empty to read all data',
|
|
122
220
|
placeholder: 'A1:D10',
|
|
123
221
|
},
|
|
124
|
-
//
|
|
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
|
-
// ✨ NEW: Lookup Column - 改為下拉選單
|
|
222
|
+
// Lookup Column
|
|
154
223
|
{
|
|
155
224
|
displayName: 'Lookup Column',
|
|
156
225
|
name: 'lookupColumn',
|
|
157
|
-
type: '
|
|
158
|
-
|
|
159
|
-
loadOptionsMethod: 'getColumnNames',
|
|
160
|
-
loadOptionsDependsOn: ['fileName', 'sheetName'],
|
|
161
|
-
},
|
|
226
|
+
type: 'resourceLocator',
|
|
227
|
+
default: { mode: 'list', value: '' },
|
|
162
228
|
displayOptions: {
|
|
163
229
|
show: {
|
|
164
230
|
operation: ['update', 'delete'],
|
|
165
|
-
identifyBy: ['lookup']
|
|
166
231
|
}
|
|
167
232
|
},
|
|
168
233
|
required: true,
|
|
169
|
-
default: '',
|
|
170
234
|
description: 'Column name to search in (automatically loaded from Excel headers)',
|
|
171
235
|
hint: '💡 The list shows all column names from the first row of your Excel file',
|
|
236
|
+
modes: [
|
|
237
|
+
{
|
|
238
|
+
displayName: 'From List',
|
|
239
|
+
name: 'list',
|
|
240
|
+
type: 'list',
|
|
241
|
+
typeOptions: {
|
|
242
|
+
searchListMethod: 'searchColumnNames',
|
|
243
|
+
searchable: true,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
displayName: 'By Name',
|
|
248
|
+
name: 'name',
|
|
249
|
+
type: 'string',
|
|
250
|
+
placeholder: 'e.g. EmployeeID',
|
|
251
|
+
hint: 'Enter the exact column name from the first row of your Excel file',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
172
254
|
},
|
|
173
255
|
// Lookup Value (for lookup method)
|
|
174
256
|
{
|
|
@@ -178,7 +260,6 @@ class ExcelApi {
|
|
|
178
260
|
displayOptions: {
|
|
179
261
|
show: {
|
|
180
262
|
operation: ['update', 'delete'],
|
|
181
|
-
identifyBy: ['lookup']
|
|
182
263
|
}
|
|
183
264
|
},
|
|
184
265
|
required: true,
|
|
@@ -195,7 +276,6 @@ class ExcelApi {
|
|
|
195
276
|
displayOptions: {
|
|
196
277
|
show: {
|
|
197
278
|
operation: ['update', 'delete'],
|
|
198
|
-
identifyBy: ['lookup']
|
|
199
279
|
}
|
|
200
280
|
},
|
|
201
281
|
options: [
|
|
@@ -223,60 +303,15 @@ class ExcelApi {
|
|
|
223
303
|
default: '{\n "Status": "Done",\n "UpdatedDate": "2024-01-01"\n}',
|
|
224
304
|
required: true,
|
|
225
305
|
description: 'Object with column names as keys and new values',
|
|
226
|
-
hint: 'Example: {"Status":
|
|
227
|
-
},
|
|
228
|
-
// Batch operation
|
|
229
|
-
{
|
|
230
|
-
displayName: 'Operations',
|
|
231
|
-
name: 'batchOperations',
|
|
232
|
-
type: 'json',
|
|
233
|
-
displayOptions: { show: { operation: ['batch'] } },
|
|
234
|
-
default: `[
|
|
235
|
-
{
|
|
236
|
-
"type": "append",
|
|
237
|
-
"values": ["value1", "value2"]
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
"type": "update",
|
|
241
|
-
"row": 5,
|
|
242
|
-
"values": ["new1", "new2"]
|
|
243
|
-
}
|
|
244
|
-
]`,
|
|
245
|
-
required: true,
|
|
246
|
-
description: 'Array of operations to execute',
|
|
247
|
-
hint: 'Each operation should have "type" (append/update/delete) and related fields',
|
|
306
|
+
hint: 'Example: {{ JSON.stringify({ "Status": $json["status"], "Salary": $json["salary"] }) }}'
|
|
248
307
|
},
|
|
249
308
|
],
|
|
250
309
|
};
|
|
251
310
|
this.methods = {
|
|
252
311
|
loadOptions: {
|
|
253
|
-
async getExcelFiles() {
|
|
254
|
-
const credentials = await this.getCredentials('excelApiAuth');
|
|
255
|
-
const apiUrl = credentials.url;
|
|
256
|
-
const apiToken = credentials.token;
|
|
257
|
-
try {
|
|
258
|
-
const response = await this.helpers.request({
|
|
259
|
-
method: 'GET',
|
|
260
|
-
url: `${apiUrl}/api/excel/files`,
|
|
261
|
-
headers: {
|
|
262
|
-
'Authorization': `Bearer ${apiToken}`,
|
|
263
|
-
},
|
|
264
|
-
json: true,
|
|
265
|
-
});
|
|
266
|
-
if (response.success && response.files) {
|
|
267
|
-
return response.files.map((file) => ({
|
|
268
|
-
name: file,
|
|
269
|
-
value: file,
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
return [];
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
return [];
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
312
|
async getExcelSheets() {
|
|
279
|
-
const
|
|
313
|
+
const fileNameRaw = this.getNodeParameter('fileName');
|
|
314
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw);
|
|
280
315
|
if (!fileName) {
|
|
281
316
|
return [];
|
|
282
317
|
}
|
|
@@ -306,8 +341,10 @@ class ExcelApi {
|
|
|
306
341
|
},
|
|
307
342
|
// ✨ NEW: 獲取欄位名稱(表頭)
|
|
308
343
|
async getColumnNames() {
|
|
309
|
-
const
|
|
310
|
-
const
|
|
344
|
+
const fileNameRaw = this.getNodeParameter('fileName');
|
|
345
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw);
|
|
346
|
+
const sheetNameRaw = this.getNodeParameter('sheetName');
|
|
347
|
+
const sheetName = (sheetNameRaw && typeof sheetNameRaw === 'object' ? sheetNameRaw.value : sheetNameRaw);
|
|
311
348
|
if (!fileName || !sheetName) {
|
|
312
349
|
return [];
|
|
313
350
|
}
|
|
@@ -337,6 +374,101 @@ class ExcelApi {
|
|
|
337
374
|
}
|
|
338
375
|
},
|
|
339
376
|
},
|
|
377
|
+
listSearch: {
|
|
378
|
+
async searchExcelFiles(filter) {
|
|
379
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
380
|
+
const apiUrl = credentials.url;
|
|
381
|
+
const apiToken = credentials.token;
|
|
382
|
+
try {
|
|
383
|
+
const response = await this.helpers.request({
|
|
384
|
+
method: 'GET',
|
|
385
|
+
url: `${apiUrl}/api/excel/files`,
|
|
386
|
+
headers: { 'Authorization': `Bearer ${apiToken}` },
|
|
387
|
+
json: true,
|
|
388
|
+
});
|
|
389
|
+
if (response.success && response.files) {
|
|
390
|
+
let files = response.files;
|
|
391
|
+
if (filter) {
|
|
392
|
+
const f = filter.toLowerCase();
|
|
393
|
+
files = files.filter((file) => file.toLowerCase().includes(f));
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
results: files.map((file) => ({ name: file, value: file })),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return { results: [] };
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return { results: [] };
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
async searchExcelSheets(filter) {
|
|
406
|
+
const fileNameRaw = this.getNodeParameter('fileName');
|
|
407
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw);
|
|
408
|
+
if (!fileName) {
|
|
409
|
+
return { results: [] };
|
|
410
|
+
}
|
|
411
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
412
|
+
const apiUrl = credentials.url;
|
|
413
|
+
const apiToken = credentials.token;
|
|
414
|
+
try {
|
|
415
|
+
const response = await this.helpers.request({
|
|
416
|
+
method: 'GET',
|
|
417
|
+
url: `${apiUrl}/api/excel/sheets?file=${encodeURIComponent(fileName)}`,
|
|
418
|
+
headers: { 'Authorization': `Bearer ${apiToken}` },
|
|
419
|
+
json: true,
|
|
420
|
+
});
|
|
421
|
+
if (response.success && response.sheets) {
|
|
422
|
+
let sheets = response.sheets;
|
|
423
|
+
if (filter) {
|
|
424
|
+
const f = filter.toLowerCase();
|
|
425
|
+
sheets = sheets.filter((s) => s.toLowerCase().includes(f));
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
results: sheets.map((sheet) => ({ name: sheet, value: sheet })),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { results: [] };
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
return { results: [] };
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
async searchColumnNames(filter) {
|
|
438
|
+
const fileNameRaw = this.getNodeParameter('fileName');
|
|
439
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw);
|
|
440
|
+
const sheetNameRaw = this.getNodeParameter('sheetName');
|
|
441
|
+
const sheetName = (sheetNameRaw && typeof sheetNameRaw === 'object' ? sheetNameRaw.value : sheetNameRaw);
|
|
442
|
+
if (!fileName || !sheetName) {
|
|
443
|
+
return { results: [] };
|
|
444
|
+
}
|
|
445
|
+
const credentials = await this.getCredentials('excelApiAuth');
|
|
446
|
+
const apiUrl = credentials.url;
|
|
447
|
+
const apiToken = credentials.token;
|
|
448
|
+
try {
|
|
449
|
+
const response = await this.helpers.request({
|
|
450
|
+
method: 'GET',
|
|
451
|
+
url: `${apiUrl}/api/excel/headers?file=${encodeURIComponent(fileName)}&sheet=${encodeURIComponent(sheetName)}`,
|
|
452
|
+
headers: { 'Authorization': `Bearer ${apiToken}` },
|
|
453
|
+
json: true,
|
|
454
|
+
});
|
|
455
|
+
if (response.success && response.headers) {
|
|
456
|
+
let headers = response.headers;
|
|
457
|
+
if (filter) {
|
|
458
|
+
const f = filter.toLowerCase();
|
|
459
|
+
headers = headers.filter((h) => h.toLowerCase().includes(f));
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
results: headers.map((header) => ({ name: header, value: header })),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return { results: [] };
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
return { results: [] };
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
},
|
|
340
472
|
};
|
|
341
473
|
}
|
|
342
474
|
async execute() {
|
|
@@ -348,15 +480,17 @@ class ExcelApi {
|
|
|
348
480
|
const credentials = await this.getCredentials('excelApiAuth');
|
|
349
481
|
const apiUrl = credentials.url;
|
|
350
482
|
const apiToken = credentials.token;
|
|
351
|
-
// Common parameters
|
|
352
|
-
const fileName = this.getNodeParameter('fileName', 0);
|
|
353
|
-
const sheetName = this.getNodeParameter('sheetName', 0) || 'Sheet1';
|
|
354
|
-
if (!fileName) {
|
|
355
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'File Name is required. Please select an Excel file.');
|
|
356
|
-
}
|
|
357
483
|
try {
|
|
358
484
|
for (let i = 0; i < items.length; i++) {
|
|
359
485
|
let responseData;
|
|
486
|
+
// Common parameters (per-item to support Expression mode)
|
|
487
|
+
const fileNameRaw = this.getNodeParameter('fileName', i);
|
|
488
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw) || '';
|
|
489
|
+
const sheetNameRaw = this.getNodeParameter('sheetName', i);
|
|
490
|
+
const sheetName = (sheetNameRaw && typeof sheetNameRaw === 'object' ? sheetNameRaw.value : sheetNameRaw) || '';
|
|
491
|
+
if (!fileName) {
|
|
492
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'File Name is required. Please select an Excel file.');
|
|
493
|
+
}
|
|
360
494
|
if (operation === 'append') {
|
|
361
495
|
// Get append mode to determine which parameter to use
|
|
362
496
|
const appendMode = this.getNodeParameter('appendMode', i);
|
|
@@ -374,6 +508,8 @@ class ExcelApi {
|
|
|
374
508
|
else {
|
|
375
509
|
appendValues = appendValuesRaw;
|
|
376
510
|
}
|
|
511
|
+
// 自動轉換值的型態
|
|
512
|
+
const convertedValues = convertObjectValues(appendValues);
|
|
377
513
|
// Use different API endpoint based on append mode
|
|
378
514
|
if (appendMode === 'object') {
|
|
379
515
|
// Object Mode: Use append_object API
|
|
@@ -387,7 +523,7 @@ class ExcelApi {
|
|
|
387
523
|
body: {
|
|
388
524
|
file: fileName,
|
|
389
525
|
sheet: sheetName,
|
|
390
|
-
values:
|
|
526
|
+
values: convertedValues,
|
|
391
527
|
},
|
|
392
528
|
json: true,
|
|
393
529
|
});
|
|
@@ -404,7 +540,7 @@ class ExcelApi {
|
|
|
404
540
|
body: {
|
|
405
541
|
file: fileName,
|
|
406
542
|
sheet: sheetName,
|
|
407
|
-
values:
|
|
543
|
+
values: convertedValues,
|
|
408
544
|
},
|
|
409
545
|
json: true,
|
|
410
546
|
});
|
|
@@ -428,6 +564,10 @@ class ExcelApi {
|
|
|
428
564
|
});
|
|
429
565
|
if (responseData.success && responseData.data) {
|
|
430
566
|
const data = responseData.data;
|
|
567
|
+
if (data.length <= 1) {
|
|
568
|
+
returnData.push({ json: { success: true, data: [] } });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
431
571
|
if (data.length > 1) {
|
|
432
572
|
const headers = data[0];
|
|
433
573
|
const hasHeaders = headers.every((h) => typeof h === 'string' && h.length > 0);
|
|
@@ -448,8 +588,6 @@ class ExcelApi {
|
|
|
448
588
|
}
|
|
449
589
|
}
|
|
450
590
|
else if (operation === 'update') {
|
|
451
|
-
// Get identification method
|
|
452
|
-
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
453
591
|
const valuesToSetRaw = this.getNodeParameter('valuesToSet', i);
|
|
454
592
|
let valuesToSet;
|
|
455
593
|
if (typeof valuesToSetRaw === 'string') {
|
|
@@ -463,24 +601,21 @@ class ExcelApi {
|
|
|
463
601
|
else {
|
|
464
602
|
valuesToSet = valuesToSetRaw;
|
|
465
603
|
}
|
|
604
|
+
// 自動轉換値的型態
|
|
605
|
+
const convertedValuesToSet = convertObjectValues(valuesToSet);
|
|
466
606
|
// Build request body
|
|
467
607
|
const requestBody = {
|
|
468
608
|
file: fileName,
|
|
469
609
|
sheet: sheetName,
|
|
470
|
-
values_to_set:
|
|
610
|
+
values_to_set: convertedValuesToSet,
|
|
471
611
|
};
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const processMode = this.getNodeParameter('processMode', i);
|
|
480
|
-
requestBody.lookup_column = lookupColumn;
|
|
481
|
-
requestBody.lookup_value = lookupValue;
|
|
482
|
-
requestBody.process_all = (processMode === 'all');
|
|
483
|
-
}
|
|
612
|
+
const lookupColumnRaw = this.getNodeParameter('lookupColumn', i);
|
|
613
|
+
const lookupColumn = (lookupColumnRaw && typeof lookupColumnRaw === 'object' ? lookupColumnRaw.value : lookupColumnRaw);
|
|
614
|
+
const lookupValue = this.getNodeParameter('lookupValue', i);
|
|
615
|
+
const processMode = this.getNodeParameter('processMode', i);
|
|
616
|
+
requestBody.lookup_column = lookupColumn;
|
|
617
|
+
requestBody.lookup_value = lookupValue;
|
|
618
|
+
requestBody.process_all = (processMode === 'all');
|
|
484
619
|
responseData = await this.helpers.request({
|
|
485
620
|
method: 'PUT',
|
|
486
621
|
url: `${apiUrl}/api/excel/update_advanced`,
|
|
@@ -493,31 +628,22 @@ class ExcelApi {
|
|
|
493
628
|
});
|
|
494
629
|
// Check if any rows were affected
|
|
495
630
|
if (responseData.success && responseData.updated_count === 0) {
|
|
496
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(),
|
|
497
|
-
? `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`
|
|
498
|
-
: `Row ${requestBody.row} not found or is protected (header row cannot be updated)`);
|
|
631
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`);
|
|
499
632
|
}
|
|
500
633
|
}
|
|
501
634
|
else if (operation === 'delete') {
|
|
502
|
-
// Get identification method
|
|
503
|
-
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
504
635
|
// Build request body
|
|
505
636
|
const requestBody = {
|
|
506
637
|
file: fileName,
|
|
507
638
|
sheet: sheetName,
|
|
508
639
|
};
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const processMode = this.getNodeParameter('processMode', i);
|
|
517
|
-
requestBody.lookup_column = lookupColumn;
|
|
518
|
-
requestBody.lookup_value = lookupValue;
|
|
519
|
-
requestBody.process_all = (processMode === 'all');
|
|
520
|
-
}
|
|
640
|
+
const lookupColumnRaw = this.getNodeParameter('lookupColumn', i);
|
|
641
|
+
const lookupColumn = (lookupColumnRaw && typeof lookupColumnRaw === 'object' ? lookupColumnRaw.value : lookupColumnRaw);
|
|
642
|
+
const lookupValue = this.getNodeParameter('lookupValue', i);
|
|
643
|
+
const processMode = this.getNodeParameter('processMode', i);
|
|
644
|
+
requestBody.lookup_column = lookupColumn;
|
|
645
|
+
requestBody.lookup_value = lookupValue;
|
|
646
|
+
requestBody.process_all = (processMode === 'all');
|
|
521
647
|
responseData = await this.helpers.request({
|
|
522
648
|
method: 'DELETE',
|
|
523
649
|
url: `${apiUrl}/api/excel/delete_advanced`,
|
|
@@ -530,40 +656,9 @@ class ExcelApi {
|
|
|
530
656
|
});
|
|
531
657
|
// Check if any rows were affected
|
|
532
658
|
if (responseData.success && responseData.deleted_count === 0) {
|
|
533
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(),
|
|
534
|
-
? `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`
|
|
535
|
-
: `Row ${requestBody.row} not found or is protected (header row cannot be deleted)`);
|
|
659
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`);
|
|
536
660
|
}
|
|
537
661
|
}
|
|
538
|
-
else if (operation === 'batch') {
|
|
539
|
-
const batchOperationsRaw = this.getNodeParameter('batchOperations', i);
|
|
540
|
-
let batchOperations;
|
|
541
|
-
if (typeof batchOperationsRaw === 'string') {
|
|
542
|
-
try {
|
|
543
|
-
batchOperations = JSON.parse(batchOperationsRaw);
|
|
544
|
-
}
|
|
545
|
-
catch {
|
|
546
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Batch Operations must be a valid JSON array');
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
batchOperations = batchOperationsRaw;
|
|
551
|
-
}
|
|
552
|
-
responseData = await this.helpers.request({
|
|
553
|
-
method: 'POST',
|
|
554
|
-
url: `${apiUrl}/api/excel/batch`,
|
|
555
|
-
headers: {
|
|
556
|
-
'Authorization': `Bearer ${apiToken}`,
|
|
557
|
-
'Content-Type': 'application/json',
|
|
558
|
-
},
|
|
559
|
-
body: {
|
|
560
|
-
file: fileName,
|
|
561
|
-
sheet: sheetName,
|
|
562
|
-
operations: batchOperations,
|
|
563
|
-
},
|
|
564
|
-
json: true,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
662
|
else {
|
|
568
663
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported operation: ${operation}`);
|
|
569
664
|
}
|