n8n-nodes-data-mapper 0.2.0 → 0.4.1

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,755 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SOURCE_PREFIX = exports.visualProperties = void 0;
4
+ exports.encodeFieldId = encodeFieldId;
5
+ exports.decodeFieldId = decodeFieldId;
6
+ exports.fieldDisplayName = fieldDisplayName;
7
+ exports.parseDragReference = parseDragReference;
8
+ exports.getProfileFields = getProfileFields;
9
+ exports.buildDropRules = buildDropRules;
10
+ exports.orderByProfile = orderByProfile;
11
+ exports.executeVisual = executeVisual;
12
+ const n8n_workflow_1 = require("n8n-workflow");
13
+ const csv_1 = require("./csv");
14
+ const mapping_1 = require("./mapping");
15
+ const profile_1 = require("./profile");
16
+ const xml_1 = require("./xml");
17
+ exports.visualProperties = [
18
+ {
19
+ displayName: 'Paste your target profile, then fill each target field: drag a field from the INPUT panel, pick one from the Source Profile dropdown, or write an expression. Or switch to "Map Automatically" to match them by name.',
20
+ name: 'howTo',
21
+ type: 'notice',
22
+ default: '',
23
+ },
24
+ {
25
+ displayName: 'Output Format',
26
+ name: 'targetFormat',
27
+ type: 'options',
28
+ noDataExpression: true,
29
+ options: [
30
+ { name: 'JSON', value: 'json', description: 'Output the mapped data as JSON' },
31
+ { name: 'XML', value: 'xml', description: 'Output the mapped data as an XML document' },
32
+ { name: 'CSV', value: 'csv', description: 'Output the mapped data as CSV rows' },
33
+ ],
34
+ default: 'json',
35
+ },
36
+ {
37
+ displayName: 'Source Data',
38
+ name: 'sourceData',
39
+ type: 'options',
40
+ noDataExpression: true,
41
+ options: [
42
+ {
43
+ name: 'Incoming Item',
44
+ value: 'item',
45
+ description: 'Map the fields of the incoming item; you can drag them from the Input panel',
46
+ },
47
+ {
48
+ name: 'XML or JSON Text in a Field',
49
+ value: 'field',
50
+ description: 'Parse XML or JSON text held in a field first, e.g. a raw order in "data"',
51
+ },
52
+ { name: 'XML or JSON File', value: 'binary', description: 'Parse an XML or JSON file first' },
53
+ ],
54
+ default: 'item',
55
+ },
56
+ {
57
+ displayName: 'Source Field',
58
+ name: 'sourceDataField',
59
+ type: 'string',
60
+ default: 'data',
61
+ required: true,
62
+ displayOptions: { show: { sourceData: ['field'] } },
63
+ },
64
+ {
65
+ displayName: 'Source File Field',
66
+ name: 'sourceDataBinary',
67
+ type: 'string',
68
+ default: 'data',
69
+ required: true,
70
+ displayOptions: { show: { sourceData: ['binary'] } },
71
+ },
72
+ {
73
+ displayName: 'Source Profile',
74
+ name: 'sourceProfile',
75
+ type: 'string',
76
+ typeOptions: { rows: 4 },
77
+ default: '',
78
+ placeholder: 'Optional: paste the structure of the incoming data, e.g. a sample order',
79
+ description: 'Optional. Describes the incoming data. When set, every target field gets a dropdown of source fields to pick from (you can still switch a field to an expression).',
80
+ },
81
+ {
82
+ displayName: 'Target Profile',
83
+ name: 'targetProfile',
84
+ type: 'string',
85
+ typeOptions: { rows: 8 },
86
+ default: '',
87
+ required: true,
88
+ placeholder: 'Paste an XML/JSON/Flat File profile component, JSON Schema, sample JSON or XML, XSD, or a CSV header row',
89
+ description: 'The structure to map into. Its fields appear below as the targets.',
90
+ },
91
+ {
92
+ displayName: 'Target Fields',
93
+ name: 'mapping',
94
+ type: 'resourceMapper',
95
+ noDataExpression: true,
96
+ default: { mappingMode: 'defineBelow', value: null },
97
+ required: true,
98
+ typeOptions: {
99
+ loadOptionsDependsOn: ['targetProfile', 'options.profileFormat', 'sourceProfile', 'options.sourceProfileFormat'],
100
+ resourceMapper: {
101
+ resourceMapperMethod: 'getProfileFields',
102
+ mode: 'add',
103
+ valuesLabel: 'Target Fields',
104
+ fieldWords: { singular: 'target field', plural: 'target fields' },
105
+ addAllFields: true,
106
+ multiKeyMatch: false,
107
+ supportAutoMap: true,
108
+ noFieldsError: 'No target fields found. Paste a target profile above.',
109
+ },
110
+ },
111
+ },
112
+ {
113
+ displayName: 'Output To',
114
+ name: 'outputAs',
115
+ type: 'options',
116
+ noDataExpression: true,
117
+ options: [
118
+ {
119
+ name: 'Item',
120
+ value: 'item',
121
+ description: 'JSON becomes the output item; XML and CSV go into a text field',
122
+ },
123
+ { name: 'File', value: 'file', description: 'Output a binary file' },
124
+ ],
125
+ default: 'item',
126
+ },
127
+ {
128
+ displayName: 'Put Output in Field',
129
+ name: 'outputField',
130
+ type: 'string',
131
+ default: '',
132
+ placeholder: 'xml',
133
+ description: 'Field for the XML or CSV text. Leave empty to use "xml" or "csv".',
134
+ displayOptions: { show: { outputAs: ['item'], targetFormat: ['xml', 'csv'] } },
135
+ },
136
+ {
137
+ displayName: 'Put Output File in Field',
138
+ name: 'binaryField',
139
+ type: 'string',
140
+ default: 'data',
141
+ required: true,
142
+ displayOptions: { show: { outputAs: ['file'] } },
143
+ },
144
+ {
145
+ displayName: 'File Name',
146
+ name: 'fileName',
147
+ type: 'string',
148
+ default: '',
149
+ placeholder: 'output.xml',
150
+ description: 'Leave empty to name the file "output" with the matching extension',
151
+ displayOptions: { show: { outputAs: ['file'] } },
152
+ },
153
+ {
154
+ displayName: 'Options',
155
+ name: 'options',
156
+ type: 'collection',
157
+ placeholder: 'Add option',
158
+ default: {},
159
+ options: [
160
+ {
161
+ displayName: 'Add Auto-Map Report',
162
+ name: 'autoMapReport',
163
+ type: 'boolean',
164
+ default: false,
165
+ description: 'Whether to add an _autoMap field listing matched and unmatched fields (Map Automatically with JSON output)',
166
+ },
167
+ {
168
+ displayName: 'Auto-Map Minimum Confidence',
169
+ name: 'threshold',
170
+ type: 'number',
171
+ typeOptions: { minValue: 0, maxValue: 1, numberPrecision: 2 },
172
+ default: 0.6,
173
+ description: 'How close names must be for Map Automatically, from 0 (anything) to 1 (identical)',
174
+ },
175
+ {
176
+ displayName: 'Convert Types From Profile',
177
+ name: 'convertTypes',
178
+ type: 'boolean',
179
+ default: true,
180
+ description: 'Whether to turn values into numbers or booleans where the profile says so. Values that cannot be converted are kept as they are.',
181
+ },
182
+ {
183
+ displayName: 'CSV Delimiter',
184
+ name: 'csvDelimiter',
185
+ type: 'string',
186
+ default: ',',
187
+ },
188
+ {
189
+ displayName: 'CSV Header Row',
190
+ name: 'csvHeader',
191
+ type: 'boolean',
192
+ default: true,
193
+ description: 'Whether to start the CSV with a row of column names',
194
+ },
195
+ {
196
+ displayName: 'CSV Line Ending',
197
+ name: 'csvLineEnding',
198
+ type: 'options',
199
+ options: [
200
+ { name: 'LF', value: 'lf' },
201
+ { name: 'CRLF', value: 'crlf' },
202
+ ],
203
+ default: 'lf',
204
+ },
205
+ {
206
+ displayName: 'Custom Synonyms',
207
+ name: 'synonyms',
208
+ type: 'string',
209
+ typeOptions: { rows: 3 },
210
+ default: '',
211
+ placeholder: 'menge=quantity\nartikel=article',
212
+ description: 'Extra name matches for Map Automatically, one source=target per line (or a JSON object). Common abbreviations such as Qty=quantity are already built in.',
213
+ },
214
+ {
215
+ displayName: 'Escape CSV Formulas',
216
+ name: 'csvEscapeFormulas',
217
+ type: 'boolean',
218
+ default: false,
219
+ description: "Whether to put ' in front of CSV values that start with =, +, -, @, a tab or a carriage return, so spreadsheet apps don't run them as formulas (CSV injection). Negative numbers then start with ' too.",
220
+ },
221
+ {
222
+ displayName: 'Include XML Declaration',
223
+ name: 'xmlDeclaration',
224
+ type: 'boolean',
225
+ default: true,
226
+ description: 'Whether to start the XML with <?xml version="1.0" encoding="UTF-8"?>',
227
+ },
228
+ {
229
+ displayName: 'Keep Empty Values',
230
+ name: 'keepEmpty',
231
+ type: 'boolean',
232
+ default: false,
233
+ description: 'Whether to write target fields whose value is empty, instead of leaving them out',
234
+ },
235
+ {
236
+ displayName: 'One CSV for All Items',
237
+ name: 'csvCombine',
238
+ type: 'boolean',
239
+ default: true,
240
+ description: 'Whether to put every item into one CSV document, instead of one CSV per item',
241
+ },
242
+ {
243
+ displayName: 'Profile Format',
244
+ name: 'profileFormat',
245
+ type: 'options',
246
+ options: [
247
+ { name: 'Detect Automatically', value: 'auto' },
248
+ { name: 'Profile Component (XML/JSON/Flat File)', value: 'component' },
249
+ { name: 'JSON Schema', value: 'jsonSchema' },
250
+ { name: 'Sample JSON', value: 'sampleJson' },
251
+ { name: 'Sample XML', value: 'sampleXml' },
252
+ { name: 'XSD', value: 'xsd' },
253
+ { name: 'CSV Header', value: 'csv' },
254
+ ],
255
+ default: 'auto',
256
+ },
257
+ {
258
+ displayName: 'Read Profile From Binary Field',
259
+ name: 'profileBinaryField',
260
+ type: 'string',
261
+ default: '',
262
+ placeholder: 'data',
263
+ description: 'Read the target profile from an uploaded file in this binary field instead of the Target Profile box. Works with Map Automatically; to drag and drop, paste the profile.',
264
+ },
265
+ {
266
+ displayName: 'Root Element',
267
+ name: 'rootElement',
268
+ type: 'string',
269
+ default: '',
270
+ placeholder: 'order',
271
+ description: 'XML root element name. Leave empty to use the profile\'s root (sample XML profiles), the single top-level field, or "root".',
272
+ },
273
+ {
274
+ displayName: 'Source Profile Format',
275
+ name: 'sourceProfileFormat',
276
+ type: 'options',
277
+ options: [
278
+ { name: 'Detect Automatically', value: 'auto' },
279
+ { name: 'Profile Component (XML/JSON/Flat File)', value: 'component' },
280
+ { name: 'JSON Schema', value: 'jsonSchema' },
281
+ { name: 'Sample JSON', value: 'sampleJson' },
282
+ { name: 'Sample XML', value: 'sampleXml' },
283
+ { name: 'XSD', value: 'xsd' },
284
+ { name: 'CSV Header', value: 'csv' },
285
+ ],
286
+ default: 'auto',
287
+ },
288
+ ],
289
+ },
290
+ ];
291
+ const ID_ESCAPES = {
292
+ '%': '%25',
293
+ '/': '%2F',
294
+ '*': '%2A',
295
+ '#': '%23',
296
+ '.': '%2E',
297
+ '[': '%5B',
298
+ ']': '%5D',
299
+ };
300
+ const escapeSegment = (s) => s.replace(/[%/*#.[\]]/g, (c) => ID_ESCAPES[c]);
301
+ const unescapeSegment = (s) => s.replace(/%(25|2F|2A|23|2E|5B|5D)/gi, (m) => decodeURIComponent(m));
302
+ function encodeFieldId(path) {
303
+ const tokens = (0, mapping_1.parsePath)(path);
304
+ if (tokens.length === 0)
305
+ return '.';
306
+ return tokens.map((t) => (t === '*' ? '*' : typeof t === 'number' ? `#${t}` : escapeSegment(t))).join('/');
307
+ }
308
+ function formatKey(key, first) {
309
+ if (/^[^.[\]\s"']+$/.test(key))
310
+ return first ? key : `.${key}`;
311
+ return key.includes('"') ? `['${key}']` : `["${key}"]`;
312
+ }
313
+ function decodeFieldId(id) {
314
+ if (id === '.' || id === '')
315
+ return '';
316
+ let path = '';
317
+ for (const segment of id.split('/')) {
318
+ if (segment === '*')
319
+ path += '[*]';
320
+ else if (/^#\d+$/.test(segment))
321
+ path += `[${segment.slice(1)}]`;
322
+ else
323
+ path += formatKey(unescapeSegment(segment), path === '');
324
+ }
325
+ return path;
326
+ }
327
+ function fieldDisplayName(field) {
328
+ const tokens = (0, mapping_1.parsePath)(field.path);
329
+ const parts = [];
330
+ for (let k = 0; k < tokens.length; k++) {
331
+ const token = tokens[k];
332
+ if (token === '*') {
333
+ if (parts.length > 0)
334
+ parts[parts.length - 1] += '[]';
335
+ else
336
+ parts.push('[]');
337
+ continue;
338
+ }
339
+ const next = tokens[k + 1];
340
+ if (token === '$' && typeof next === 'string' && next !== '*') {
341
+ parts.push(`@${next}`);
342
+ k++;
343
+ continue;
344
+ }
345
+ parts.push(token === '_' ? '(text)' : String(token));
346
+ }
347
+ let label = parts.join(' › ') || '(whole document)';
348
+ if (field.type !== 'string')
349
+ label += ` (${field.type})`;
350
+ if (field.required)
351
+ label += ' *';
352
+ return label;
353
+ }
354
+ const DRAG_REFERENCE = /^=\s*\{\{\s*\$json((?:\??\.[A-Za-z_$][\w$]*|\??\.?\[\s*(?:\d+|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\])*)\s*\}\}\s*$/;
355
+ const DRAG_SEGMENT = /\??\.([A-Za-z_$][\w$]*)|\??\.?\[\s*(\d+|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\]/g;
356
+ function parseDragReference(raw, targetStars) {
357
+ if (typeof raw !== 'string')
358
+ return undefined;
359
+ const match = DRAG_REFERENCE.exec(raw);
360
+ if (!match)
361
+ return undefined;
362
+ const tokens = [];
363
+ for (const m of match[1].matchAll(DRAG_SEGMENT)) {
364
+ if (m[1] !== undefined)
365
+ tokens.push({ kind: 'key', key: m[1] });
366
+ else if (/^\d+$/.test(m[2]))
367
+ tokens.push({ kind: 'index', index: Number(m[2]) });
368
+ else
369
+ tokens.push({ kind: 'key', key: m[2].slice(1, -1).replace(/\\(.)/g, '$1') });
370
+ }
371
+ const indexPositions = tokens.flatMap((t, i) => (t.kind === 'index' ? [i] : []));
372
+ if (indexPositions.length < targetStars)
373
+ return undefined;
374
+ for (const position of indexPositions.slice(indexPositions.length - targetStars)) {
375
+ tokens[position] = { kind: 'star' };
376
+ }
377
+ let path = '';
378
+ for (const token of tokens) {
379
+ if (token.kind === 'key')
380
+ path += formatKey(token.key, path === '');
381
+ else if (token.kind === 'index')
382
+ path += `[${token.index}]`;
383
+ else
384
+ path += '[*]';
385
+ }
386
+ return path;
387
+ }
388
+ async function getProfileFields() {
389
+ var _a;
390
+ let text = '';
391
+ try {
392
+ text = String((_a = this.getCurrentNodeParameter('targetProfile')) !== null && _a !== void 0 ? _a : '').trim();
393
+ }
394
+ catch {
395
+ text = '';
396
+ }
397
+ if (text === '')
398
+ return { fields: [], emptyFieldsNotice: 'Paste a target profile above to load its fields.' };
399
+ if (text.startsWith('=') || text.includes('{{')) {
400
+ return {
401
+ fields: [],
402
+ emptyFieldsNotice: 'The target profile comes from an expression, so its fields cannot be listed here. Paste the profile instead, or use Map Automatically.',
403
+ };
404
+ }
405
+ let format = 'auto';
406
+ try {
407
+ format = this.getCurrentNodeParameter('options.profileFormat') || 'auto';
408
+ }
409
+ catch {
410
+ format = 'auto';
411
+ }
412
+ let profile;
413
+ try {
414
+ profile = (0, profile_1.parseProfile)(text, format);
415
+ }
416
+ catch (error) {
417
+ return { fields: [], emptyFieldsNotice: `Could not read the target profile: ${error.message}` };
418
+ }
419
+ const sourceOptions = loadSourceOptions(this);
420
+ const fields = profile.fields.map((field) => ({
421
+ id: encodeFieldId(field.path),
422
+ displayName: fieldDisplayName(field),
423
+ required: false,
424
+ defaultMatch: false,
425
+ canBeUsedToMatch: false,
426
+ display: true,
427
+ ...(sourceOptions ? { type: 'options', options: sourceOptions } : {}),
428
+ }));
429
+ return { fields };
430
+ }
431
+ exports.SOURCE_PREFIX = 'src::';
432
+ function loadSourceOptions(ctx) {
433
+ var _a;
434
+ let text = '';
435
+ let format = 'auto';
436
+ try {
437
+ text = String((_a = ctx.getCurrentNodeParameter('sourceProfile')) !== null && _a !== void 0 ? _a : '').trim();
438
+ format = ctx.getCurrentNodeParameter('options.sourceProfileFormat') || 'auto';
439
+ }
440
+ catch {
441
+ return undefined;
442
+ }
443
+ if (text === '' || text.startsWith('=') || text.includes('{{'))
444
+ return undefined;
445
+ let fields;
446
+ try {
447
+ fields = (0, profile_1.sourceFields)((0, profile_1.parseProfile)(text, format));
448
+ }
449
+ catch {
450
+ return undefined;
451
+ }
452
+ if (fields.length === 0)
453
+ return undefined;
454
+ return [
455
+ { name: '(Not Mapped)', value: '' },
456
+ ...fields.map((field) => ({ name: fieldDisplayName(field), value: `${exports.SOURCE_PREFIX}${field.path}` })),
457
+ ];
458
+ }
459
+ const typeTransform = (type) => type === 'number' ? 'number' : type === 'boolean' ? 'boolean' : 'none';
460
+ const isEmpty = (v) => v === undefined || v === null || v === '' || v === '=';
461
+ function starCount(path) {
462
+ return (0, mapping_1.parsePath)(path).filter((t) => t === '*').length;
463
+ }
464
+ function parseSourceText(ctx, text, itemIndex) {
465
+ const trimmed = text.replace(/^\uFEFF/, '').trim();
466
+ if (trimmed.startsWith('<')) {
467
+ return (0, xml_1.xmlToJson)((0, xml_1.parseXml)(trimmed), {
468
+ attributes: 'group',
469
+ attributeKey: '$',
470
+ textKey: '_',
471
+ includeRoot: true,
472
+ alwaysArray: false,
473
+ trim: true,
474
+ });
475
+ }
476
+ const parsed = tryJson(trimmed);
477
+ if (parsed.ok)
478
+ return parsed.value;
479
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'The source text is neither valid JSON nor XML', {
480
+ itemIndex,
481
+ description: parsed.error,
482
+ });
483
+ }
484
+ function tryJson(text) {
485
+ try {
486
+ return { ok: true, value: JSON.parse(text) };
487
+ }
488
+ catch (error) {
489
+ return { ok: false, error: error.message };
490
+ }
491
+ }
492
+ async function readSource(ctx, item, itemIndex, options) {
493
+ var _a, _b;
494
+ const sourceData = ctx.getNodeParameter('sourceData', itemIndex, 'item');
495
+ const binaryField = (sourceData === 'binary'
496
+ ? ctx.getNodeParameter('sourceDataBinary', itemIndex, 'data')
497
+ : String((_a = options.sourceBinaryField) !== null && _a !== void 0 ? _a : '')).trim();
498
+ if (binaryField) {
499
+ ctx.helpers.assertBinaryData(itemIndex, binaryField);
500
+ const text = (await ctx.helpers.getBinaryDataBuffer(itemIndex, binaryField)).toString('utf8');
501
+ return parseSourceText(ctx, text, itemIndex);
502
+ }
503
+ const field = (sourceData === 'field'
504
+ ? ctx.getNodeParameter('sourceDataField', itemIndex, 'data')
505
+ : String((_b = options.sourceField) !== null && _b !== void 0 ? _b : '')).trim();
506
+ if (field) {
507
+ const value = item.json[field];
508
+ if (value === undefined) {
509
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Source field "${field}" does not exist`, { itemIndex });
510
+ }
511
+ return typeof value === 'object' ? value : parseSourceText(ctx, String(value), itemIndex);
512
+ }
513
+ return item.json;
514
+ }
515
+ async function readProfile(ctx, itemIndex, options, cache) {
516
+ var _a, _b;
517
+ const binaryField = String((_a = options.profileBinaryField) !== null && _a !== void 0 ? _a : '').trim();
518
+ let text;
519
+ if (binaryField) {
520
+ ctx.helpers.assertBinaryData(itemIndex, binaryField);
521
+ text = (await ctx.helpers.getBinaryDataBuffer(itemIndex, binaryField)).toString('utf8').trim();
522
+ }
523
+ else {
524
+ text = String((_b = ctx.getNodeParameter('targetProfile', itemIndex, '')) !== null && _b !== void 0 ? _b : '').trim();
525
+ }
526
+ if (text === '')
527
+ return undefined;
528
+ const format = options.profileFormat || 'auto';
529
+ const key = `${format}\u0000${text}`;
530
+ if (!cache.has(key))
531
+ cache.set(key, (0, profile_1.parseProfile)(text, format));
532
+ return cache.get(key);
533
+ }
534
+ function buildDropRules(raw, resolved, profile, options) {
535
+ var _a, _b;
536
+ const fieldsById = new Map(((_a = profile === null || profile === void 0 ? void 0 : profile.fields) !== null && _a !== void 0 ? _a : []).map((f) => [encodeFieldId(f.path), f]));
537
+ const sourceRules = [];
538
+ const spreadRules = [];
539
+ const broadcastRules = [];
540
+ const valueRules = [];
541
+ const ids = [...new Set([...Object.keys(raw), ...Object.keys(resolved)])];
542
+ for (const id of ids) {
543
+ const target = decodeFieldId(id);
544
+ const rawValue = raw[id];
545
+ const value = resolved[id];
546
+ if (isEmpty(rawValue) && isEmpty(value)) {
547
+ if (options.keepEmpty)
548
+ valueRules.push({ value: '', target });
549
+ continue;
550
+ }
551
+ const transform = options.convertTypes ? typeTransform((_b = fieldsById.get(id)) === null || _b === void 0 ? void 0 : _b.type) : 'none';
552
+ const stars = starCount(target);
553
+ const picked = typeof rawValue === 'string' && rawValue.startsWith(exports.SOURCE_PREFIX) ? rawValue.slice(exports.SOURCE_PREFIX.length) : undefined;
554
+ const source = picked === undefined ? parseDragReference(rawValue, stars) : undefined;
555
+ const loose = picked !== null && picked !== void 0 ? picked : (source === undefined && stars > 0 ? parseDragReference(rawValue, 0) : undefined);
556
+ if (source !== undefined) {
557
+ sourceRules.push({ source, target, transform, lenient: true });
558
+ }
559
+ else if (loose !== undefined) {
560
+ (starCount(loose) >= stars ? sourceRules : broadcastRules).push({
561
+ source: loose,
562
+ target,
563
+ transform,
564
+ lenient: true,
565
+ broadcast: true,
566
+ });
567
+ }
568
+ else if (stars > 0 && Array.isArray(value)) {
569
+ spreadRules.push({ value, target, transform, lenient: true, spread: true });
570
+ }
571
+ else {
572
+ valueRules.push({ value: value !== null && value !== void 0 ? value : '', target, transform, lenient: true });
573
+ }
574
+ }
575
+ return [...sourceRules, ...spreadRules, ...broadcastRules, ...valueRules];
576
+ }
577
+ function buildRules(ctx, itemIndex, source, profile, options) {
578
+ var _a, _b;
579
+ const mapping = ctx.getNodeParameter('mapping', itemIndex, undefined, { skipValidation: true });
580
+ const convertTypes = options.convertTypes !== false;
581
+ if ((mapping === null || mapping === void 0 ? void 0 : mapping.mappingMode) === 'autoMapInputData') {
582
+ if (!profile) {
583
+ throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Map Automatically needs a target profile. Paste one into the Target Profile box.', { itemIndex });
584
+ }
585
+ const result = (0, profile_1.autoMap)((0, profile_1.flattenSource)(source), profile, {
586
+ threshold: typeof options.threshold === 'number' ? options.threshold : 0.6,
587
+ strategy: 'fuzzy',
588
+ caseSensitive: false,
589
+ reuseSources: false,
590
+ unmatched: options.keepEmpty ? 'null' : 'skip',
591
+ synonyms: (0, profile_1.parseSynonyms)(options.synonyms),
592
+ });
593
+ const rules = result.rules.map((rule) => ({
594
+ ...rule,
595
+ transform: convertTypes && rule.transform !== 'date' ? rule.transform : 'none',
596
+ lenient: true,
597
+ }));
598
+ const built = { rules };
599
+ if (options.autoMapReport === true) {
600
+ built.report = { matched: result.matched, unmatched: result.unmatched };
601
+ }
602
+ return built;
603
+ }
604
+ const raw = ((_a = ctx.getNodeParameter('mapping.value', itemIndex, {}, { rawExpressions: true, skipValidation: true })) !== null && _a !== void 0 ? _a : {});
605
+ const resolved = ((_b = mapping === null || mapping === void 0 ? void 0 : mapping.value) !== null && _b !== void 0 ? _b : {});
606
+ return {
607
+ rules: buildDropRules(raw, resolved, profile, { convertTypes, keepEmpty: options.keepEmpty === true }),
608
+ };
609
+ }
610
+ function profileOrder(profile) {
611
+ const root = { children: new Map() };
612
+ for (const field of profile.fields) {
613
+ let node = root;
614
+ for (const token of (0, mapping_1.parsePath)(field.path)) {
615
+ if (!node.children.has(token))
616
+ node.children.set(token, { children: new Map() });
617
+ node = node.children.get(token);
618
+ }
619
+ }
620
+ return root;
621
+ }
622
+ function reorder(value, node) {
623
+ var _a;
624
+ if (Array.isArray(value)) {
625
+ const element = node === null || node === void 0 ? void 0 : node.children.get('*');
626
+ return value.map((entry) => reorder(entry, element));
627
+ }
628
+ if (value === null || typeof value !== 'object' || value instanceof Date)
629
+ return value;
630
+ const input = value;
631
+ const output = {};
632
+ for (const key of (_a = node === null || node === void 0 ? void 0 : node.children.keys()) !== null && _a !== void 0 ? _a : []) {
633
+ if (typeof key === 'string' && key !== '*' && Object.prototype.hasOwnProperty.call(input, key)) {
634
+ (0, mapping_1.setOwn)(output, key, reorder(input[key], node.children.get(key)));
635
+ }
636
+ }
637
+ for (const key of Object.keys(input)) {
638
+ if (!Object.prototype.hasOwnProperty.call(output, key))
639
+ (0, mapping_1.setOwn)(output, key, reorder(input[key], node === null || node === void 0 ? void 0 : node.children.get(key)));
640
+ }
641
+ return output;
642
+ }
643
+ function orderByProfile(mapped, profile) {
644
+ if (!profile || profile.fields.length === 0)
645
+ return mapped;
646
+ return reorder(mapped, profileOrder(profile));
647
+ }
648
+ function csvColumns(profile, records) {
649
+ if (profile && profile.fields.length > 0) {
650
+ return profile.fields.map((field) => ({
651
+ path: field.path,
652
+ header: profile.kind === 'csv' ? field.name : field.path.replace(/\[\*\]/g, '').replace(/\$\./g, '@'),
653
+ }));
654
+ }
655
+ const seen = new Set();
656
+ const columns = [];
657
+ for (const record of records) {
658
+ for (const { path } of (0, profile_1.flattenSource)(record)) {
659
+ if (seen.has(path))
660
+ continue;
661
+ seen.add(path);
662
+ columns.push({ path, header: path.replace(/\[\*\]/g, '') });
663
+ }
664
+ }
665
+ return columns;
666
+ }
667
+ function csvText(records, profile, options) {
668
+ var _a;
669
+ return (0, csv_1.toCsv)(records, csvColumns(profile, records), {
670
+ delimiter: String((_a = options.csvDelimiter) !== null && _a !== void 0 ? _a : ',') || ',',
671
+ header: options.csvHeader !== false,
672
+ lineEnding: options.csvLineEnding === 'crlf' ? '\r\n' : '\n',
673
+ escapeFormulas: options.csvEscapeFormulas === true,
674
+ });
675
+ }
676
+ const MIME = { json: 'application/json', xml: 'application/xml', csv: 'text/csv' };
677
+ async function textOutput(ctx, format, text, itemIndex) {
678
+ const outputAs = ctx.getNodeParameter('outputAs', itemIndex, 'item');
679
+ if (outputAs === 'file') {
680
+ const field = ctx.getNodeParameter('binaryField', itemIndex, 'data');
681
+ const fileName = ctx.getNodeParameter('fileName', itemIndex, '') || `output.${format}`;
682
+ const binary = await ctx.helpers.prepareBinaryData(Buffer.from(text, 'utf8'), fileName, MIME[format]);
683
+ return { json: {}, binary: { [field]: binary } };
684
+ }
685
+ const field = ctx.getNodeParameter('outputField', itemIndex, '') || format;
686
+ return { json: { [field]: text } };
687
+ }
688
+ async function writeOne(ctx, format, mapped, itemIndex, profile, options, report) {
689
+ var _a;
690
+ if (format === 'json') {
691
+ const outputAs = ctx.getNodeParameter('outputAs', itemIndex, 'item');
692
+ if (outputAs === 'item')
693
+ return { json: (report ? { ...mapped, _autoMap: report } : mapped) };
694
+ return textOutput(ctx, 'json', JSON.stringify(mapped, null, 2), itemIndex);
695
+ }
696
+ if (format === 'xml') {
697
+ const xml = (0, xml_1.jsonToXml)(mapped, {
698
+ rootName: String((_a = options.rootElement) !== null && _a !== void 0 ? _a : '').trim() || ((profile === null || profile === void 0 ? void 0 : profile.kind) === 'sampleXml' ? profile.rootName : undefined),
699
+ declaration: options.xmlDeclaration !== false,
700
+ attributeKey: '$',
701
+ textKey: '_',
702
+ });
703
+ return textOutput(ctx, 'xml', xml, itemIndex);
704
+ }
705
+ return textOutput(ctx, 'csv', csvText([mapped], profile, options), itemIndex);
706
+ }
707
+ function toNodeError(ctx, error, itemIndex) {
708
+ if (error instanceof n8n_workflow_1.NodeOperationError)
709
+ return error;
710
+ const known = error instanceof mapping_1.MappingError || error instanceof xml_1.XmlParseError || error instanceof profile_1.ProfileError;
711
+ return new n8n_workflow_1.NodeOperationError(ctx.getNode(), error.message, {
712
+ itemIndex,
713
+ description: known ? undefined : 'Unexpected error while mapping this item',
714
+ });
715
+ }
716
+ async function executeVisual() {
717
+ const items = this.getInputData();
718
+ const format = this.getNodeParameter('targetFormat', 0, 'json');
719
+ const firstOptions = this.getNodeParameter('options', 0, {});
720
+ const combineCsv = format === 'csv' && firstOptions.csvCombine !== false;
721
+ const cache = new Map();
722
+ const returnData = [];
723
+ const csvRecords = [];
724
+ let csvProfile;
725
+ for (let i = 0; i < items.length; i++) {
726
+ try {
727
+ const options = this.getNodeParameter('options', i, {});
728
+ const source = await readSource(this, items[i], i, options);
729
+ const profile = await readProfile(this, i, options, cache);
730
+ const { rules, report } = buildRules(this, i, source, profile, options);
731
+ const mapped = orderByProfile((0, mapping_1.applyMappings)(source, rules, {}), profile);
732
+ if (combineCsv) {
733
+ csvRecords.push({ mapped, index: i });
734
+ csvProfile !== null && csvProfile !== void 0 ? csvProfile : (csvProfile = profile);
735
+ continue;
736
+ }
737
+ const output = await writeOne(this, format, mapped, i, profile, options, report);
738
+ returnData.push({ ...output, pairedItem: { item: i } });
739
+ }
740
+ catch (error) {
741
+ if (this.continueOnFail()) {
742
+ returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
743
+ continue;
744
+ }
745
+ throw toNodeError(this, error, i);
746
+ }
747
+ }
748
+ if (combineCsv && csvRecords.length > 0) {
749
+ const text = csvText(csvRecords.map((r) => r.mapped), csvProfile, firstOptions);
750
+ const output = await textOutput(this, 'csv', text, 0);
751
+ returnData.push({ ...output, pairedItem: csvRecords.map((r) => ({ item: r.index })) });
752
+ }
753
+ return [returnData];
754
+ }
755
+ //# sourceMappingURL=visual.js.map