n8n-nodes-excel-api 1.0.3 → 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 +15 -110
- package/dist/nodes/ExcelApi/ExcelApi.node.d.ts +6 -2
- package/dist/nodes/ExcelApi/ExcelApi.node.js +204 -180
- 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,7 +488,7 @@ 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
|
|
@@ -618,10 +524,9 @@ range: "A1:D100"
|
|
|
618
524
|
- ✅ No need to remember column order
|
|
619
525
|
|
|
620
526
|
### Advanced Update and Delete
|
|
621
|
-
- ✅ Support operations by row number
|
|
622
527
|
- ✅ Support operations by column value lookup
|
|
623
528
|
- ✅ Can update specific columns without affecting others
|
|
624
|
-
- ✅
|
|
529
|
+
- ✅ Process modes: all matching records or first match only
|
|
625
530
|
|
|
626
531
|
### Lookup Column Selection
|
|
627
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
|
}
|
|
@@ -38,12 +38,15 @@ function convertValueType(value) {
|
|
|
38
38
|
return num;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
if (/^\d{4}-\d{2}-\d{2}
|
|
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())) {
|
|
44
48
|
const date = new Date(value);
|
|
45
49
|
if (!isNaN(date.getTime())) {
|
|
46
|
-
// 返回 ISO 字串格式,Excel API Server 會處理
|
|
47
50
|
return date.toISOString();
|
|
48
51
|
}
|
|
49
52
|
}
|
|
@@ -55,15 +58,15 @@ function convertValueType(value) {
|
|
|
55
58
|
*/
|
|
56
59
|
function convertObjectValues(obj) {
|
|
57
60
|
if (obj === null || obj === undefined) {
|
|
58
|
-
return
|
|
61
|
+
return null;
|
|
59
62
|
}
|
|
60
63
|
if (Array.isArray(obj)) {
|
|
61
|
-
return obj.map(item =>
|
|
64
|
+
return obj.map(item => convertObjectValues(item));
|
|
62
65
|
}
|
|
63
66
|
if (typeof obj === 'object') {
|
|
64
67
|
const converted = {};
|
|
65
68
|
for (const [key, value] of Object.entries(obj)) {
|
|
66
|
-
converted[key] =
|
|
69
|
+
converted[key] = convertObjectValues(value);
|
|
67
70
|
}
|
|
68
71
|
return converted;
|
|
69
72
|
}
|
|
@@ -101,7 +104,6 @@ class ExcelApi {
|
|
|
101
104
|
{ name: 'Read', value: 'read', action: 'Read Excel file', description: 'Read data from Excel file' },
|
|
102
105
|
{ name: 'Update', value: 'update', action: 'Update row', description: 'Update an existing row' },
|
|
103
106
|
{ name: 'Delete', value: 'delete', action: 'Delete row', description: 'Delete a row' },
|
|
104
|
-
{ name: 'Batch', value: 'batch', action: 'Batch operations', description: 'Execute multiple operations at once' },
|
|
105
107
|
],
|
|
106
108
|
default: 'append',
|
|
107
109
|
},
|
|
@@ -109,25 +111,54 @@ class ExcelApi {
|
|
|
109
111
|
{
|
|
110
112
|
displayName: 'File Name',
|
|
111
113
|
name: 'fileName',
|
|
112
|
-
type: '
|
|
114
|
+
type: 'resourceLocator',
|
|
113
115
|
required: true,
|
|
114
|
-
|
|
115
|
-
loadOptionsMethod: 'getExcelFiles',
|
|
116
|
-
},
|
|
117
|
-
default: '',
|
|
116
|
+
default: { mode: 'list', value: '' },
|
|
118
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
|
+
],
|
|
119
136
|
},
|
|
120
137
|
// Sheet selection
|
|
121
138
|
{
|
|
122
139
|
displayName: 'Sheet Name',
|
|
123
140
|
name: 'sheetName',
|
|
124
|
-
type: '
|
|
125
|
-
|
|
126
|
-
loadOptionsMethod: 'getExcelSheets',
|
|
127
|
-
loadOptionsDependsOn: ['fileName'],
|
|
128
|
-
},
|
|
129
|
-
default: 'Sheet1',
|
|
141
|
+
type: 'resourceLocator',
|
|
142
|
+
default: { mode: 'list', value: '' },
|
|
130
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
|
+
],
|
|
131
162
|
},
|
|
132
163
|
// Append operation - Mode selection
|
|
133
164
|
{
|
|
@@ -157,10 +188,10 @@ class ExcelApi {
|
|
|
157
188
|
appendMode: ['object']
|
|
158
189
|
}
|
|
159
190
|
},
|
|
160
|
-
default: '{\n "Column1":
|
|
191
|
+
default: '{{ JSON.stringify({\n "Column1": $json["field1"],\n "Column2": $json["field2"]\n}) }}',
|
|
161
192
|
required: true,
|
|
162
193
|
description: 'Object with column names as keys. Column names must match Excel headers exactly.',
|
|
163
|
-
hint: 'Example: {"員工編號":
|
|
194
|
+
hint: 'Example: {{ JSON.stringify({ "員工編號": $json["employeeId"], "姓名": $json["name"] }) }}',
|
|
164
195
|
},
|
|
165
196
|
// Append - Array Mode
|
|
166
197
|
{
|
|
@@ -188,54 +219,38 @@ class ExcelApi {
|
|
|
188
219
|
description: 'Cell range to read (e.g., A1:D10). Leave empty to read all data',
|
|
189
220
|
placeholder: 'A1:D10',
|
|
190
221
|
},
|
|
191
|
-
//
|
|
192
|
-
{
|
|
193
|
-
displayName: 'Identify Row By',
|
|
194
|
-
name: 'identifyBy',
|
|
195
|
-
type: 'options',
|
|
196
|
-
displayOptions: { show: { operation: ['update', 'delete'] } },
|
|
197
|
-
options: [
|
|
198
|
-
{ name: 'Row Number', value: 'rowNumber', description: 'Specify the exact row number' },
|
|
199
|
-
{ name: 'Lookup', value: 'lookup', description: 'Find row by matching a column value' },
|
|
200
|
-
],
|
|
201
|
-
default: 'rowNumber',
|
|
202
|
-
description: 'How to identify the row to update/delete',
|
|
203
|
-
},
|
|
204
|
-
// Row Number (for direct specification)
|
|
205
|
-
{
|
|
206
|
-
displayName: 'Row Number',
|
|
207
|
-
name: 'rowNumber',
|
|
208
|
-
type: 'number',
|
|
209
|
-
displayOptions: {
|
|
210
|
-
show: {
|
|
211
|
-
operation: ['update', 'delete'],
|
|
212
|
-
identifyBy: ['rowNumber']
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
required: true,
|
|
216
|
-
default: 2,
|
|
217
|
-
description: 'Row number to update/delete (1-based, row 1 is header)',
|
|
218
|
-
hint: '⚠️ Row 1 is protected (header row). Data rows start from row 2.',
|
|
219
|
-
},
|
|
220
|
-
// ✨ NEW: Lookup Column - 改為下拉選單
|
|
222
|
+
// Lookup Column
|
|
221
223
|
{
|
|
222
224
|
displayName: 'Lookup Column',
|
|
223
225
|
name: 'lookupColumn',
|
|
224
|
-
type: '
|
|
225
|
-
|
|
226
|
-
loadOptionsMethod: 'getColumnNames',
|
|
227
|
-
loadOptionsDependsOn: ['fileName', 'sheetName'],
|
|
228
|
-
},
|
|
226
|
+
type: 'resourceLocator',
|
|
227
|
+
default: { mode: 'list', value: '' },
|
|
229
228
|
displayOptions: {
|
|
230
229
|
show: {
|
|
231
230
|
operation: ['update', 'delete'],
|
|
232
|
-
identifyBy: ['lookup']
|
|
233
231
|
}
|
|
234
232
|
},
|
|
235
233
|
required: true,
|
|
236
|
-
default: '',
|
|
237
234
|
description: 'Column name to search in (automatically loaded from Excel headers)',
|
|
238
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
|
+
],
|
|
239
254
|
},
|
|
240
255
|
// Lookup Value (for lookup method)
|
|
241
256
|
{
|
|
@@ -245,7 +260,6 @@ class ExcelApi {
|
|
|
245
260
|
displayOptions: {
|
|
246
261
|
show: {
|
|
247
262
|
operation: ['update', 'delete'],
|
|
248
|
-
identifyBy: ['lookup']
|
|
249
263
|
}
|
|
250
264
|
},
|
|
251
265
|
required: true,
|
|
@@ -262,7 +276,6 @@ class ExcelApi {
|
|
|
262
276
|
displayOptions: {
|
|
263
277
|
show: {
|
|
264
278
|
operation: ['update', 'delete'],
|
|
265
|
-
identifyBy: ['lookup']
|
|
266
279
|
}
|
|
267
280
|
},
|
|
268
281
|
options: [
|
|
@@ -290,60 +303,15 @@ class ExcelApi {
|
|
|
290
303
|
default: '{\n "Status": "Done",\n "UpdatedDate": "2024-01-01"\n}',
|
|
291
304
|
required: true,
|
|
292
305
|
description: 'Object with column names as keys and new values',
|
|
293
|
-
hint: 'Example: {"Status":
|
|
294
|
-
},
|
|
295
|
-
// Batch operation
|
|
296
|
-
{
|
|
297
|
-
displayName: 'Operations',
|
|
298
|
-
name: 'batchOperations',
|
|
299
|
-
type: 'json',
|
|
300
|
-
displayOptions: { show: { operation: ['batch'] } },
|
|
301
|
-
default: `[
|
|
302
|
-
{
|
|
303
|
-
"type": "append",
|
|
304
|
-
"values": ["value1", "value2"]
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
"type": "update",
|
|
308
|
-
"row": 5,
|
|
309
|
-
"values": ["new1", "new2"]
|
|
310
|
-
}
|
|
311
|
-
]`,
|
|
312
|
-
required: true,
|
|
313
|
-
description: 'Array of operations to execute',
|
|
314
|
-
hint: 'Each operation should have "type" (append/update/delete) and related fields',
|
|
306
|
+
hint: 'Example: {{ JSON.stringify({ "Status": $json["status"], "Salary": $json["salary"] }) }}'
|
|
315
307
|
},
|
|
316
308
|
],
|
|
317
309
|
};
|
|
318
310
|
this.methods = {
|
|
319
311
|
loadOptions: {
|
|
320
|
-
async getExcelFiles() {
|
|
321
|
-
const credentials = await this.getCredentials('excelApiAuth');
|
|
322
|
-
const apiUrl = credentials.url;
|
|
323
|
-
const apiToken = credentials.token;
|
|
324
|
-
try {
|
|
325
|
-
const response = await this.helpers.request({
|
|
326
|
-
method: 'GET',
|
|
327
|
-
url: `${apiUrl}/api/excel/files`,
|
|
328
|
-
headers: {
|
|
329
|
-
'Authorization': `Bearer ${apiToken}`,
|
|
330
|
-
},
|
|
331
|
-
json: true,
|
|
332
|
-
});
|
|
333
|
-
if (response.success && response.files) {
|
|
334
|
-
return response.files.map((file) => ({
|
|
335
|
-
name: file,
|
|
336
|
-
value: file,
|
|
337
|
-
}));
|
|
338
|
-
}
|
|
339
|
-
return [];
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
return [];
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
312
|
async getExcelSheets() {
|
|
346
|
-
const
|
|
313
|
+
const fileNameRaw = this.getNodeParameter('fileName');
|
|
314
|
+
const fileName = (fileNameRaw && typeof fileNameRaw === 'object' ? fileNameRaw.value : fileNameRaw);
|
|
347
315
|
if (!fileName) {
|
|
348
316
|
return [];
|
|
349
317
|
}
|
|
@@ -373,8 +341,10 @@ class ExcelApi {
|
|
|
373
341
|
},
|
|
374
342
|
// ✨ NEW: 獲取欄位名稱(表頭)
|
|
375
343
|
async getColumnNames() {
|
|
376
|
-
const
|
|
377
|
-
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);
|
|
378
348
|
if (!fileName || !sheetName) {
|
|
379
349
|
return [];
|
|
380
350
|
}
|
|
@@ -404,6 +374,101 @@ class ExcelApi {
|
|
|
404
374
|
}
|
|
405
375
|
},
|
|
406
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
|
+
},
|
|
407
472
|
};
|
|
408
473
|
}
|
|
409
474
|
async execute() {
|
|
@@ -415,15 +480,17 @@ class ExcelApi {
|
|
|
415
480
|
const credentials = await this.getCredentials('excelApiAuth');
|
|
416
481
|
const apiUrl = credentials.url;
|
|
417
482
|
const apiToken = credentials.token;
|
|
418
|
-
// Common parameters
|
|
419
|
-
const fileName = this.getNodeParameter('fileName', 0);
|
|
420
|
-
const sheetName = this.getNodeParameter('sheetName', 0) || 'Sheet1';
|
|
421
|
-
if (!fileName) {
|
|
422
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'File Name is required. Please select an Excel file.');
|
|
423
|
-
}
|
|
424
483
|
try {
|
|
425
484
|
for (let i = 0; i < items.length; i++) {
|
|
426
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
|
+
}
|
|
427
494
|
if (operation === 'append') {
|
|
428
495
|
// Get append mode to determine which parameter to use
|
|
429
496
|
const appendMode = this.getNodeParameter('appendMode', i);
|
|
@@ -497,6 +564,10 @@ class ExcelApi {
|
|
|
497
564
|
});
|
|
498
565
|
if (responseData.success && responseData.data) {
|
|
499
566
|
const data = responseData.data;
|
|
567
|
+
if (data.length <= 1) {
|
|
568
|
+
returnData.push({ json: { success: true, data: [] } });
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
500
571
|
if (data.length > 1) {
|
|
501
572
|
const headers = data[0];
|
|
502
573
|
const hasHeaders = headers.every((h) => typeof h === 'string' && h.length > 0);
|
|
@@ -517,8 +588,6 @@ class ExcelApi {
|
|
|
517
588
|
}
|
|
518
589
|
}
|
|
519
590
|
else if (operation === 'update') {
|
|
520
|
-
// Get identification method
|
|
521
|
-
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
522
591
|
const valuesToSetRaw = this.getNodeParameter('valuesToSet', i);
|
|
523
592
|
let valuesToSet;
|
|
524
593
|
if (typeof valuesToSetRaw === 'string') {
|
|
@@ -532,7 +601,7 @@ class ExcelApi {
|
|
|
532
601
|
else {
|
|
533
602
|
valuesToSet = valuesToSetRaw;
|
|
534
603
|
}
|
|
535
|
-
//
|
|
604
|
+
// 自動轉換値的型態
|
|
536
605
|
const convertedValuesToSet = convertObjectValues(valuesToSet);
|
|
537
606
|
// Build request body
|
|
538
607
|
const requestBody = {
|
|
@@ -540,18 +609,13 @@ class ExcelApi {
|
|
|
540
609
|
sheet: sheetName,
|
|
541
610
|
values_to_set: convertedValuesToSet,
|
|
542
611
|
};
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const processMode = this.getNodeParameter('processMode', i);
|
|
551
|
-
requestBody.lookup_column = lookupColumn;
|
|
552
|
-
requestBody.lookup_value = lookupValue;
|
|
553
|
-
requestBody.process_all = (processMode === 'all');
|
|
554
|
-
}
|
|
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');
|
|
555
619
|
responseData = await this.helpers.request({
|
|
556
620
|
method: 'PUT',
|
|
557
621
|
url: `${apiUrl}/api/excel/update_advanced`,
|
|
@@ -564,31 +628,22 @@ class ExcelApi {
|
|
|
564
628
|
});
|
|
565
629
|
// Check if any rows were affected
|
|
566
630
|
if (responseData.success && responseData.updated_count === 0) {
|
|
567
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(),
|
|
568
|
-
? `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`
|
|
569
|
-
: `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}"`);
|
|
570
632
|
}
|
|
571
633
|
}
|
|
572
634
|
else if (operation === 'delete') {
|
|
573
|
-
// Get identification method
|
|
574
|
-
const identifyBy = this.getNodeParameter('identifyBy', i);
|
|
575
635
|
// Build request body
|
|
576
636
|
const requestBody = {
|
|
577
637
|
file: fileName,
|
|
578
638
|
sheet: sheetName,
|
|
579
639
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const processMode = this.getNodeParameter('processMode', i);
|
|
588
|
-
requestBody.lookup_column = lookupColumn;
|
|
589
|
-
requestBody.lookup_value = lookupValue;
|
|
590
|
-
requestBody.process_all = (processMode === 'all');
|
|
591
|
-
}
|
|
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');
|
|
592
647
|
responseData = await this.helpers.request({
|
|
593
648
|
method: 'DELETE',
|
|
594
649
|
url: `${apiUrl}/api/excel/delete_advanced`,
|
|
@@ -601,39 +656,8 @@ class ExcelApi {
|
|
|
601
656
|
});
|
|
602
657
|
// Check if any rows were affected
|
|
603
658
|
if (responseData.success && responseData.deleted_count === 0) {
|
|
604
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(),
|
|
605
|
-
? `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`
|
|
606
|
-
: `Row ${requestBody.row} not found or is protected (header row cannot be deleted)`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
else if (operation === 'batch') {
|
|
610
|
-
const batchOperationsRaw = this.getNodeParameter('batchOperations', i);
|
|
611
|
-
let batchOperations;
|
|
612
|
-
if (typeof batchOperationsRaw === 'string') {
|
|
613
|
-
try {
|
|
614
|
-
batchOperations = JSON.parse(batchOperationsRaw);
|
|
615
|
-
}
|
|
616
|
-
catch {
|
|
617
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Batch Operations must be a valid JSON array');
|
|
618
|
-
}
|
|
659
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No matching rows found. Lookup column: "${requestBody.lookup_column}", Lookup value: "${requestBody.lookup_value}"`);
|
|
619
660
|
}
|
|
620
|
-
else {
|
|
621
|
-
batchOperations = batchOperationsRaw;
|
|
622
|
-
}
|
|
623
|
-
responseData = await this.helpers.request({
|
|
624
|
-
method: 'POST',
|
|
625
|
-
url: `${apiUrl}/api/excel/batch`,
|
|
626
|
-
headers: {
|
|
627
|
-
'Authorization': `Bearer ${apiToken}`,
|
|
628
|
-
'Content-Type': 'application/json',
|
|
629
|
-
},
|
|
630
|
-
body: {
|
|
631
|
-
file: fileName,
|
|
632
|
-
sheet: sheetName,
|
|
633
|
-
operations: batchOperations,
|
|
634
|
-
},
|
|
635
|
-
json: true,
|
|
636
|
-
});
|
|
637
661
|
}
|
|
638
662
|
else {
|
|
639
663
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported operation: ${operation}`);
|