n8n-nodes-data-mapper 0.0.0-stage → 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.
@@ -0,0 +1,722 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DataMapper = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const mapping_1 = require("./mapping");
6
+ const profile_1 = require("./profile");
7
+ const xml_1 = require("./xml");
8
+ const show = (values) => ({ show: values });
9
+ const mappingFields = [
10
+ {
11
+ displayName: 'Source',
12
+ name: 'sourceType',
13
+ type: 'options',
14
+ options: [
15
+ { name: 'Input Field', value: 'field', description: 'Read a value from the input by path' },
16
+ { name: 'Fixed Value', value: 'value', description: 'Write a fixed value or the result of an expression' },
17
+ ],
18
+ default: 'field',
19
+ },
20
+ {
21
+ displayName: 'Source Path',
22
+ name: 'source',
23
+ type: 'string',
24
+ default: '',
25
+ placeholder: 'customer.addresses[*].city',
26
+ description: 'Dot path into the input. Use [0] for one array element, [*] for every element, or leave empty for the whole input.',
27
+ displayOptions: show({ sourceType: ['field'] }),
28
+ },
29
+ {
30
+ displayName: 'Value',
31
+ name: 'value',
32
+ type: 'string',
33
+ default: '',
34
+ description: 'Fixed text, or an expression such as {{ $now.toISO() }}',
35
+ displayOptions: show({ sourceType: ['value'] }),
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
+ },
60
+ {
61
+ displayName: 'Target Path',
62
+ name: 'target',
63
+ type: 'string',
64
+ default: '',
65
+ placeholder: 'shipping.cities[*]',
66
+ description: 'Where to write the value in the output. Each [*] matches a [*] in the source path, in order.',
67
+ displayOptions: show({ targetMode: ['custom'] }),
68
+ },
69
+ {
70
+ displayName: 'Transform',
71
+ name: 'transform',
72
+ type: 'options',
73
+ options: [
74
+ { name: 'None', value: 'none' },
75
+ { name: 'To String', value: 'string' },
76
+ { name: 'To Number', value: 'number' },
77
+ { name: 'To Integer', value: 'integer' },
78
+ { name: 'Round', value: 'round', description: 'Round a number to a number of decimal places' },
79
+ { name: 'To Boolean', value: 'boolean', description: 'Accepts true/false, yes/no, 1/0, on/off' },
80
+ { name: 'Trim', value: 'trim' },
81
+ { name: 'Uppercase', value: 'uppercase' },
82
+ { name: 'Lowercase', value: 'lowercase' },
83
+ { name: 'Format Date', value: 'date', description: 'Read a date and write it in another format (UTC)' },
84
+ { name: 'Split Into List', value: 'split', description: 'Turn "a,b,c" into ["a", "b", "c"]' },
85
+ { name: 'Join List', value: 'join', description: 'Turn ["a", "b", "c"] into "a,b,c"' },
86
+ { name: 'Lookup Table', value: 'lookup', description: 'Replace codes using a key=value table' },
87
+ ],
88
+ default: 'none',
89
+ },
90
+ {
91
+ displayName: 'Separator',
92
+ name: 'separator',
93
+ type: 'string',
94
+ default: ',',
95
+ displayOptions: show({ transform: ['split', 'join'] }),
96
+ },
97
+ {
98
+ displayName: 'Decimal Places',
99
+ name: 'decimals',
100
+ type: 'number',
101
+ typeOptions: { minValue: 0, maxValue: 15 },
102
+ default: 2,
103
+ displayOptions: show({ transform: ['round'] }),
104
+ },
105
+ {
106
+ displayName: 'Input Date Format',
107
+ name: 'dateInputFormat',
108
+ type: 'string',
109
+ default: '',
110
+ placeholder: 'yyyyMMdd',
111
+ description: 'Leave empty for ISO 8601 or Unix milliseconds. Tokens: yyyy, MM, dd, HH, mm, ss, SSS.',
112
+ displayOptions: show({ transform: ['date'] }),
113
+ },
114
+ {
115
+ displayName: 'Output Date Format',
116
+ name: 'dateOutputFormat',
117
+ type: 'string',
118
+ default: '',
119
+ placeholder: 'dd/MM/yyyy',
120
+ description: 'Leave empty for ISO 8601. Tokens: yyyy, MM, dd, HH, mm, ss, SSS.',
121
+ displayOptions: show({ transform: ['date'] }),
122
+ },
123
+ {
124
+ displayName: 'Lookup Table',
125
+ name: 'lookupTable',
126
+ type: 'string',
127
+ typeOptions: { rows: 4 },
128
+ default: '',
129
+ placeholder: 'NL=Netherlands\nDE=Germany',
130
+ description: 'One key=value per line, or a JSON object',
131
+ displayOptions: show({ transform: ['lookup'] }),
132
+ },
133
+ {
134
+ displayName: 'If No Match',
135
+ name: 'lookupFallback',
136
+ type: 'options',
137
+ options: [
138
+ { name: 'Keep Original Value', value: 'keep' },
139
+ { name: 'Use Default', value: 'default' },
140
+ { name: 'Set to Null', value: 'null' },
141
+ { name: 'Stop With Error', value: 'error' },
142
+ ],
143
+ default: 'keep',
144
+ displayOptions: show({ transform: ['lookup'] }),
145
+ },
146
+ {
147
+ displayName: 'Lookup Default',
148
+ name: 'lookupDefault',
149
+ type: 'string',
150
+ default: '',
151
+ displayOptions: show({ transform: ['lookup'], lookupFallback: ['default'] }),
152
+ },
153
+ {
154
+ displayName: 'If Source Is Missing',
155
+ name: 'ifMissing',
156
+ type: 'options',
157
+ options: [
158
+ { name: 'Leave Out', value: 'skip' },
159
+ { name: 'Set to Null', value: 'null' },
160
+ { name: 'Use Default', value: 'default' },
161
+ { name: 'Stop With Error', value: 'error' },
162
+ ],
163
+ default: 'skip',
164
+ displayOptions: show({ sourceType: ['field'] }),
165
+ },
166
+ {
167
+ displayName: 'Default Value',
168
+ name: 'defaultValue',
169
+ type: 'string',
170
+ default: '',
171
+ displayOptions: show({ sourceType: ['field'], ifMissing: ['default'] }),
172
+ },
173
+ ];
174
+ const exampleDefinition = JSON.stringify([
175
+ { source: 'id', target: 'orderId' },
176
+ { source: 'customer.name', target: 'buyer.name', transform: 'trim' },
177
+ { source: 'items[*].sku', target: 'lines[*].productCode' },
178
+ { source: 'items[*].qty', target: 'lines[*].quantity', transform: 'integer' },
179
+ { value: 'EUR', target: 'lines[*].currency' },
180
+ ], null, 2);
181
+ class DataMapper {
182
+ constructor() {
183
+ this.description = {
184
+ displayName: 'Data Mapper',
185
+ name: 'dataMapper',
186
+ icon: { light: 'file:mapper.svg', dark: 'file:mapper.dark.svg' },
187
+ group: ['transform'],
188
+ version: 1,
189
+ subtitle: '={{ $parameter["definitionMode"] === "auto" ? "Auto-map from profile" : $parameter["definitionMode"] === "json" ? "JSON definition" : "Field mappings" }}',
190
+ description: 'Map and transform data from one structure to another, between JSON and XML',
191
+ defaults: {
192
+ name: 'Data Mapper',
193
+ },
194
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
195
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
196
+ usableAsTool: true,
197
+ properties: [
198
+ {
199
+ displayName: 'Input',
200
+ name: 'inputSource',
201
+ type: 'options',
202
+ noDataExpression: true,
203
+ options: [
204
+ { name: 'Item JSON', value: 'json', description: 'Map the fields of each incoming item' },
205
+ {
206
+ name: 'Text Field',
207
+ value: 'field',
208
+ description: 'Parse JSON or XML text stored in a field first',
209
+ },
210
+ { name: 'File', value: 'binary', description: 'Parse a JSON or XML file first' },
211
+ ],
212
+ default: 'json',
213
+ },
214
+ {
215
+ displayName: 'Input Field',
216
+ name: 'inputField',
217
+ type: 'string',
218
+ default: 'data',
219
+ required: true,
220
+ displayOptions: show({ inputSource: ['field'] }),
221
+ },
222
+ {
223
+ displayName: 'Input Binary Field',
224
+ name: 'inputBinaryField',
225
+ type: 'string',
226
+ default: 'data',
227
+ required: true,
228
+ displayOptions: show({ inputSource: ['binary'] }),
229
+ },
230
+ {
231
+ displayName: 'Input Format',
232
+ name: 'inputFormat',
233
+ type: 'options',
234
+ options: [
235
+ { name: 'Detect Automatically', value: 'auto' },
236
+ { name: 'JSON', value: 'json' },
237
+ { name: 'XML', value: 'xml' },
238
+ ],
239
+ default: 'auto',
240
+ description: 'XML is read with attributes under "$" and text under "_", so a path looks like order.$.ID or order.total._',
241
+ displayOptions: show({ inputSource: ['field', 'binary'] }),
242
+ },
243
+ {
244
+ displayName: 'Define Mappings',
245
+ name: 'definitionMode',
246
+ type: 'options',
247
+ noDataExpression: true,
248
+ options: [
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
+ },
255
+ {
256
+ name: 'Using JSON',
257
+ value: 'json',
258
+ description: 'Paste or reference a mapping definition, so one mapping can be reused across workflows',
259
+ },
260
+ ],
261
+ default: 'fields',
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
+ },
326
+ {
327
+ displayName: 'Mappings',
328
+ name: 'mappings',
329
+ placeholder: 'Add Mapping',
330
+ type: 'fixedCollection',
331
+ typeOptions: { multipleValues: true, sortable: true },
332
+ default: {},
333
+ description: 'Applied in order; a later mapping to the same target overwrites an earlier one',
334
+ displayOptions: show({ definitionMode: ['fields'] }),
335
+ options: [{ displayName: 'Mapping', name: 'mapping', values: mappingFields }],
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
+ },
345
+ {
346
+ displayName: 'Mapping Definition',
347
+ name: 'mappingJson',
348
+ type: 'json',
349
+ default: exampleDefinition,
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".',
351
+ displayOptions: show({ definitionMode: ['json'] }),
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
+ },
412
+ {
413
+ displayName: 'Unmapped Fields',
414
+ name: 'unmapped',
415
+ type: 'options',
416
+ options: [
417
+ { name: 'Drop', value: 'drop', description: 'Output only the mapped fields' },
418
+ {
419
+ name: 'Keep',
420
+ value: 'keep',
421
+ description: 'Start from a copy of the input and apply the mappings on top',
422
+ },
423
+ ],
424
+ default: 'drop',
425
+ },
426
+ {
427
+ displayName: 'Output Format',
428
+ name: 'outputFormat',
429
+ type: 'options',
430
+ noDataExpression: true,
431
+ options: [
432
+ { name: 'JSON', value: 'json', description: 'Output the mapped data as the item' },
433
+ { name: 'JSON File', value: 'jsonFile', description: 'Output the mapped data as a JSON file' },
434
+ { name: 'XML', value: 'xml', description: 'Output the mapped data as XML text in a field' },
435
+ { name: 'XML File', value: 'xmlFile', description: 'Output the mapped data as an XML file' },
436
+ ],
437
+ default: 'json',
438
+ },
439
+ {
440
+ displayName: 'Put Output in Field',
441
+ name: 'outputField',
442
+ type: 'string',
443
+ default: 'xml',
444
+ required: true,
445
+ displayOptions: show({ outputFormat: ['xml'] }),
446
+ },
447
+ {
448
+ displayName: 'Put Output File in Field',
449
+ name: 'outputBinaryField',
450
+ type: 'string',
451
+ default: 'data',
452
+ required: true,
453
+ displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
454
+ },
455
+ {
456
+ displayName: 'File Name',
457
+ name: 'fileName',
458
+ type: 'string',
459
+ default: '',
460
+ placeholder: 'output.xml',
461
+ description: 'Leave empty to name the file output with the matching extension',
462
+ displayOptions: show({ outputFormat: ['xmlFile', 'jsonFile'] }),
463
+ },
464
+ {
465
+ displayName: 'Root Element',
466
+ name: 'rootElement',
467
+ type: 'string',
468
+ default: '',
469
+ placeholder: 'order',
470
+ description: 'Name of the XML root element. Leave empty to use the single top-level field, or "root" when there are several.',
471
+ displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
472
+ },
473
+ {
474
+ displayName: 'Include XML Declaration',
475
+ name: 'xmlDeclaration',
476
+ type: 'boolean',
477
+ default: true,
478
+ description: 'Whether to start the XML with &lt;?xml version="1.0" encoding="UTF-8"?&gt;',
479
+ displayOptions: show({ outputFormat: ['xml', 'xmlFile'] }),
480
+ },
481
+ ],
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
+ };
515
+ }
516
+ async execute() {
517
+ const items = this.getInputData();
518
+ const returnData = [];
519
+ const profileCache = new Map();
520
+ for (let i = 0; i < items.length; i++) {
521
+ try {
522
+ const input = await readInput(this, items[i], i);
523
+ const { rules, report } = await buildRules(this, i, input, profileCache);
524
+ const unmapped = this.getNodeParameter('unmapped', i);
525
+ const start = unmapped === 'keep' && input && typeof input === 'object' && !Array.isArray(input)
526
+ ? structuredClone(input)
527
+ : {};
528
+ const mapped = (0, mapping_1.applyMappings)(input, rules, start);
529
+ if (report)
530
+ mapped._autoMap = report;
531
+ returnData.push(await writeOutput(this, mapped, i));
532
+ }
533
+ catch (error) {
534
+ if (this.continueOnFail()) {
535
+ returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
536
+ continue;
537
+ }
538
+ throw error instanceof n8n_workflow_1.NodeOperationError
539
+ ? error
540
+ : new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
541
+ itemIndex: i,
542
+ description: error instanceof mapping_1.MappingError ||
543
+ error instanceof xml_1.XmlParseError ||
544
+ error instanceof profile_1.ProfileError
545
+ ? undefined
546
+ : 'Unexpected error while mapping this item',
547
+ });
548
+ }
549
+ }
550
+ return [returnData];
551
+ }
552
+ }
553
+ exports.DataMapper = DataMapper;
554
+ async function readInput(ctx, item, itemIndex) {
555
+ const source = ctx.getNodeParameter('inputSource', itemIndex);
556
+ if (source === 'json')
557
+ return item.json;
558
+ let text;
559
+ if (source === 'field') {
560
+ const field = ctx.getNodeParameter('inputField', itemIndex);
561
+ const value = item.json[field];
562
+ if (value === undefined) {
563
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Input field "${field}" does not exist`, { itemIndex });
564
+ }
565
+ if (typeof value === 'object')
566
+ return value;
567
+ text = String(value);
568
+ }
569
+ else {
570
+ const field = ctx.getNodeParameter('inputBinaryField', itemIndex);
571
+ ctx.helpers.assertBinaryData(itemIndex, field);
572
+ text = (await ctx.helpers.getBinaryDataBuffer(itemIndex, field)).toString('utf8');
573
+ }
574
+ const format = ctx.getNodeParameter('inputFormat', itemIndex, 'auto');
575
+ const trimmed = text.replace(/^\uFEFF/, '').trim();
576
+ const isXml = format === 'xml' || (format === 'auto' && trimmed.startsWith('<'));
577
+ if (isXml) {
578
+ return (0, xml_1.xmlToJson)((0, xml_1.parseXml)(trimmed), {
579
+ attributes: 'group',
580
+ attributeKey: '$',
581
+ textKey: '_',
582
+ includeRoot: true,
583
+ alwaysArray: false,
584
+ trim: true,
585
+ });
586
+ }
587
+ try {
588
+ return JSON.parse(trimmed);
589
+ }
590
+ catch (error) {
591
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'The input is not valid JSON', {
592
+ itemIndex,
593
+ description: error.message,
594
+ });
595
+ }
596
+ }
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) {
661
+ const mode = ctx.getNodeParameter('definitionMode', itemIndex);
662
+ if (mode === 'json')
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',
691
+ });
692
+ rules.push(...result.rules);
693
+ }
694
+ return { rules, profile };
695
+ }
696
+ async function writeOutput(ctx, mapped, itemIndex) {
697
+ const format = ctx.getNodeParameter('outputFormat', itemIndex);
698
+ const pairedItem = { item: itemIndex };
699
+ if (format === 'json')
700
+ return { json: mapped, pairedItem };
701
+ if (format === 'jsonFile') {
702
+ const field = ctx.getNodeParameter('outputBinaryField', itemIndex);
703
+ const fileName = ctx.getNodeParameter('fileName', itemIndex, '') || 'output.json';
704
+ const binary = await ctx.helpers.prepareBinaryData(Buffer.from(JSON.stringify(mapped, null, 2), 'utf8'), fileName, 'application/json');
705
+ return { json: {}, binary: { [field]: binary }, pairedItem };
706
+ }
707
+ const xml = (0, xml_1.jsonToXml)(mapped, {
708
+ rootName: ctx.getNodeParameter('rootElement', itemIndex, '') || undefined,
709
+ declaration: ctx.getNodeParameter('xmlDeclaration', itemIndex, true),
710
+ attributeKey: '$',
711
+ textKey: '_',
712
+ });
713
+ if (format === 'xml') {
714
+ const field = ctx.getNodeParameter('outputField', itemIndex);
715
+ return { json: { [field]: xml }, pairedItem };
716
+ }
717
+ const field = ctx.getNodeParameter('outputBinaryField', itemIndex);
718
+ const fileName = ctx.getNodeParameter('fileName', itemIndex, '') || 'output.xml';
719
+ const binary = await ctx.helpers.prepareBinaryData(Buffer.from(xml, 'utf8'), fileName, 'application/xml');
720
+ return { json: {}, binary: { [field]: binary }, pairedItem };
721
+ }
722
+ //# sourceMappingURL=DataMapper.node.js.map