n8n-nodes-data-mapper 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DataMapper = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const mapping_1 = require("./mapping");
6
+ const profile_1 = require("./profile");
7
+ const visual_1 = require("./visual");
6
8
  const xml_1 = require("./xml");
7
9
  const show = (values) => ({ show: values });
8
10
  const mappingFields = [
@@ -12,10 +14,24 @@ const mappingFields = [
12
14
  type: 'options',
13
15
  options: [
14
16
  { name: 'Input Field', value: 'field', description: 'Read a value from the input by path' },
17
+ {
18
+ name: 'From Source Profile',
19
+ value: 'profile',
20
+ description: 'Pick a field from the source profile',
21
+ },
15
22
  { name: 'Fixed Value', value: 'value', description: 'Write a fixed value or the result of an expression' },
16
23
  ],
17
24
  default: 'field',
18
25
  },
26
+ {
27
+ displayName: 'Source Field Name or ID',
28
+ name: 'sourceField',
29
+ type: 'options',
30
+ typeOptions: { loadOptionsMethod: 'getSourceFields' },
31
+ default: '',
32
+ description: 'Field from the source profile. Fill the Source Profile field above first. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
33
+ displayOptions: show({ sourceType: ['profile'] }),
34
+ },
19
35
  {
20
36
  displayName: 'Source Path',
21
37
  name: 'source',
@@ -33,14 +49,37 @@ const mappingFields = [
33
49
  description: 'Fixed text, or an expression such as {{ $now.toISO() }}',
34
50
  displayOptions: show({ sourceType: ['value'] }),
35
51
  },
52
+ {
53
+ displayName: 'Target',
54
+ name: 'targetMode',
55
+ type: 'options',
56
+ options: [
57
+ { name: 'Custom Path', value: 'custom', description: 'Type the target path yourself' },
58
+ {
59
+ name: 'From Target Profile',
60
+ value: 'profile',
61
+ description: 'Pick a field from the uploaded target profile',
62
+ },
63
+ ],
64
+ default: 'custom',
65
+ },
66
+ {
67
+ displayName: 'Target Field Name or ID',
68
+ name: 'targetField',
69
+ type: 'options',
70
+ typeOptions: { loadOptionsMethod: 'getTargetFields' },
71
+ default: '',
72
+ description: 'Field from the target profile. Fill the Target Profile field above first. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
73
+ displayOptions: show({ targetMode: ['profile'] }),
74
+ },
36
75
  {
37
76
  displayName: 'Target Path',
38
77
  name: 'target',
39
78
  type: 'string',
40
79
  default: '',
41
- required: true,
42
80
  placeholder: 'shipping.cities[*]',
43
81
  description: 'Where to write the value in the output. Each [*] matches a [*] in the source path, in order.',
82
+ displayOptions: show({ targetMode: ['custom'] }),
44
83
  },
45
84
  {
46
85
  displayName: 'Transform',
@@ -137,14 +176,14 @@ const mappingFields = [
137
176
  { name: 'Stop With Error', value: 'error' },
138
177
  ],
139
178
  default: 'skip',
140
- displayOptions: show({ sourceType: ['field'] }),
179
+ displayOptions: show({ sourceType: ['field', 'profile'] }),
141
180
  },
142
181
  {
143
182
  displayName: 'Default Value',
144
183
  name: 'defaultValue',
145
184
  type: 'string',
146
185
  default: '',
147
- displayOptions: show({ sourceType: ['field'], ifMissing: ['default'] }),
186
+ displayOptions: show({ sourceType: ['field', 'profile'], ifMissing: ['default'] }),
148
187
  },
149
188
  ];
150
189
  const exampleDefinition = JSON.stringify([
@@ -154,6 +193,329 @@ const exampleDefinition = JSON.stringify([
154
193
  { source: 'items[*].qty', target: 'lines[*].quantity', transform: 'integer' },
155
194
  { value: 'EUR', target: 'lines[*].currency' },
156
195
  ], null, 2);
196
+ const v1Properties = [
197
+ {
198
+ displayName: 'Input',
199
+ name: 'inputSource',
200
+ type: 'options',
201
+ noDataExpression: true,
202
+ options: [
203
+ { name: 'Item JSON', value: 'json', description: 'Map the fields of each incoming item' },
204
+ {
205
+ name: 'Text Field',
206
+ value: 'field',
207
+ description: 'Parse JSON or XML text stored in a field first',
208
+ },
209
+ { name: 'File', value: 'binary', description: 'Parse a JSON or XML file first' },
210
+ ],
211
+ default: 'json',
212
+ },
213
+ {
214
+ displayName: 'Input Field',
215
+ name: 'inputField',
216
+ type: 'string',
217
+ default: 'data',
218
+ required: true,
219
+ displayOptions: show({ inputSource: ['field'] }),
220
+ },
221
+ {
222
+ displayName: 'Input Binary Field',
223
+ name: 'inputBinaryField',
224
+ type: 'string',
225
+ default: 'data',
226
+ required: true,
227
+ displayOptions: show({ inputSource: ['binary'] }),
228
+ },
229
+ {
230
+ displayName: 'Input Format',
231
+ name: 'inputFormat',
232
+ type: 'options',
233
+ options: [
234
+ { name: 'Detect Automatically', value: 'auto' },
235
+ { name: 'JSON', value: 'json' },
236
+ { name: 'XML', value: 'xml' },
237
+ ],
238
+ default: 'auto',
239
+ description: 'XML is read with attributes under "$" and text under "_", so a path looks like order.$.ID or order.total._',
240
+ displayOptions: show({ inputSource: ['field', 'binary'] }),
241
+ },
242
+ {
243
+ displayName: 'Define Mappings',
244
+ name: 'definitionMode',
245
+ type: 'options',
246
+ noDataExpression: true,
247
+ options: [
248
+ { name: 'Using Fields Below', value: 'fields' },
249
+ {
250
+ name: 'Auto-Map From Profile',
251
+ value: 'auto',
252
+ description: 'Match source fields to a target profile automatically by name',
253
+ },
254
+ {
255
+ name: 'Using JSON',
256
+ value: 'json',
257
+ description: 'Paste or reference a mapping definition, so one mapping can be reused across workflows',
258
+ },
259
+ ],
260
+ default: 'fields',
261
+ },
262
+ {
263
+ displayName: 'Source Profile',
264
+ name: 'sourceProfile',
265
+ type: 'string',
266
+ typeOptions: { rows: 4 },
267
+ default: '',
268
+ placeholder: 'Optional: paste the structure of the incoming data (profile component, JSON Schema, sample JSON or XML, XSD, CSV header)',
269
+ description: 'Optional. Describes the incoming data, so you can pick source fields from a dropdown (Source: From Source Profile) instead of typing paths.',
270
+ displayOptions: show({ definitionMode: ['fields'] }),
271
+ },
272
+ {
273
+ displayName: 'Source Profile Format',
274
+ name: 'sourceProfileFormat',
275
+ type: 'options',
276
+ options: [
277
+ { name: 'Detect Automatically', value: 'auto' },
278
+ { name: 'Profile Component (XML/JSON/Flat File)', value: 'component' },
279
+ { name: 'JSON Schema', value: 'jsonSchema' },
280
+ { name: 'Sample JSON', value: 'sampleJson' },
281
+ { name: 'Sample XML', value: 'sampleXml' },
282
+ { name: 'XSD', value: 'xsd' },
283
+ { name: 'CSV Header', value: 'csv' },
284
+ ],
285
+ default: 'auto',
286
+ displayOptions: show({ definitionMode: ['fields'] }),
287
+ },
288
+ {
289
+ displayName: 'Define the target structure with a profile, then map into it — pick target fields from a dropdown, or let Auto-Map match them for you',
290
+ name: 'profileNotice',
291
+ type: 'notice',
292
+ default: '',
293
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
294
+ },
295
+ {
296
+ displayName: 'Target Profile',
297
+ name: 'targetProfileSource',
298
+ type: 'options',
299
+ options: [
300
+ {
301
+ name: 'Paste It',
302
+ value: 'paste',
303
+ description: 'Paste the profile text. Lets you pick target fields from a dropdown.',
304
+ },
305
+ {
306
+ name: 'Upload a File',
307
+ value: 'binary',
308
+ description: 'Read the profile from an uploaded file (a binary field from a Read/Write Files, Form or HTTP node)',
309
+ },
310
+ ],
311
+ default: 'paste',
312
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
313
+ },
314
+ {
315
+ displayName: 'Profile',
316
+ name: 'targetProfile',
317
+ type: 'string',
318
+ typeOptions: { rows: 6 },
319
+ default: '',
320
+ placeholder: 'Paste a profile component (XML/JSON/Flat File), JSON Schema, sample JSON or XML, XSD, or a CSV header row',
321
+ description: 'The structure to map into. The target fields come from this.',
322
+ displayOptions: show({ definitionMode: ['fields', 'auto'], targetProfileSource: ['paste'] }),
323
+ },
324
+ {
325
+ displayName: 'Profile File Field',
326
+ name: 'profileBinaryField',
327
+ type: 'string',
328
+ default: 'data',
329
+ required: true,
330
+ hint: 'The name of the binary field that holds the uploaded profile file',
331
+ description: 'Add a Read/Write Files From Disk, Form Trigger (file upload) or HTTP Request node before this one to bring the profile in, then name its binary field here. Auto-Map uses it; the field dropdown is only available when you paste the profile.',
332
+ displayOptions: show({ definitionMode: ['fields', 'auto'], targetProfileSource: ['binary'] }),
333
+ },
334
+ {
335
+ displayName: 'Profile Format',
336
+ name: 'targetProfileFormat',
337
+ type: 'options',
338
+ options: [
339
+ { name: 'Detect Automatically', value: 'auto' },
340
+ { name: 'Profile Component (XML/JSON/Flat File)', value: 'component' },
341
+ { name: 'JSON Schema', value: 'jsonSchema' },
342
+ { name: 'Sample JSON', value: 'sampleJson' },
343
+ { name: 'Sample XML', value: 'sampleXml' },
344
+ { name: 'XSD', value: 'xsd' },
345
+ { name: 'CSV Header', value: 'csv' },
346
+ ],
347
+ default: 'auto',
348
+ description: 'The kind of profile you provided. Detect Automatically works for most profiles.',
349
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
350
+ },
351
+ {
352
+ displayName: 'Mappings',
353
+ name: 'mappings',
354
+ placeholder: 'Add Mapping',
355
+ type: 'fixedCollection',
356
+ typeOptions: { multipleValues: true, sortable: true },
357
+ default: {},
358
+ description: 'Applied in order; a later mapping to the same target overwrites an earlier one',
359
+ displayOptions: show({ definitionMode: ['fields'] }),
360
+ options: [{ displayName: 'Mapping', name: 'mapping', values: mappingFields }],
361
+ },
362
+ {
363
+ displayName: 'Auto-Map Remaining Target Fields',
364
+ name: 'autoMapRemaining',
365
+ type: 'boolean',
366
+ default: false,
367
+ description: 'Whether to also fill any target-profile fields you have not mapped above, by matching them to source fields by name',
368
+ displayOptions: show({ definitionMode: ['fields'] }),
369
+ },
370
+ {
371
+ displayName: 'Mapping Definition',
372
+ name: 'mappingJson',
373
+ type: 'json',
374
+ default: exampleDefinition,
375
+ description: 'An array of mappings, or { "mappings": [...] }. Each mapping has "target" and either "source" or "value", plus optional "transform", "ifMissing", "default", "separator", "decimals", "dateInputFormat", "dateOutputFormat", "lookup", "lookupFallback" and "lookupDefault".',
376
+ displayOptions: show({ definitionMode: ['json'] }),
377
+ },
378
+ {
379
+ displayName: 'Auto-Map Options',
380
+ name: 'autoOptions',
381
+ type: 'collection',
382
+ placeholder: 'Add option',
383
+ default: {},
384
+ displayOptions: show({ definitionMode: ['auto'] }),
385
+ options: [
386
+ {
387
+ displayName: 'Add Auto-Map Report',
388
+ name: 'report',
389
+ type: 'boolean',
390
+ default: false,
391
+ description: 'Whether to add an _autoMap field listing which fields matched and which did not (JSON output only)',
392
+ },
393
+ {
394
+ displayName: 'Case Sensitive',
395
+ name: 'caseSensitive',
396
+ type: 'boolean',
397
+ default: false,
398
+ description: 'Whether field names must match in the same letter case',
399
+ },
400
+ {
401
+ displayName: 'Match',
402
+ name: 'strategy',
403
+ type: 'options',
404
+ options: [
405
+ { name: 'Fuzzy (Similar Names)', value: 'fuzzy' },
406
+ { name: 'Exact Names Only', value: 'exact' },
407
+ ],
408
+ default: 'fuzzy',
409
+ },
410
+ {
411
+ displayName: 'Minimum Confidence',
412
+ name: 'threshold',
413
+ type: 'number',
414
+ typeOptions: { minValue: 0, maxValue: 1, numberPrecision: 2 },
415
+ default: 0.6,
416
+ description: 'How close names must be to match, from 0 (match anything) to 1 (identical names)',
417
+ },
418
+ {
419
+ displayName: 'Reuse a Source for Several Targets',
420
+ name: 'reuseSources',
421
+ type: 'boolean',
422
+ default: false,
423
+ description: 'Whether one source field may be mapped to more than one target field',
424
+ },
425
+ {
426
+ displayName: 'Unmatched Target Fields',
427
+ name: 'unmatched',
428
+ type: 'options',
429
+ options: [
430
+ { name: 'Leave Out', value: 'skip' },
431
+ { name: 'Set to Null', value: 'null' },
432
+ ],
433
+ default: 'skip',
434
+ },
435
+ ],
436
+ },
437
+ {
438
+ displayName: 'Unmapped Fields',
439
+ name: 'unmapped',
440
+ type: 'options',
441
+ options: [
442
+ { name: 'Drop', value: 'drop', description: 'Output only the mapped fields' },
443
+ {
444
+ name: 'Keep',
445
+ value: 'keep',
446
+ description: 'Start from a copy of the input and apply the mappings on top',
447
+ },
448
+ ],
449
+ default: 'drop',
450
+ },
451
+ {
452
+ displayName: 'Output Format',
453
+ name: 'outputFormat',
454
+ type: 'options',
455
+ noDataExpression: true,
456
+ options: [
457
+ { name: 'JSON', value: 'json', description: 'Output the mapped data as the item' },
458
+ { name: 'JSON File', value: 'jsonFile', description: 'Output the mapped data as a JSON file' },
459
+ { name: 'XML', value: 'xml', description: 'Output the mapped data as XML text in a field' },
460
+ { name: 'XML File', value: 'xmlFile', description: 'Output the mapped data as an XML file' },
461
+ ],
462
+ default: 'json',
463
+ },
464
+ {
465
+ displayName: 'Put Output in Field',
466
+ name: 'outputField',
467
+ type: 'string',
468
+ default: 'xml',
469
+ required: true,
470
+ displayOptions: show({ outputFormat: ['xml'] }),
471
+ },
472
+ {
473
+ displayName: 'Put Output File in Field',
474
+ name: 'outputBinaryField',
475
+ type: 'string',
476
+ default: 'data',
477
+ required: true,
478
+ displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
479
+ },
480
+ {
481
+ displayName: 'File Name',
482
+ name: 'fileName',
483
+ type: 'string',
484
+ default: '',
485
+ placeholder: 'output.xml',
486
+ description: 'Leave empty to name the file output with the matching extension',
487
+ displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
488
+ },
489
+ {
490
+ displayName: 'Root Element',
491
+ name: 'rootElement',
492
+ type: 'string',
493
+ default: '',
494
+ placeholder: 'order',
495
+ description: 'Name of the XML root element. Leave empty to use the single top-level field, or "root" when there are several.',
496
+ displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
497
+ },
498
+ {
499
+ displayName: 'Include XML Declaration',
500
+ name: 'xmlDeclaration',
501
+ type: 'boolean',
502
+ default: true,
503
+ description: 'Whether to start the XML with &lt;?xml version="1.0" encoding="UTF-8"?&gt;',
504
+ displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
505
+ },
506
+ ];
507
+ function forVersions(properties, versions) {
508
+ return properties.map((property) => {
509
+ var _a;
510
+ return ({
511
+ ...property,
512
+ displayOptions: {
513
+ ...property.displayOptions,
514
+ show: { ...(_a = property.displayOptions) === null || _a === void 0 ? void 0 : _a.show, '@version': versions },
515
+ },
516
+ });
517
+ });
518
+ }
157
519
  class DataMapper {
158
520
  constructor() {
159
521
  this.description = {
@@ -161,179 +523,93 @@ class DataMapper {
161
523
  name: 'dataMapper',
162
524
  icon: { light: 'file:mapper.svg', dark: 'file:mapper.dark.svg' },
163
525
  group: ['transform'],
164
- version: 1,
165
- subtitle: '={{ $parameter["definitionMode"] === "json" ? "JSON definition" : "Field mappings" }}',
166
- description: 'Map and transform data from one structure to another, between JSON and XML',
526
+ version: [1, 2],
527
+ defaultVersion: 2,
528
+ subtitle: '={{ $parameter["targetFormat"] ? "Map to " + $parameter["targetFormat"].toUpperCase() : $parameter["definitionMode"] === "auto" ? "Auto-map from profile" : $parameter["definitionMode"] === "json" ? "JSON definition" : "Field mappings" }}',
529
+ description: 'Drag and drop incoming data onto a target profile and output XML, JSON or CSV',
167
530
  defaults: {
168
531
  name: 'Data Mapper',
169
532
  },
170
533
  inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
171
534
  outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
172
535
  usableAsTool: true,
173
- properties: [
174
- {
175
- displayName: 'Input',
176
- name: 'inputSource',
177
- type: 'options',
178
- noDataExpression: true,
179
- options: [
180
- { name: 'Item JSON', value: 'json', description: 'Map the fields of each incoming item' },
181
- {
182
- name: 'Text Field',
183
- value: 'field',
184
- description: 'Parse JSON or XML text stored in a field first',
185
- },
186
- { name: 'File', value: 'binary', description: 'Parse a JSON or XML file first' },
187
- ],
188
- default: 'json',
189
- },
190
- {
191
- displayName: 'Input Field',
192
- name: 'inputField',
193
- type: 'string',
194
- default: 'data',
195
- required: true,
196
- displayOptions: show({ inputSource: ['field'] }),
197
- },
198
- {
199
- displayName: 'Input Binary Field',
200
- name: 'inputBinaryField',
201
- type: 'string',
202
- default: 'data',
203
- required: true,
204
- displayOptions: show({ inputSource: ['binary'] }),
205
- },
206
- {
207
- displayName: 'Input Format',
208
- name: 'inputFormat',
209
- type: 'options',
210
- options: [
211
- { name: 'Detect Automatically', value: 'auto' },
212
- { name: 'JSON', value: 'json' },
213
- { name: 'XML', value: 'xml' },
214
- ],
215
- default: 'auto',
216
- description: 'XML is read with attributes under "$" and text under "_", so a path looks like order.$.ID or order.total._',
217
- displayOptions: show({ inputSource: ['field', 'binary'] }),
218
- },
219
- {
220
- displayName: 'Define Mappings',
221
- name: 'definitionMode',
222
- type: 'options',
223
- noDataExpression: true,
224
- options: [
225
- { name: 'Using Fields Below', value: 'fields' },
226
- {
227
- name: 'Using JSON',
228
- value: 'json',
229
- description: 'Paste or reference a mapping definition, so one mapping can be reused across workflows',
230
- },
231
- ],
232
- default: 'fields',
233
- },
234
- {
235
- displayName: 'Mappings',
236
- name: 'mappings',
237
- placeholder: 'Add Mapping',
238
- type: 'fixedCollection',
239
- typeOptions: { multipleValues: true, sortable: true },
240
- default: {},
241
- description: 'Applied in order; a later mapping to the same target overwrites an earlier one',
242
- displayOptions: show({ definitionMode: ['fields'] }),
243
- options: [{ displayName: 'Mapping', name: 'mapping', values: mappingFields }],
244
- },
245
- {
246
- displayName: 'Mapping Definition',
247
- name: 'mappingJson',
248
- type: 'json',
249
- default: exampleDefinition,
250
- description: 'An array of mappings, or { "mappings": [...] }. Each mapping has "target" and either "source" or "value", plus optional "transform", "ifMissing", "default", "separator", "decimals", "dateInputFormat", "dateOutputFormat", "lookup", "lookupFallback" and "lookupDefault".',
251
- displayOptions: show({ definitionMode: ['json'] }),
252
- },
253
- {
254
- displayName: 'Unmapped Fields',
255
- name: 'unmapped',
256
- type: 'options',
257
- options: [
258
- { name: 'Drop', value: 'drop', description: 'Output only the mapped fields' },
259
- {
260
- name: 'Keep',
261
- value: 'keep',
262
- description: 'Start from a copy of the input and apply the mappings on top',
263
- },
264
- ],
265
- default: 'drop',
266
- },
267
- {
268
- displayName: 'Output Format',
269
- name: 'outputFormat',
270
- type: 'options',
271
- noDataExpression: true,
272
- options: [
273
- { name: 'JSON', value: 'json', description: 'Output the mapped data as the item' },
274
- { name: 'JSON File', value: 'jsonFile', description: 'Output the mapped data as a JSON file' },
275
- { name: 'XML', value: 'xml', description: 'Output the mapped data as XML text in a field' },
276
- { name: 'XML File', value: 'xmlFile', description: 'Output the mapped data as an XML file' },
277
- ],
278
- default: 'json',
279
- },
280
- {
281
- displayName: 'Put Output in Field',
282
- name: 'outputField',
283
- type: 'string',
284
- default: 'xml',
285
- required: true,
286
- displayOptions: show({ outputFormat: ['xml'] }),
287
- },
288
- {
289
- displayName: 'Put Output File in Field',
290
- name: 'outputBinaryField',
291
- type: 'string',
292
- default: 'data',
293
- required: true,
294
- displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
295
- },
296
- {
297
- displayName: 'File Name',
298
- name: 'fileName',
299
- type: 'string',
300
- default: '',
301
- placeholder: 'output.xml',
302
- description: 'Leave empty to name the file output with the matching extension',
303
- displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
304
- },
305
- {
306
- displayName: 'Root Element',
307
- name: 'rootElement',
308
- type: 'string',
309
- default: '',
310
- placeholder: 'order',
311
- description: 'Name of the XML root element. Leave empty to use the single top-level field, or "root" when there are several.',
312
- displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
536
+ properties: [...forVersions(v1Properties, [1]), ...forVersions(visual_1.visualProperties, [2])],
537
+ };
538
+ this.methods = {
539
+ resourceMapping: {
540
+ getProfileFields: visual_1.getProfileFields,
541
+ },
542
+ loadOptions: {
543
+ async getSourceFields() {
544
+ var _a, _b;
545
+ const text = ((_a = safeParam(this, 'sourceProfile')) !== null && _a !== void 0 ? _a : '').trim();
546
+ if (text === '')
547
+ return [{ name: 'Paste A Source Profile First', value: '' }];
548
+ if (text.startsWith('=') || text.includes('{{')) {
549
+ return [{ name: 'Source Profile Is Set By An Expression — Type The Source Path Instead', value: '' }];
550
+ }
551
+ const format = ((_b = safeParam(this, 'sourceProfileFormat')) !== null && _b !== void 0 ? _b : 'auto');
552
+ try {
553
+ const fields = (0, profile_1.sourceFields)((0, profile_1.parseProfile)(text, format));
554
+ if (fields.length === 0)
555
+ return [{ name: 'No Fields Found In The Source Profile', value: '' }];
556
+ return fields.map((field) => ({
557
+ name: `${field.path}${field.repeating ? ' (list)' : ''} [${field.type}]`,
558
+ value: field.path,
559
+ }));
560
+ }
561
+ catch (error) {
562
+ return [{ name: `Profile Error: ${error.message}`, value: '' }];
563
+ }
313
564
  },
314
- {
315
- displayName: 'Include XML Declaration',
316
- name: 'xmlDeclaration',
317
- type: 'boolean',
318
- default: true,
319
- description: 'Whether to start the XML with &lt;?xml version="1.0" encoding="UTF-8"?&gt;',
320
- displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
565
+ async getTargetFields() {
566
+ var _a, _b, _c;
567
+ const source = (_a = safeParam(this, 'targetProfileSource')) !== null && _a !== void 0 ? _a : 'paste';
568
+ if (source !== 'paste') {
569
+ return [
570
+ { name: 'Using An Uploaded File — Use Auto-Map, Or Paste To Pick Fields', value: '' },
571
+ ];
572
+ }
573
+ const text = ((_b = safeParam(this, 'targetProfile')) !== null && _b !== void 0 ? _b : '').trim();
574
+ if (text === '')
575
+ return [{ name: 'Paste A Profile First', value: '' }];
576
+ if (text.startsWith('=') || text.includes('{{')) {
577
+ return [{ name: 'Profile Is Set By An Expression — Use Auto-Map To Match Fields', value: '' }];
578
+ }
579
+ const format = ((_c = safeParam(this, 'targetProfileFormat')) !== null && _c !== void 0 ? _c : 'auto');
580
+ try {
581
+ const profile = (0, profile_1.parseProfile)(text, format);
582
+ if (profile.fields.length === 0)
583
+ return [{ name: 'No Fields Found In The Profile', value: '' }];
584
+ return profile.fields.map((field) => ({
585
+ name: `${field.path}${field.repeating ? ' (list)' : ''} [${field.type}]`,
586
+ value: field.path,
587
+ }));
588
+ }
589
+ catch (error) {
590
+ return [{ name: `Profile Error: ${error.message}`, value: '' }];
591
+ }
321
592
  },
322
- ],
593
+ },
323
594
  };
324
595
  }
325
596
  async execute() {
597
+ if (this.getNode().typeVersion >= 2)
598
+ return await visual_1.executeVisual.call(this);
326
599
  const items = this.getInputData();
327
600
  const returnData = [];
601
+ const profileCache = new Map();
328
602
  for (let i = 0; i < items.length; i++) {
329
603
  try {
330
604
  const input = await readInput(this, items[i], i);
331
- const rules = readRules(this, i);
605
+ const { rules, report } = await buildRules(this, i, input, profileCache);
332
606
  const unmapped = this.getNodeParameter('unmapped', i);
333
607
  const start = unmapped === 'keep' && input && typeof input === 'object' && !Array.isArray(input)
334
608
  ? structuredClone(input)
335
609
  : {};
336
610
  const mapped = (0, mapping_1.applyMappings)(input, rules, start);
611
+ if (report)
612
+ mapped._autoMap = report;
337
613
  returnData.push(await writeOutput(this, mapped, i));
338
614
  }
339
615
  catch (error) {
@@ -345,7 +621,9 @@ class DataMapper {
345
621
  ? error
346
622
  : new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
347
623
  itemIndex: i,
348
- description: error instanceof mapping_1.MappingError || error instanceof xml_1.XmlParseError
624
+ description: error instanceof mapping_1.MappingError ||
625
+ error instanceof xml_1.XmlParseError ||
626
+ error instanceof profile_1.ProfileError
349
627
  ? undefined
350
628
  : 'Unexpected error while mapping this item',
351
629
  });
@@ -398,28 +676,115 @@ async function readInput(ctx, item, itemIndex) {
398
676
  });
399
677
  }
400
678
  }
401
- function readRules(ctx, itemIndex) {
679
+ function safeParam(ctx, name) {
680
+ try {
681
+ return ctx.getCurrentNodeParameter(name);
682
+ }
683
+ catch {
684
+ return undefined;
685
+ }
686
+ }
687
+ async function getProfile(ctx, itemIndex, cache) {
688
+ var _a;
689
+ const profileSource = ctx.getNodeParameter('targetProfileSource', itemIndex, 'paste');
690
+ let text;
691
+ if (profileSource === 'binary') {
692
+ const field = ctx.getNodeParameter('profileBinaryField', itemIndex);
693
+ ctx.helpers.assertBinaryData(itemIndex, field);
694
+ text = (await ctx.helpers.getBinaryDataBuffer(itemIndex, field)).toString('utf8').trim();
695
+ }
696
+ else {
697
+ text = ((_a = ctx.getNodeParameter('targetProfile', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
698
+ }
699
+ if (text === '')
700
+ return undefined;
701
+ const format = ctx.getNodeParameter('targetProfileFormat', itemIndex, 'auto');
702
+ const key = `${format}\u0000${text}`;
703
+ if (!cache.has(key))
704
+ cache.set(key, (0, profile_1.parseProfile)(text, format));
705
+ return cache.get(key);
706
+ }
707
+ function readAutoOptions(ctx, itemIndex) {
708
+ var _a, _b, _c;
709
+ const o = ctx.getNodeParameter('autoOptions', itemIndex, {});
710
+ return {
711
+ threshold: (_a = o.threshold) !== null && _a !== void 0 ? _a : 0.6,
712
+ strategy: (_b = o.strategy) !== null && _b !== void 0 ? _b : 'fuzzy',
713
+ caseSensitive: o.caseSensitive === true,
714
+ reuseSources: o.reuseSources === true,
715
+ unmatched: (_c = o.unmatched) !== null && _c !== void 0 ? _c : 'skip',
716
+ };
717
+ }
718
+ function uiMappingToRule(m, ctx, itemIndex) {
719
+ var _a, _b, _c;
720
+ let target = m.target;
721
+ if (m.targetMode === 'profile') {
722
+ target = (_a = m.targetField) !== null && _a !== void 0 ? _a : '';
723
+ if (target === '') {
724
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'A mapping is set to "From Target Profile" but no target field is selected', { itemIndex });
725
+ }
726
+ }
727
+ let sourcePart;
728
+ if (m.sourceType === 'value')
729
+ sourcePart = { value: (_b = m.value) !== null && _b !== void 0 ? _b : '' };
730
+ else if (m.sourceType === 'profile') {
731
+ if (!m.sourceField) {
732
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'A mapping is set to "From Source Profile" but no source field is selected', { itemIndex });
733
+ }
734
+ sourcePart = { source: m.sourceField, broadcast: true };
735
+ }
736
+ else
737
+ sourcePart = { source: (_c = m.source) !== null && _c !== void 0 ? _c : '' };
738
+ return {
739
+ ...sourcePart,
740
+ target,
741
+ transform: m.transform,
742
+ separator: m.separator,
743
+ decimals: m.decimals,
744
+ dateInputFormat: m.dateInputFormat,
745
+ dateOutputFormat: m.dateOutputFormat,
746
+ lookup: m.lookupTable,
747
+ lookupFallback: m.lookupFallback,
748
+ lookupDefault: m.lookupDefault,
749
+ ifMissing: m.ifMissing,
750
+ default: m.defaultValue,
751
+ };
752
+ }
753
+ async function buildRules(ctx, itemIndex, input, cache) {
402
754
  const mode = ctx.getNodeParameter('definitionMode', itemIndex);
403
755
  if (mode === 'json')
404
- return (0, mapping_1.parseDefinition)(ctx.getNodeParameter('mappingJson', itemIndex));
405
- const entries = ctx.getNodeParameter('mappings.mapping', itemIndex, []);
406
- return entries.map((m) => {
407
- var _a, _b;
408
- return ({
409
- ...(m.sourceType === 'value' ? { value: (_a = m.value) !== null && _a !== void 0 ? _a : '' } : { source: (_b = m.source) !== null && _b !== void 0 ? _b : '' }),
410
- target: m.target,
411
- transform: m.transform,
412
- separator: m.separator,
413
- decimals: m.decimals,
414
- dateInputFormat: m.dateInputFormat,
415
- dateOutputFormat: m.dateOutputFormat,
416
- lookup: m.lookupTable,
417
- lookupFallback: m.lookupFallback,
418
- lookupDefault: m.lookupDefault,
419
- ifMissing: m.ifMissing,
420
- default: m.defaultValue,
756
+ return { rules: (0, mapping_1.parseDefinition)(ctx.getNodeParameter('mappingJson', itemIndex)) };
757
+ const profile = await getProfile(ctx, itemIndex, cache);
758
+ if (mode === 'auto') {
759
+ if (!profile) {
760
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Auto-Map needs a target profile. Paste one in, or upload a profile file.', { itemIndex });
761
+ }
762
+ const options = readAutoOptions(ctx, itemIndex);
763
+ const result = (0, profile_1.autoMap)((0, profile_1.flattenSource)(input), profile, options);
764
+ const built = { rules: result.rules, profile };
765
+ if (ctx.getNodeParameter('autoOptions.report', itemIndex, false) === true) {
766
+ built.report = {
767
+ matched: result.matched,
768
+ unmatched: result.unmatched,
769
+ };
770
+ }
771
+ return built;
772
+ }
773
+ const entries = ctx.getNodeParameter('mappings.mapping', itemIndex, []).filter(Boolean);
774
+ const rules = entries.map((m) => uiMappingToRule(m, ctx, itemIndex));
775
+ if (profile && ctx.getNodeParameter('autoMapRemaining', itemIndex, false) === true) {
776
+ const covered = new Set(rules.map((r) => r.target));
777
+ const remaining = { ...profile, fields: profile.fields.filter((f) => !covered.has(f.path)) };
778
+ const result = (0, profile_1.autoMap)((0, profile_1.flattenSource)(input), remaining, {
779
+ threshold: 0.6,
780
+ strategy: 'fuzzy',
781
+ caseSensitive: false,
782
+ reuseSources: false,
783
+ unmatched: 'skip',
421
784
  });
422
- });
785
+ rules.push(...result.rules);
786
+ }
787
+ return { rules, profile };
423
788
  }
424
789
  async function writeOutput(ctx, mapped, itemIndex) {
425
790
  const format = ctx.getNodeParameter('outputFormat', itemIndex);