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 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
- ##### 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,13 +488,34 @@ 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
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
- - ✅ Batch processing support with process modes
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: 'options',
114
+ type: 'resourceLocator',
46
115
  required: true,
47
- typeOptions: {
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: 'options',
58
- typeOptions: {
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": "{{ $json.field1 }}",\n "Column2": "{{ $json.field2 }}"\n}',
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: {"員工編號": "{{ $json.body.employeeId }}", "姓名": "{{ $json.body.name }}"}',
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
- // Update & Delete: Row Identification Method
125
- {
126
- displayName: 'Identify Row By',
127
- name: 'identifyBy',
128
- type: 'options',
129
- displayOptions: { show: { operation: ['update', 'delete'] } },
130
- options: [
131
- { name: 'Row Number', value: 'rowNumber', description: 'Specify the exact row number' },
132
- { name: 'Lookup', value: 'lookup', description: 'Find row by matching a column value' },
133
- ],
134
- default: 'rowNumber',
135
- description: 'How to identify the row to update/delete',
136
- },
137
- // Row Number (for direct specification)
138
- {
139
- displayName: 'Row Number',
140
- name: 'rowNumber',
141
- type: 'number',
142
- displayOptions: {
143
- show: {
144
- operation: ['update', 'delete'],
145
- identifyBy: ['rowNumber']
146
- }
147
- },
148
- required: true,
149
- default: 2,
150
- description: 'Row number to update/delete (1-based, row 1 is header)',
151
- hint: '⚠️ Row 1 is protected (header row). Data rows start from row 2.',
152
- },
153
- // ✨ NEW: Lookup Column - 改為下拉選單
222
+ // Lookup Column
154
223
  {
155
224
  displayName: 'Lookup Column',
156
225
  name: 'lookupColumn',
157
- type: 'options',
158
- typeOptions: {
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": "{{ $json.status }}", "Salary": {{ $json.salary }}}',
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 fileName = this.getNodeParameter('fileName');
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 fileName = this.getNodeParameter('fileName');
310
- 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);
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: appendValues,
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: appendValues,
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: valuesToSet,
610
+ values_to_set: convertedValuesToSet,
471
611
  };
472
- if (identifyBy === 'rowNumber') {
473
- const rowNumber = this.getNodeParameter('rowNumber', i);
474
- requestBody.row = rowNumber;
475
- }
476
- else if (identifyBy === 'lookup') {
477
- const lookupColumn = this.getNodeParameter('lookupColumn', i);
478
- const lookupValue = this.getNodeParameter('lookupValue', i);
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(), identifyBy === 'lookup'
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
- if (identifyBy === 'rowNumber') {
510
- const rowNumber = this.getNodeParameter('rowNumber', i);
511
- requestBody.row = rowNumber;
512
- }
513
- else if (identifyBy === 'lookup') {
514
- const lookupColumn = this.getNodeParameter('lookupColumn', i);
515
- const lookupValue = this.getNodeParameter('lookupValue', i);
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(), identifyBy === 'lookup'
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-excel-api",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "n8n node for accessing Excel files via API with concurrent safety",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",