n8n-nodes-data-mapper 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -6,13 +6,17 @@
6
6
 
7
7
  An [n8n](https://n8n.io) community node that **maps data from one structure to another**. Rename and nest fields, map whole arrays with `[*]`, convert types, format dates, translate codes with lookup tables, and convert between **JSON and XML**, all in one node.
8
8
 
9
+ Load a **target profile** (a profile component exported as XML/JSON/Flat File, JSON Schema, sample JSON/XML, XSD or a CSV header) — paste it or upload it — to pick target fields from a dropdown, or let **auto-map** match source fields to the target for you.
10
+
9
11
  Use it to turn an API response into your internal format, shape database rows into a request payload, read a supplier's XML into clean JSON, or pull just the useful parts out of a large payload.
10
12
 
13
+ - [Screenshots](#screenshots)
11
14
  - [Installation](#installation)
12
15
  - [Quick start](#quick-start)
13
16
  - [Paths](#paths)
14
17
  - [Parameters](#parameters)
15
18
  - [Transforms](#transforms)
19
+ - [Target profiles and auto-map](#target-profiles-and-auto-map)
16
20
  - [Reusable JSON definitions](#reusable-json-definitions)
17
21
  - [Working with XML](#working-with-xml)
18
22
  - [Examples](#examples)
@@ -20,6 +24,16 @@ Use it to turn an API response into your internal format, shape database rows in
20
24
  - [Development](#development)
21
25
  - [License](#license)
22
26
 
27
+ ## Screenshots
28
+
29
+ **Mapping fields to a JSON output** — rename and nest fields, convert types, and reshape arrays:
30
+
31
+ ![Data Mapper producing JSON output](docs/images/json-mapping.webp)
32
+
33
+ **Building XML with transforms and added fields** — attributes, nested elements, repeated items, a country lookup, rounding and a generated timestamp:
34
+
35
+ ![Data Mapper producing XML output](docs/images/xml-output.webp)
36
+
23
37
  ## Installation
24
38
 
25
39
  ### From the n8n UI (recommended)
@@ -108,8 +122,10 @@ Mappings run in order, and a later mapping to the same target overwrites an earl
108
122
  | **Input** | *Item JSON* maps each incoming item. *Text Field* first parses JSON or XML text stored in a field. *File* first parses a JSON or XML binary file. |
109
123
  | **Input Field** / **Input Binary Field** | Which field holds the text or file. |
110
124
  | **Input Format** | *Detect Automatically* (XML when the text starts with `<`), *JSON* or *XML*. |
111
- | **Define Mappings** | *Using Fields Below* or *Using JSON*. See [Reusable JSON definitions](#reusable-json-definitions). |
112
- | **Mappings** | The list of mappings. Each has a **Source** (*Input Field* path or *Fixed Value*, which can be an expression), a **Target Path**, an optional **Transform**, and **If Source Is Missing**: *Leave Out* (default), *Set to Null*, *Use Default* or *Stop With Error*. |
125
+ | **Define Mappings** | *Using Fields Below*, *Auto-Map From Profile*, or *Using JSON*. See [Target profiles and auto-map](#target-profiles-and-auto-map) and [Reusable JSON definitions](#reusable-json-definitions). |
126
+ | **Target Profile** / **Target Profile Format** | Optional. The structure to map into. See [Target profiles and auto-map](#target-profiles-and-auto-map). |
127
+ | **Mappings** | The list of mappings. Each has a **Source** (*Input Field* path or *Fixed Value*, which can be an expression), a **Target** (*Custom Path* you type, or *From Target Profile* picked from a dropdown), an optional **Transform**, and **If Source Is Missing**: *Leave Out* (default), *Set to Null*, *Use Default* or *Stop With Error*. |
128
+ | **Auto-Map Remaining Target Fields** | *Fields mode.* Fills any profile fields you did not map by hand by matching them to source fields by name. |
113
129
  | **Unmapped Fields** | *Drop* outputs only mapped fields. *Keep* starts from a copy of the input and applies the mappings on top. |
114
130
  | **Output Format** | *JSON* (the item), *JSON File*, *XML* (text in a field) or *XML File*. |
115
131
  | **Put Output in Field** | *XML only.* Default `xml`. |
@@ -137,6 +153,53 @@ Turn on **Settings → On Error → Continue** to output failed items as `{ "err
137
153
 
138
154
  For anything else, use a **Fixed Value** with an expression, e.g. `{{ $json.first + ' ' + $json.last }}`.
139
155
 
156
+ ## Target profiles and auto-map
157
+
158
+ A **target profile** describes the structure you want to map into. Paste it into the **Target Profile** field (or reference an uploaded file with an expression — see below). The node reads it and gives you two things: a **dropdown of target fields** in manual mode, and **auto-map**, which matches your source fields to the profile for you.
159
+
160
+ ### Supported formats
161
+
162
+ **Target Profile Format** is *Detect Automatically* by default. Supported:
163
+
164
+ | Format | Notes |
165
+ | --- | --- |
166
+ | Profile Component (XML/JSON/Flat File) | Integration profile components exported as XML, with `type="profile.xml"`, `"profile.json"` or `"profile.flatfile"`. Attributes become `.$.name`, repeating elements become `[*]`, and the JSON profile's `Root`/`Object` wrappers are skipped. |
167
+ | JSON Schema | `properties`, `required`, `items`, `type` and `format` (date/date-time → datetime). |
168
+ | Sample JSON | A representative JSON document; array object fields are merged into one shape. |
169
+ | Sample XML | A representative XML document; attributes become `.$.name`. |
170
+ | XSD | `xs:element` / `xs:complexType` / `xs:attribute`, with `maxOccurs` for repeating elements. |
171
+ | CSV Header | The first line, split on comma, semicolon, tab or pipe. |
172
+
173
+ ### Paste or upload
174
+
175
+ **Target Profile** has two sources:
176
+
177
+ - **Paste It** — paste the profile text into the box. This is the only source that fills the target-field dropdown.
178
+ - **Upload a File** — read the profile from an uploaded file. Put a node that produces a binary file before Data Mapper (for example **Read/Write Files From Disk**, a **Form Trigger** with a file-upload field, or an **HTTP Request**), then name that binary field. Auto-map reads the uploaded file directly; to pick fields from the dropdown, paste the profile instead.
179
+
180
+ ### Picking targets (manual mode)
181
+
182
+ With **Define Mappings** = *Using Fields Below*, paste a profile, then set a mapping's **Target** to *From Target Profile* and choose the field from the dropdown. The dropdown shows each field's path, whether it is a list, and its type. Switch **Target** to *Custom Path* to type a path the profile does not contain.
183
+
184
+ ### Auto-map
185
+
186
+ With **Define Mappings** = *Auto-Map From Profile*, the node matches each target field to the best source field by name and builds the mapping automatically. The target's type becomes the transform (a `number` field gets *To Number*, a `datetime` field gets *Format Date*, and so on).
187
+
188
+ **Auto-Map Options:**
189
+
190
+ | Option | Default | Description |
191
+ | --- | --- | --- |
192
+ | Minimum Confidence | 0.6 | How close names must be, from 0 (anything) to 1 (identical). Names are compared ignoring case and punctuation. |
193
+ | Match | Fuzzy | *Fuzzy* allows similar names; *Exact Names Only* requires the same name. |
194
+ | Case Sensitive | off | Whether names must match in the same case. |
195
+ | Reuse a Source for Several Targets | off | Whether one source may fill more than one target. |
196
+ | Unmatched Target Fields | Leave Out | *Leave Out*, or *Set to Null* to include them as null. |
197
+ | Add Auto-Map Report | off | Adds an `_autoMap` field listing matched and unmatched fields (JSON output). |
198
+
199
+ Auto-map respects arrays: `items[*].Qty` → `lines[*].qty` works, and a non-repeating source is never mapped onto a repeating target. To tweak a few fields by hand, use *Using Fields Below* with **Auto-Map Remaining Target Fields** on — your manual mappings win, and the rest are filled in automatically.
200
+
201
+ For an XML target profile, the root element name comes from the profile, so XML output needs no **Root Element**.
202
+
140
203
  ## Reusable JSON definitions
141
204
 
142
205
  Set **Define Mappings** to **Using JSON** to describe the mapping as data. You can paste it in, or load it with an expression so one mapping is shared by many workflows. For example, `{{ $('Get mapping').item.json.mapping }}` can read it from a database row, a file or a static data node.
@@ -216,6 +279,53 @@ Special characters are escaped automatically. Invalid XML input fails with the l
216
279
 
217
280
  ## Examples
218
281
 
282
+ The [`examples/`](examples) folder has importable workflows (**Workflows → Import from File**). The easiest way to build a flow, though, is to add **Data Mapper** from the node panel directly.
283
+
284
+ | File | Shows |
285
+ | --- | --- |
286
+ | [`examples/data-mapper-examples.json`](examples/data-mapper-examples.json) | JSON, XML and CSV output side by side |
287
+ | [`examples/auto-map-with-profile.json`](examples/auto-map-with-profile.json) | Auto-map and the profile-field dropdown |
288
+ | [`examples/fixed-workflow.json`](examples/fixed-workflow.json) | Field mapping to JSON (the first screenshot above) |
289
+ | [`examples/xml-output-workflow.json`](examples/xml-output-workflow.json) | Building XML with transforms and added fields (the second screenshot above) |
290
+
291
+ > **Importing a workflow and the node shows as `?`** — the node type in a workflow file depends on how the package was installed. Installed through **Settings → Community Nodes**, the type is `n8n-nodes-data-mapper.dataMapper` (what the files in `examples/` use). Installed by copying into n8n's **custom** folder, n8n registers it as `CUSTOM.dataMapper` instead. If an imported workflow shows `?`, either add the node from the panel, or edit the file and swap the type prefix to match your install. [`examples/fixed-workflow.json`](examples/fixed-workflow.json) and [`examples/xml-output-workflow.json`](examples/xml-output-workflow.json) use the `CUSTOM.` prefix.
292
+
293
+ ### Build an XML invoice with transforms
294
+
295
+ This is the second screenshot above ([`examples/xml-output-workflow.json`](examples/xml-output-workflow.json)). From an order like `{ "OrderID": "A1", "CustomerName": " Ann de Vries ", "Country": "NL", "Currency": "eur", "Amount": "129.456", "OrderDate": "2026-10-17", "Items": [...] }`, these mappings (Output Format **XML**):
296
+
297
+ | Source | Target | Transform |
298
+ | --- | --- | --- |
299
+ | `OrderID` | `invoice.$.id` | |
300
+ | *Fixed* `n8n` | `invoice.$.source` | |
301
+ | `CustomerName` | `invoice.customer.name` | Trim |
302
+ | `Country` | `invoice.customer.country` | Lookup `NL=Netherlands` |
303
+ | `Amount` | `invoice.amount._` | Round (2) |
304
+ | `Currency` | `invoice.amount.$.currency` | Uppercase |
305
+ | `OrderDate` | `invoice.issuedDate` | Format Date `dd/MM/yyyy` |
306
+ | *Fixed* `{{ $now.toISO() }}` | `invoice.generatedAt` | |
307
+ | `Items[*].SKU` | `invoice.lines.line[*].$.sku` | |
308
+ | `Items[*].Qty` | `invoice.lines.line[*].qty` | To Integer |
309
+
310
+ produce:
311
+
312
+ ```xml
313
+ <?xml version="1.0" encoding="UTF-8"?>
314
+ <invoice id="A1" source="n8n">
315
+ <customer>
316
+ <name>Ann de Vries</name>
317
+ <country>Netherlands</country>
318
+ </customer>
319
+ <amount currency="EUR">129.46</amount>
320
+ <issuedDate>17/10/2026</issuedDate>
321
+ <generatedAt>2026-10-17T09:00:00.000Z</generatedAt>
322
+ <lines>
323
+ <line sku="P-1"><qty>2</qty></line>
324
+ <line sku="P-2"><qty>1</qty></line>
325
+ </lines>
326
+ </invoice>
327
+ ```
328
+
219
329
  ### Distill a large API response
220
330
 
221
331
  Keep three fields from a big payload: **Unmapped Fields** *Drop*, with mappings `data.user.id` → `id`, `data.user.profile.email` → `email` (Lowercase) and `data.user.roles[*].name` → `roles`.
@@ -244,7 +354,7 @@ npm run lint
244
354
  npm test
245
355
  ```
246
356
 
247
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout and release process, and [CHANGELOG.md](CHANGELOG.md) for version history.
357
+ See the [wiki](docs/Home.md) for full documentation, [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout and release process, and [CHANGELOG.md](CHANGELOG.md) for version history.
248
358
 
249
359
  ## License
250
360
 
@@ -1,5 +1,10 @@
1
- import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
1
+ import type { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
2
  export declare class DataMapper implements INodeType {
3
3
  description: INodeTypeDescription;
4
+ methods: {
5
+ loadOptions: {
6
+ getTargetFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ };
8
+ };
4
9
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
10
  }
@@ -3,6 +3,7 @@ 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");
6
7
  const xml_1 = require("./xml");
7
8
  const show = (values) => ({ show: values });
8
9
  const mappingFields = [
@@ -33,14 +34,37 @@ const mappingFields = [
33
34
  description: 'Fixed text, or an expression such as {{ $now.toISO() }}',
34
35
  displayOptions: show({ sourceType: ['value'] }),
35
36
  },
37
+ {
38
+ displayName: 'Target',
39
+ name: 'targetMode',
40
+ type: 'options',
41
+ options: [
42
+ { name: 'Custom Path', value: 'custom', description: 'Type the target path yourself' },
43
+ {
44
+ name: 'From Target Profile',
45
+ value: 'profile',
46
+ description: 'Pick a field from the uploaded target profile',
47
+ },
48
+ ],
49
+ default: 'custom',
50
+ },
51
+ {
52
+ displayName: 'Target Field Name or ID',
53
+ name: 'targetField',
54
+ type: 'options',
55
+ typeOptions: { loadOptionsMethod: 'getTargetFields' },
56
+ default: '',
57
+ description: 'Field from the target profile. Fill the Target Profile field above first. Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
58
+ displayOptions: show({ targetMode: ['profile'] }),
59
+ },
36
60
  {
37
61
  displayName: 'Target Path',
38
62
  name: 'target',
39
63
  type: 'string',
40
64
  default: '',
41
- required: true,
42
65
  placeholder: 'shipping.cities[*]',
43
66
  description: 'Where to write the value in the output. Each [*] matches a [*] in the source path, in order.',
67
+ displayOptions: show({ targetMode: ['custom'] }),
44
68
  },
45
69
  {
46
70
  displayName: 'Transform',
@@ -162,7 +186,7 @@ class DataMapper {
162
186
  icon: { light: 'file:mapper.svg', dark: 'file:mapper.dark.svg' },
163
187
  group: ['transform'],
164
188
  version: 1,
165
- subtitle: '={{ $parameter["definitionMode"] === "json" ? "JSON definition" : "Field mappings" }}',
189
+ subtitle: '={{ $parameter["definitionMode"] === "auto" ? "Auto-map from profile" : $parameter["definitionMode"] === "json" ? "JSON definition" : "Field mappings" }}',
166
190
  description: 'Map and transform data from one structure to another, between JSON and XML',
167
191
  defaults: {
168
192
  name: 'Data Mapper',
@@ -223,6 +247,11 @@ class DataMapper {
223
247
  noDataExpression: true,
224
248
  options: [
225
249
  { name: 'Using Fields Below', value: 'fields' },
250
+ {
251
+ name: 'Auto-Map From Profile',
252
+ value: 'auto',
253
+ description: 'Match source fields to a target profile automatically by name',
254
+ },
226
255
  {
227
256
  name: 'Using JSON',
228
257
  value: 'json',
@@ -231,6 +260,69 @@ class DataMapper {
231
260
  ],
232
261
  default: 'fields',
233
262
  },
263
+ {
264
+ 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',
265
+ name: 'profileNotice',
266
+ type: 'notice',
267
+ default: '',
268
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
269
+ },
270
+ {
271
+ displayName: 'Target Profile',
272
+ name: 'targetProfileSource',
273
+ type: 'options',
274
+ options: [
275
+ {
276
+ name: 'Paste It',
277
+ value: 'paste',
278
+ description: 'Paste the profile text. Lets you pick target fields from a dropdown.',
279
+ },
280
+ {
281
+ name: 'Upload a File',
282
+ value: 'binary',
283
+ description: 'Read the profile from an uploaded file (a binary field from a Read/Write Files, Form or HTTP node)',
284
+ },
285
+ ],
286
+ default: 'paste',
287
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
288
+ },
289
+ {
290
+ displayName: 'Profile',
291
+ name: 'targetProfile',
292
+ type: 'string',
293
+ typeOptions: { rows: 6 },
294
+ default: '',
295
+ placeholder: 'Paste a profile component (XML/JSON/Flat File), JSON Schema, sample JSON or XML, XSD, or a CSV header row',
296
+ description: 'The structure to map into. The target fields come from this.',
297
+ displayOptions: show({ definitionMode: ['fields', 'auto'], targetProfileSource: ['paste'] }),
298
+ },
299
+ {
300
+ displayName: 'Profile File Field',
301
+ name: 'profileBinaryField',
302
+ type: 'string',
303
+ default: 'data',
304
+ required: true,
305
+ hint: 'The name of the binary field that holds the uploaded profile file',
306
+ 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.',
307
+ displayOptions: show({ definitionMode: ['fields', 'auto'], targetProfileSource: ['binary'] }),
308
+ },
309
+ {
310
+ displayName: 'Profile Format',
311
+ name: 'targetProfileFormat',
312
+ type: 'options',
313
+ options: [
314
+ { name: 'Detect Automatically', value: 'auto' },
315
+ { name: 'Profile Component (XML/JSON/Flat File)', value: 'component' },
316
+ { name: 'JSON Schema', value: 'jsonSchema' },
317
+ { name: 'Sample JSON', value: 'sampleJson' },
318
+ { name: 'Sample XML', value: 'sampleXml' },
319
+ { name: 'XSD', value: 'xsd' },
320
+ { name: 'CSV Header', value: 'csv' },
321
+ ],
322
+ default: 'auto',
323
+ description: 'The kind of profile you provided. Detect Automatically works for most profiles.',
324
+ displayOptions: show({ definitionMode: ['fields', 'auto'] }),
325
+ },
234
326
  {
235
327
  displayName: 'Mappings',
236
328
  name: 'mappings',
@@ -242,6 +334,14 @@ class DataMapper {
242
334
  displayOptions: show({ definitionMode: ['fields'] }),
243
335
  options: [{ displayName: 'Mapping', name: 'mapping', values: mappingFields }],
244
336
  },
337
+ {
338
+ displayName: 'Auto-Map Remaining Target Fields',
339
+ name: 'autoMapRemaining',
340
+ type: 'boolean',
341
+ default: false,
342
+ description: 'Whether to also fill any target-profile fields you have not mapped above, by matching them to source fields by name',
343
+ displayOptions: show({ definitionMode: ['fields'] }),
344
+ },
245
345
  {
246
346
  displayName: 'Mapping Definition',
247
347
  name: 'mappingJson',
@@ -250,6 +350,65 @@ class DataMapper {
250
350
  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
351
  displayOptions: show({ definitionMode: ['json'] }),
252
352
  },
353
+ {
354
+ displayName: 'Auto-Map Options',
355
+ name: 'autoOptions',
356
+ type: 'collection',
357
+ placeholder: 'Add option',
358
+ default: {},
359
+ displayOptions: show({ definitionMode: ['auto'] }),
360
+ options: [
361
+ {
362
+ displayName: 'Add Auto-Map Report',
363
+ name: 'report',
364
+ type: 'boolean',
365
+ default: false,
366
+ description: 'Whether to add an _autoMap field listing which fields matched and which did not (JSON output only)',
367
+ },
368
+ {
369
+ displayName: 'Case Sensitive',
370
+ name: 'caseSensitive',
371
+ type: 'boolean',
372
+ default: false,
373
+ description: 'Whether field names must match in the same letter case',
374
+ },
375
+ {
376
+ displayName: 'Match',
377
+ name: 'strategy',
378
+ type: 'options',
379
+ options: [
380
+ { name: 'Fuzzy (Similar Names)', value: 'fuzzy' },
381
+ { name: 'Exact Names Only', value: 'exact' },
382
+ ],
383
+ default: 'fuzzy',
384
+ },
385
+ {
386
+ displayName: 'Minimum Confidence',
387
+ name: 'threshold',
388
+ type: 'number',
389
+ typeOptions: { minValue: 0, maxValue: 1, numberPrecision: 2 },
390
+ default: 0.6,
391
+ description: 'How close names must be to match, from 0 (match anything) to 1 (identical names)',
392
+ },
393
+ {
394
+ displayName: 'Reuse a Source for Several Targets',
395
+ name: 'reuseSources',
396
+ type: 'boolean',
397
+ default: false,
398
+ description: 'Whether one source field may be mapped to more than one target field',
399
+ },
400
+ {
401
+ displayName: 'Unmatched Target Fields',
402
+ name: 'unmatched',
403
+ type: 'options',
404
+ options: [
405
+ { name: 'Leave Out', value: 'skip' },
406
+ { name: 'Set to Null', value: 'null' },
407
+ ],
408
+ default: 'skip',
409
+ },
410
+ ],
411
+ },
253
412
  {
254
413
  displayName: 'Unmapped Fields',
255
414
  name: 'unmapped',
@@ -321,19 +480,54 @@ class DataMapper {
321
480
  },
322
481
  ],
323
482
  };
483
+ this.methods = {
484
+ loadOptions: {
485
+ async getTargetFields() {
486
+ var _a, _b, _c;
487
+ const source = (_a = safeParam(this, 'targetProfileSource')) !== null && _a !== void 0 ? _a : 'paste';
488
+ if (source !== 'paste') {
489
+ return [
490
+ { name: 'Using An Uploaded File — Use Auto-Map, Or Paste To Pick Fields', value: '' },
491
+ ];
492
+ }
493
+ const text = ((_b = safeParam(this, 'targetProfile')) !== null && _b !== void 0 ? _b : '').trim();
494
+ if (text === '')
495
+ return [{ name: 'Paste A Profile First', value: '' }];
496
+ if (text.startsWith('=') || text.includes('{{')) {
497
+ return [{ name: 'Profile Is Set By An Expression — Use Auto-Map To Match Fields', value: '' }];
498
+ }
499
+ const format = ((_c = safeParam(this, 'targetProfileFormat')) !== null && _c !== void 0 ? _c : 'auto');
500
+ try {
501
+ const profile = (0, profile_1.parseProfile)(text, format);
502
+ if (profile.fields.length === 0)
503
+ return [{ name: 'No Fields Found In The Profile', value: '' }];
504
+ return profile.fields.map((field) => ({
505
+ name: `${field.path}${field.repeating ? ' (list)' : ''} [${field.type}]`,
506
+ value: field.path,
507
+ }));
508
+ }
509
+ catch (error) {
510
+ return [{ name: `Profile Error: ${error.message}`, value: '' }];
511
+ }
512
+ },
513
+ },
514
+ };
324
515
  }
325
516
  async execute() {
326
517
  const items = this.getInputData();
327
518
  const returnData = [];
519
+ const profileCache = new Map();
328
520
  for (let i = 0; i < items.length; i++) {
329
521
  try {
330
522
  const input = await readInput(this, items[i], i);
331
- const rules = readRules(this, i);
523
+ const { rules, report } = await buildRules(this, i, input, profileCache);
332
524
  const unmapped = this.getNodeParameter('unmapped', i);
333
525
  const start = unmapped === 'keep' && input && typeof input === 'object' && !Array.isArray(input)
334
526
  ? structuredClone(input)
335
527
  : {};
336
528
  const mapped = (0, mapping_1.applyMappings)(input, rules, start);
529
+ if (report)
530
+ mapped._autoMap = report;
337
531
  returnData.push(await writeOutput(this, mapped, i));
338
532
  }
339
533
  catch (error) {
@@ -345,7 +539,9 @@ class DataMapper {
345
539
  ? error
346
540
  : new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
347
541
  itemIndex: i,
348
- description: error instanceof mapping_1.MappingError || error instanceof xml_1.XmlParseError
542
+ description: error instanceof mapping_1.MappingError ||
543
+ error instanceof xml_1.XmlParseError ||
544
+ error instanceof profile_1.ProfileError
349
545
  ? undefined
350
546
  : 'Unexpected error while mapping this item',
351
547
  });
@@ -398,28 +594,104 @@ async function readInput(ctx, item, itemIndex) {
398
594
  });
399
595
  }
400
596
  }
401
- function readRules(ctx, itemIndex) {
597
+ function safeParam(ctx, name) {
598
+ try {
599
+ return ctx.getCurrentNodeParameter(name);
600
+ }
601
+ catch {
602
+ return undefined;
603
+ }
604
+ }
605
+ async function getProfile(ctx, itemIndex, cache) {
606
+ var _a;
607
+ const profileSource = ctx.getNodeParameter('targetProfileSource', itemIndex, 'paste');
608
+ let text;
609
+ if (profileSource === 'binary') {
610
+ const field = ctx.getNodeParameter('profileBinaryField', itemIndex);
611
+ ctx.helpers.assertBinaryData(itemIndex, field);
612
+ text = (await ctx.helpers.getBinaryDataBuffer(itemIndex, field)).toString('utf8').trim();
613
+ }
614
+ else {
615
+ text = ((_a = ctx.getNodeParameter('targetProfile', itemIndex, '')) !== null && _a !== void 0 ? _a : '').trim();
616
+ }
617
+ if (text === '')
618
+ return undefined;
619
+ const format = ctx.getNodeParameter('targetProfileFormat', itemIndex, 'auto');
620
+ const key = `${format}\u0000${text}`;
621
+ if (!cache.has(key))
622
+ cache.set(key, (0, profile_1.parseProfile)(text, format));
623
+ return cache.get(key);
624
+ }
625
+ function readAutoOptions(ctx, itemIndex) {
626
+ var _a, _b, _c;
627
+ const o = ctx.getNodeParameter('autoOptions', itemIndex, {});
628
+ return {
629
+ threshold: (_a = o.threshold) !== null && _a !== void 0 ? _a : 0.6,
630
+ strategy: (_b = o.strategy) !== null && _b !== void 0 ? _b : 'fuzzy',
631
+ caseSensitive: o.caseSensitive === true,
632
+ reuseSources: o.reuseSources === true,
633
+ unmatched: (_c = o.unmatched) !== null && _c !== void 0 ? _c : 'skip',
634
+ };
635
+ }
636
+ function uiMappingToRule(m, ctx, itemIndex) {
637
+ var _a, _b, _c;
638
+ let target = m.target;
639
+ if (m.targetMode === 'profile') {
640
+ target = (_a = m.targetField) !== null && _a !== void 0 ? _a : '';
641
+ if (target === '') {
642
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'A mapping is set to "From Target Profile" but no target field is selected', { itemIndex });
643
+ }
644
+ }
645
+ return {
646
+ ...(m.sourceType === 'value' ? { value: (_b = m.value) !== null && _b !== void 0 ? _b : '' } : { source: (_c = m.source) !== null && _c !== void 0 ? _c : '' }),
647
+ target,
648
+ transform: m.transform,
649
+ separator: m.separator,
650
+ decimals: m.decimals,
651
+ dateInputFormat: m.dateInputFormat,
652
+ dateOutputFormat: m.dateOutputFormat,
653
+ lookup: m.lookupTable,
654
+ lookupFallback: m.lookupFallback,
655
+ lookupDefault: m.lookupDefault,
656
+ ifMissing: m.ifMissing,
657
+ default: m.defaultValue,
658
+ };
659
+ }
660
+ async function buildRules(ctx, itemIndex, input, cache) {
402
661
  const mode = ctx.getNodeParameter('definitionMode', itemIndex);
403
662
  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,
663
+ return { rules: (0, mapping_1.parseDefinition)(ctx.getNodeParameter('mappingJson', itemIndex)) };
664
+ const profile = await getProfile(ctx, itemIndex, cache);
665
+ if (mode === 'auto') {
666
+ if (!profile) {
667
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Auto-Map needs a target profile. Paste one in, or upload a profile file.', { itemIndex });
668
+ }
669
+ const options = readAutoOptions(ctx, itemIndex);
670
+ const result = (0, profile_1.autoMap)((0, profile_1.flattenSource)(input), profile, options);
671
+ const built = { rules: result.rules, profile };
672
+ if (ctx.getNodeParameter('autoOptions.report', itemIndex, false) === true) {
673
+ built.report = {
674
+ matched: result.matched,
675
+ unmatched: result.unmatched,
676
+ };
677
+ }
678
+ return built;
679
+ }
680
+ const entries = ctx.getNodeParameter('mappings.mapping', itemIndex, []).filter(Boolean);
681
+ const rules = entries.map((m) => uiMappingToRule(m, ctx, itemIndex));
682
+ if (profile && ctx.getNodeParameter('autoMapRemaining', itemIndex, false) === true) {
683
+ const covered = new Set(rules.map((r) => r.target));
684
+ const remaining = { ...profile, fields: profile.fields.filter((f) => !covered.has(f.path)) };
685
+ const result = (0, profile_1.autoMap)((0, profile_1.flattenSource)(input), remaining, {
686
+ threshold: 0.6,
687
+ strategy: 'fuzzy',
688
+ caseSensitive: false,
689
+ reuseSources: false,
690
+ unmatched: 'skip',
421
691
  });
422
- });
692
+ rules.push(...result.rules);
693
+ }
694
+ return { rules, profile };
423
695
  }
424
696
  async function writeOutput(ctx, mapped, itemIndex) {
425
697
  const format = ctx.getNodeParameter('outputFormat', itemIndex);