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 +113 -3
- package/dist/nodes/DataMapper/DataMapper.node.d.ts +6 -1
- package/dist/nodes/DataMapper/DataMapper.node.js +295 -23
- package/dist/nodes/DataMapper/DataMapper.node.js.map +1 -1
- package/dist/nodes/DataMapper/profile.d.ts +41 -0
- package/dist/nodes/DataMapper/profile.js +527 -0
- package/dist/nodes/DataMapper/profile.js.map +1 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
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
|
+

|
|
32
|
+
|
|
33
|
+
**Building XML with transforms and added fields** — attributes, nested elements, repeated items, a country lookup, rounding and a generated timestamp:
|
|
34
|
+
|
|
35
|
+

|
|
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
|
-
| **
|
|
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 =
|
|
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 ||
|
|
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
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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);
|