n8n-nodes-excel-api 1.0.3 → 1.0.5

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 CHANGED
@@ -17,12 +17,11 @@ When working with Excel files directly in n8n:
17
17
  - ❌ Difficult to handle simultaneous webhook form submissions
18
18
 
19
19
  ### The Solution
20
- This node works with [Excel API Server](https://github.com/code4Copilot/excel-api-server) to provide:
20
+ This node works with [Excel API Server](https://gitlab.com/hueyanchen-group/excel-api-server.git) to provide:
21
21
  - ✅ **File Locking** - Automatically queue concurrent requests
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
 
@@ -36,7 +35,7 @@ npm install n8n-nodes-excel-api
36
35
 
37
36
  ```bash
38
37
  # 1. Clone repository
39
- git clone https://github.com/code4Copilot/n8n-nodes-excel-api.git
38
+ git clone https://gitlab.com/hueyanchen-group/n8n-nodes-excel-api.git
40
39
  cd n8n-nodes-excel-api
41
40
 
42
41
  # 2. Install dependencies
@@ -66,7 +65,7 @@ In n8n:
66
65
 
67
66
  **You must run Excel API Server first!**
68
67
 
69
- Install and start [Excel API Server](https://github.com/code4Copilot/excel-api-server):
68
+ Install and start [Excel API Server](https://gitlab.com/hueyanchen-group/excel-api-server.git):
70
69
 
71
70
  ```bash
72
71
  # Quick start with Docker
@@ -77,7 +76,7 @@ docker run -d \
77
76
  yourusername/excel-api-server
78
77
  ```
79
78
 
80
- See [Excel API Server Documentation](https://github.com/code4Copilot/excel-api-server) for details.
79
+ See [Excel API Server Documentation](https://gitlab.com/hueyanchen-group/excel-api-server.git) for details.
81
80
 
82
81
  ## 🔧 Configuration
83
82
 
@@ -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
- ##### All Matching Records - Default
181
- Update all matching rows, suitable for batch update scenarios.
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
- ##### First Match Only
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 batch updating multiple records
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 a row from the sheet.
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
- ##### All Matching Records - Default
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
- ##### First Match Only
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 when batch deleting to avoid accidental data loss
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: Batch Updates
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 5: Batch Department Status Update
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 6: Clean Up Expired Data
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. Use Batch Operations
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
- ### 3. Use Efficient Workflows
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
- - ✅ Batch processing support with process modes
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
@@ -645,12 +550,13 @@ MIT License - see [LICENSE](LICENSE) file
645
550
 
646
551
  ## 🔗 Related Projects
647
552
 
648
- - [Excel API Server](https://github.com/code4Copilot/excel-api-server) - Backend API server (Required)
553
+ - [Excel API Server](https://gitlab.com/hueyanchen-group/excel-api-server.git) - Backend API server (Required)
649
554
  - [n8n](https://github.com/n8n-io/n8n) - Workflow automation tool
650
555
 
651
556
  ## 📧 Support
652
557
 
653
- - GitHub Issues: [Report Issues](https://github.com/code4Copilot/n8n-nodes-excel-api/issues)
558
+ - Work Items: [GitLab Work Items](https://gitlab.com/hueyanchen-group/n8n-nodes-excel-api/-/work_items)
559
+ - Merge Requests: [GitLab Merge Requests](https://gitlab.com/hueyanchen-group/n8n-nodes-excel-api/-/merge_requests)
654
560
  - Email: hueyan.chen@gmail.com
655
561
  - n8n Community: [n8n Forum](https://community.n8n.io)
656
562
 
@@ -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
- // 處理 ISO 日期格式
42
- // 格式如:2024-01-15 或 2024-01-15T10:30:00 或 2024-01-15T10:30:00.000Z
43
- if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3}Z?)?)?$/.test(value.trim())) {
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 obj;
61
+ return null;
59
62
  }
60
63
  if (Array.isArray(obj)) {
61
- return obj.map(item => convertValueType(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] = convertValueType(value);
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: 'options',
114
+ type: 'resourceLocator',
113
115
  required: true,
114
- typeOptions: {
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: 'options',
125
- typeOptions: {
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": "{{ $json.field1 }}",\n "Column2": "{{ $json.field2 }}"\n}',
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: {"員工編號": "{{ $json.body.employeeId }}", "姓名": "{{ $json.body.name }}"}',
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
- // Update & Delete: Row Identification Method
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: 'options',
225
- typeOptions: {
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": "{{ $json.status }}", "Salary": {{ $json.salary }}}',
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 fileName = this.getNodeParameter('fileName');
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 fileName = this.getNodeParameter('fileName');
377
- const sheetName = this.getNodeParameter('sheetName');
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
- if (identifyBy === 'rowNumber') {
544
- const rowNumber = this.getNodeParameter('rowNumber', i);
545
- requestBody.row = rowNumber;
546
- }
547
- else if (identifyBy === 'lookup') {
548
- const lookupColumn = this.getNodeParameter('lookupColumn', i);
549
- const lookupValue = this.getNodeParameter('lookupValue', i);
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(), identifyBy === 'lookup'
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
- if (identifyBy === 'rowNumber') {
581
- const rowNumber = this.getNodeParameter('rowNumber', i);
582
- requestBody.row = rowNumber;
583
- }
584
- else if (identifyBy === 'lookup') {
585
- const lookupColumn = this.getNodeParameter('lookupColumn', i);
586
- const lookupValue = this.getNodeParameter('lookupValue', i);
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(), identifyBy === 'lookup'
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-excel-api",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "n8n node for accessing Excel files via API with concurrent safety",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -10,14 +10,14 @@
10
10
  "spreadsheet"
11
11
  ],
12
12
  "license": "MIT",
13
- "homepage": "https://github.com/code4Copilot/n8n-nodes-excel-api",
13
+ "homepage": "https://gitlab.com/hueyanchen-group/n8n-nodes-excel-api.git",
14
14
  "author": {
15
15
  "name": "Hueyan Chen",
16
16
  "email": "hueyan.chen@gmail.com"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "git+https://github.com/code4Copilot/n8n-nodes-excel-api.git"
20
+ "url": "git+https://gitlab.com/hueyanchen-group/n8n-nodes-excel-api.git"
21
21
  },
22
22
  "main": "index.js",
23
23
  "scripts": {