n8n-nodes-mautic-advanced 0.2.5 → 0.3.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.
@@ -1272,6 +1272,38 @@ exports.contactFields = [
1272
1272
  },
1273
1273
  default: '',
1274
1274
  },
1275
+ {
1276
+ displayName: 'Options',
1277
+ name: 'options',
1278
+ type: 'collection',
1279
+ placeholder: 'Add Option',
1280
+ default: {},
1281
+ displayOptions: {
1282
+ show: {
1283
+ resource: ['contact'],
1284
+ operation: ['get'],
1285
+ },
1286
+ },
1287
+ options: [
1288
+ {
1289
+ displayName: 'Fields to Return',
1290
+ name: 'fieldsToReturn',
1291
+ type: 'multiOptions',
1292
+ typeOptions: {
1293
+ loadOptionsMethod: 'getContactFields',
1294
+ },
1295
+ default: [],
1296
+ description: 'Select which fields to include in the output. Leave empty to return all fields.',
1297
+ },
1298
+ {
1299
+ displayName: 'RAW Data',
1300
+ name: 'rawData',
1301
+ type: 'boolean',
1302
+ default: true,
1303
+ description: 'By default only the data of the fields get returned. If this option is set, the RAW response with all data gets returned.',
1304
+ },
1305
+ ],
1306
+ },
1275
1307
  /* -------------------------------------------------------------------------- */
1276
1308
  /* contact:getAll */
1277
1309
  /* -------------------------------------------------------------------------- */
@@ -1305,79 +1337,29 @@ exports.contactFields = [
1305
1337
  default: 30,
1306
1338
  description: 'Max number of results to return. If you request more than 30 records, the node will automatically use pagination to fetch up to the requested number.',
1307
1339
  },
1308
- /* -------------------------------------------------------------------------- */
1309
- /* contact:delete */
1310
- /* -------------------------------------------------------------------------- */
1311
- {
1312
- displayName: 'Contact ID',
1313
- name: 'contactId',
1314
- type: 'string',
1315
- displayOptions: {
1316
- show: {
1317
- operation: ['delete'],
1318
- resource: ['contact'],
1319
- },
1320
- },
1321
- default: '',
1322
- },
1323
- /* -------------------------------------------------------------------------- */
1324
- /* contact:deleteBatch */
1325
- /* -------------------------------------------------------------------------- */
1326
- {
1327
- displayName: 'Contact IDs',
1328
- name: 'contactIds',
1329
- type: 'string',
1330
- required: false,
1331
- displayOptions: {
1332
- show: {
1333
- resource: ['contact'],
1334
- operation: ['deleteBatch'],
1335
- },
1336
- },
1337
- default: '',
1338
- placeholder: '1,2,3,4,5',
1339
- description: 'Comma-separated list of contact IDs to delete (e.g., "1,2,3,4,5"). If left empty, all input items\' contactId fields will be used for batch deletion.',
1340
- },
1341
1340
  {
1342
1341
  displayName: 'Options',
1343
1342
  name: 'options',
1344
1343
  type: 'collection',
1344
+ placeholder: 'Add Option',
1345
+ default: {},
1345
1346
  displayOptions: {
1346
1347
  show: {
1347
1348
  resource: ['contact'],
1348
- operation: ['deleteBatch'],
1349
+ operation: ['getAll'],
1349
1350
  },
1350
1351
  },
1351
- placeholder: 'Add option',
1352
- default: {},
1353
1352
  options: [
1354
1353
  {
1355
- displayName: 'RAW Data',
1356
- name: 'rawData',
1357
- type: 'boolean',
1358
- default: true,
1359
- description: 'Whether to return the raw response data from the API or just a success summary',
1360
- },
1361
- ],
1362
- },
1363
- /* -------------------------------------------------------------------------- */
1364
- /* contact:all */
1365
- /* -------------------------------------------------------------------------- */
1366
- {
1367
- displayName: 'Options',
1368
- name: 'options',
1369
- type: 'collection',
1370
- displayOptions: {
1371
- show: {
1372
- resource: ['contact'],
1373
- },
1374
- hide: {
1375
- operation: ['sendEmail', 'editDoNotContactList', 'editContactPoint', 'deleteBatch'],
1354
+ displayName: 'Fields to Return',
1355
+ name: 'fieldsToReturn',
1356
+ type: 'multiOptions',
1357
+ typeOptions: {
1358
+ loadOptionsMethod: 'getContactFields',
1359
+ },
1360
+ default: [],
1361
+ description: 'Select which fields to include in the output. Leave empty to return all fields.',
1376
1362
  },
1377
- },
1378
- placeholder: 'Add option',
1379
- default: {},
1380
- options: [
1381
1363
  {
1382
1364
  displayName: 'Search',
1383
1365
  name: 'search',
@@ -1453,13 +1435,160 @@ exports.contactFields = [
1453
1435
  default: false,
1454
1436
  description: 'Whether to return array of entities without additional lists in it',
1455
1437
  },
1438
+ {
1439
+ displayName: 'Where',
1440
+ name: 'where',
1441
+ type: 'fixedCollection',
1442
+ placeholder: 'Add Condition',
1443
+ typeOptions: {
1444
+ multipleValues: true,
1445
+ },
1446
+ default: {},
1447
+ options: [
1448
+ {
1449
+ name: 'conditions',
1450
+ displayName: 'Condition',
1451
+ values: [
1452
+ {
1453
+ displayName: 'Column',
1454
+ name: 'col',
1455
+ type: 'options',
1456
+ typeOptions: {
1457
+ loadOptionsMethod: 'getContactFields',
1458
+ },
1459
+ default: '',
1460
+ description: 'Database column (snake_case, e.g. date_modified, or custom field)',
1461
+ },
1462
+ {
1463
+ displayName: 'Expression',
1464
+ name: 'expr',
1465
+ type: 'options',
1466
+ options: [
1467
+ { name: 'Equals', value: 'eq' },
1468
+ { name: 'Not Equals', value: 'neq' },
1469
+ { name: 'Less Than', value: 'lt' },
1470
+ { name: 'Less Than or Equal', value: 'lte' },
1471
+ { name: 'Greater Than', value: 'gt' },
1472
+ { name: 'Greater Than or Equal', value: 'gte' },
1473
+ { name: 'Between', value: 'between' },
1474
+ { name: 'In', value: 'in' },
1475
+ { name: 'Is Null', value: 'isNull' },
1476
+ { name: 'Is Not Null', value: 'isNotNull' },
1477
+ { name: 'AND (andX)', value: 'andX' },
1478
+ { name: 'OR (orX)', value: 'orX' },
1479
+ ],
1480
+ default: 'eq',
1481
+ description: 'Comparison expression',
1482
+ },
1483
+ {
1484
+ displayName: 'Value',
1485
+ name: 'val',
1486
+ type: 'string',
1487
+ default: '',
1488
+ description: 'Value for the condition. For andX/orX, leave blank and use nested conditions below.',
1489
+ displayOptions: {
1490
+ hide: {
1491
+ expr: ['andX', 'orX'],
1492
+ },
1493
+ },
1494
+ },
1495
+ {
1496
+ displayName: 'Nested Conditions',
1497
+ name: 'nested',
1498
+ type: 'fixedCollection',
1499
+ typeOptions: {
1500
+ multipleValues: true,
1501
+ },
1502
+ default: {},
1503
+ options: [
1504
+ {
1505
+ name: 'conditions',
1506
+ displayName: 'Condition',
1507
+ values: [
1508
+ {
1509
+ displayName: 'Column',
1510
+ name: 'col',
1511
+ type: 'options',
1512
+ typeOptions: {
1513
+ loadOptionsMethod: 'getContactFields',
1514
+ },
1515
+ default: '',
1516
+ description: 'Database column (snake_case, e.g. date_modified, or custom field)',
1517
+ },
1518
+ {
1519
+ displayName: 'Expression',
1520
+ name: 'expr',
1521
+ type: 'options',
1522
+ options: [
1523
+ { name: 'Equals', value: 'eq' },
1524
+ { name: 'Not Equals', value: 'neq' },
1525
+ { name: 'Less Than', value: 'lt' },
1526
+ { name: 'Less Than or Equal', value: 'lte' },
1527
+ { name: 'Greater Than', value: 'gt' },
1528
+ { name: 'Greater Than or Equal', value: 'gte' },
1529
+ { name: 'Between', value: 'between' },
1530
+ { name: 'In', value: 'in' },
1531
+ { name: 'Is Null', value: 'isNull' },
1532
+ { name: 'Is Not Null', value: 'isNotNull' },
1533
+ { name: 'AND (andX)', value: 'andX' },
1534
+ { name: 'OR (orX)', value: 'orX' },
1535
+ ],
1536
+ default: 'eq',
1537
+ description: 'Comparison expression',
1538
+ },
1539
+ {
1540
+ displayName: 'Value',
1541
+ name: 'val',
1542
+ type: 'string',
1543
+ default: '',
1544
+ description: 'Value for the condition. For andX/orX, leave blank and use nested conditions below.',
1545
+ displayOptions: {
1546
+ hide: {
1547
+ expr: ['andX', 'orX'],
1548
+ },
1549
+ },
1550
+ },
1551
+ // Nested again for further levels if needed (recursive pattern)
1552
+ ],
1553
+ },
1554
+ ],
1555
+ displayOptions: {
1556
+ show: {
1557
+ expr: ['andX', 'orX'],
1558
+ },
1559
+ },
1560
+ },
1561
+ ],
1562
+ },
1563
+ ],
1564
+ },
1565
+ {
1566
+ displayName: 'Email Do Not Contact Only',
1567
+ name: 'emailDncOnly',
1568
+ type: 'boolean',
1569
+ default: false,
1570
+ description: 'Return only contacts with Email Do Not Contact enabled',
1571
+ },
1572
+ {
1573
+ displayName: 'SMS Do Not Contact Only',
1574
+ name: 'smsDncOnly',
1575
+ type: 'boolean',
1576
+ default: false,
1577
+ description: 'Return only contacts with SMS Do Not Contact enabled',
1578
+ },
1579
+ {
1580
+ displayName: 'Any Do Not Contact Only',
1581
+ name: 'anyDncOnly',
1582
+ type: 'boolean',
1583
+ default: false,
1584
+ description: 'Return only contacts with any Do Not Contact enabled',
1585
+ },
1456
1586
  {
1457
1587
  displayName: 'RAW Data',
1458
1588
  name: 'rawData',
1459
1589
  type: 'boolean',
1460
1590
  default: true,
1461
- // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
1462
- description: 'By default only the data of the fields get returned. If this options gets set the RAW response with all data gets returned.',
1591
+ description: 'By default only the data of the fields get returned. If this option gets set the RAW response with all data gets returned.',
1463
1592
  },
1464
1593
  ],
1465
1594
  },
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateJSON = exports.mauticApiRequestAllItems = exports.mauticApiRequest = void 0;
3
+ exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.mauticApiRequest = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  async function mauticApiRequest(method, endpoint, body = {}, query, uri) {
6
6
  const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials');
@@ -86,6 +86,43 @@ async function mauticApiRequestAllItems(propertyName, method, endpoint, body = {
86
86
  return returnData;
87
87
  }
88
88
  exports.mauticApiRequestAllItems = mauticApiRequestAllItems;
89
+ /**
90
+ * Serialise the n8n fixedCollection 'where' structure into Mautic API query parameters.
91
+ * Handles nested andX/orX logic recursively.
92
+ * @param whereArray Array of conditions from the fixedCollection
93
+ * @param prefix Used internally for recursion (should be omitted by callers)
94
+ * @returns Object with keys/values for qs
95
+ */
96
+ function serialiseMauticWhere(whereArray, prefix = 'where') {
97
+ const params = {};
98
+ whereArray.forEach((condition, idx) => {
99
+ const base = `${prefix}[${idx}]`;
100
+ if (condition.expr === 'andX' || condition.expr === 'orX') {
101
+ params[`${base}[expr]`] = condition.expr;
102
+ // Nested conditions: recurse
103
+ if (condition.nested && Array.isArray(condition.nested.conditions)) {
104
+ // The value for 'val' is an array of nested conditions
105
+ const nestedParams = serialiseMauticWhere(condition.nested.conditions, `${base}[val]`);
106
+ Object.assign(params, nestedParams);
107
+ }
108
+ else {
109
+ // Defensive: empty group
110
+ params[`${base}[val]`] = [];
111
+ }
112
+ }
113
+ else {
114
+ // Simple condition
115
+ if (condition.col)
116
+ params[`${base}[col]`] = condition.col;
117
+ if (condition.expr)
118
+ params[`${base}[expr]`] = condition.expr;
119
+ if (condition.val !== undefined && condition.val !== '')
120
+ params[`${base}[val]`] = condition.val;
121
+ }
122
+ });
123
+ return params;
124
+ }
125
+ exports.serialiseMauticWhere = serialiseMauticWhere;
89
126
  function validateJSON(json) {
90
127
  let result;
91
128
  try {
@@ -221,6 +221,15 @@ class MauticAdvanced {
221
221
  // select them easily
222
222
  async getContactFields() {
223
223
  const returnData = [];
224
+ // Add key system fields manually (except last_active, which is already present)
225
+ const systemFields = [
226
+ { name: 'Date Added', value: 'date_added' },
227
+ { name: 'Date Modified', value: 'date_modified' },
228
+ { name: 'ID', value: 'id' },
229
+ { name: 'Owner ID', value: 'owner_id' },
230
+ ];
231
+ returnData.push(...systemFields);
232
+ // Fetch custom and other fields from Mautic
224
233
  const fields = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/contact');
225
234
  for (const field of fields) {
226
235
  returnData.push({
@@ -435,7 +444,7 @@ class MauticAdvanced {
435
444
  ...additionalFields,
436
445
  };
437
446
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'POST', '/segments/new', body);
438
- responseData = responseData.list; // Changed from .segment to .list
447
+ responseData = responseData.list;
439
448
  }
440
449
  if (operation === 'update') {
441
450
  const segmentId = this.getNodeParameter('segmentId', i);
@@ -448,32 +457,31 @@ class MauticAdvanced {
448
457
  };
449
458
  const method = createIfNotFound ? 'PUT' : 'PATCH';
450
459
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, method, `/segments/${segmentId}/edit`, body);
451
- responseData = responseData.list; // Changed from .segment to .list
460
+ responseData = responseData.list;
452
461
  }
453
462
  if (operation === 'get') {
454
463
  const segmentId = this.getNodeParameter('segmentId', i);
455
464
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'GET', `/segments/${segmentId}`);
456
- responseData = responseData.list; // Changed from .segment to .list
465
+ responseData = responseData.list;
457
466
  }
458
467
  if (operation === 'getAll') {
459
468
  const returnAll = this.getNodeParameter('returnAll', i);
460
469
  const options = this.getNodeParameter('options', i);
461
470
  qs = { ...options };
462
471
  if (returnAll) {
463
- responseData = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', // Using 'lists' property name
464
- 'GET', '/segments', {}, qs);
472
+ responseData = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', 'GET', '/segments', {}, qs);
465
473
  }
466
474
  else {
467
475
  const limit = this.getNodeParameter('limit', i);
468
476
  qs.limit = limit;
469
477
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'GET', '/segments', {}, qs);
470
- responseData = responseData.lists ? Object.values(responseData.lists) : []; // Convert object to array
478
+ responseData = responseData.lists ? Object.values(responseData.lists) : [];
471
479
  }
472
480
  }
473
481
  if (operation === 'delete') {
474
482
  const segmentId = this.getNodeParameter('segmentId', i);
475
483
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'DELETE', `/segments/${segmentId}/delete`);
476
- responseData = responseData.list; // Changed from .segment to .list
484
+ responseData = responseData.list;
477
485
  }
478
486
  if (operation === 'addContact') {
479
487
  const segmentId = this.getNodeParameter('segmentId', i);
@@ -934,6 +942,18 @@ class MauticAdvanced {
934
942
  responseData = [responseData.contact];
935
943
  if (options.rawData === false) {
936
944
  responseData = responseData.map((item) => item.fields.all);
945
+ // Filter fields if fieldsToReturn is set
946
+ if (Array.isArray(options.fieldsToReturn) && options.fieldsToReturn.length > 0) {
947
+ responseData = responseData.map((item) => {
948
+ const filtered = {};
949
+ for (const field of options.fieldsToReturn) {
950
+ if (Object.prototype.hasOwnProperty.call(item, field)) {
951
+ filtered[field] = item[field];
952
+ }
953
+ }
954
+ return filtered;
955
+ });
956
+ }
937
957
  }
938
958
  }
939
959
  //https://developer.mautic.org/?php#list-contacts
@@ -951,13 +971,90 @@ class MauticAdvanced {
951
971
  if (qs.orderBy) {
952
972
  qs.orderBy = (0, change_case_1.snakeCase)(qs.orderBy);
953
973
  }
974
+ // Advanced where support
975
+ const whereObj = options.where;
976
+ let filteredWhere = [];
977
+ if (whereObj && Array.isArray(whereObj.conditions)) {
978
+ // Only keep non-empty conditions in the API query
979
+ filteredWhere = whereObj.conditions;
980
+ if (filteredWhere.length > 0) {
981
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
982
+ const { serialiseMauticWhere } = require('./GenericFunctions');
983
+ const whereParams = serialiseMauticWhere(filteredWhere);
984
+ Object.assign(qs, whereParams);
985
+ }
986
+ // Remove the original 'where' to avoid sending as JSON
987
+ delete qs.where;
988
+ }
954
989
  let limit = undefined;
955
990
  if (!returnAll) {
956
991
  limit = this.getNodeParameter('limit', i);
957
992
  }
958
- responseData = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs, limit);
993
+ // In contact:getAll, after fetching allContacts, apply post-filtering if any DNC boolean option is true
994
+ const emailDncOnly = options.emailDncOnly === true;
995
+ const smsDncOnly = options.smsDncOnly === true;
996
+ const anyDncOnly = options.anyDncOnly === true;
997
+ const useDncPostFilter = emailDncOnly || smsDncOnly || anyDncOnly;
998
+ if (useDncPostFilter) {
999
+ let filteredContacts = [];
1000
+ let page = 0;
1001
+ const pageSize = 100;
1002
+ const moreData = true;
1003
+ while (moreData) {
1004
+ const pagedQs = { ...qs, limit: pageSize, start: page * pageSize };
1005
+ const pageData = await GenericFunctions_1.mauticApiRequest.call(this, 'GET', '/contacts', {}, pagedQs);
1006
+ let contacts = [];
1007
+ if (pageData.contacts) {
1008
+ contacts = Object.values(pageData.contacts);
1009
+ }
1010
+ if (!contacts.length)
1011
+ break;
1012
+ // Filter DNC contacts from this page and add to filteredContacts
1013
+ const dncMatches = contacts.filter((contact) => {
1014
+ if (!contact.doNotContact || !Array.isArray(contact.doNotContact))
1015
+ return false;
1016
+ if (anyDncOnly) {
1017
+ return contact.doNotContact.length > 0;
1018
+ }
1019
+ else if (emailDncOnly) {
1020
+ return contact.doNotContact.some((dnc) => dnc.channel === 'email');
1021
+ }
1022
+ else if (smsDncOnly) {
1023
+ return contact.doNotContact.some((dnc) => dnc.channel === 'sms');
1024
+ }
1025
+ return false;
1026
+ });
1027
+ filteredContacts = filteredContacts.concat(dncMatches);
1028
+ // Stop if we have enough DNC matches
1029
+ if (!returnAll && filteredContacts.length >= (limit || 0))
1030
+ break;
1031
+ if (contacts.length < pageSize)
1032
+ break;
1033
+ page++;
1034
+ }
1035
+ if (!returnAll && limit) {
1036
+ filteredContacts = filteredContacts.slice(0, limit);
1037
+ }
1038
+ responseData = filteredContacts;
1039
+ }
1040
+ else {
1041
+ // No DNC filter, fetch as usual
1042
+ responseData = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'contacts', 'GET', '/contacts', {}, qs, limit);
1043
+ }
959
1044
  if (options.rawData === false) {
960
1045
  responseData = responseData.map((item) => item.fields.all);
1046
+ // Filter fields if fieldsToReturn is set
1047
+ if (Array.isArray(options.fieldsToReturn) && options.fieldsToReturn.length > 0) {
1048
+ responseData = responseData.map((item) => {
1049
+ const filtered = {};
1050
+ for (const field of options.fieldsToReturn) {
1051
+ if (Object.prototype.hasOwnProperty.call(item, field)) {
1052
+ filtered[field] = item[field];
1053
+ }
1054
+ }
1055
+ return filtered;
1056
+ });
1057
+ }
961
1058
  }
962
1059
  }
963
1060
  //https://developer.mautic.org/?php#delete-contact
@@ -1139,7 +1236,7 @@ class MauticAdvanced {
1139
1236
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'POST', `/companies/${companyId}/contact/${contactId}/add`, {});
1140
1237
  // responseData = responseData.company;
1141
1238
  // if (simple === true) {
1142
- // responseData = responseData.fields.all;
1239
+ // responseData = responseData.fields.all;
1143
1240
  // }
1144
1241
  }
1145
1242
  //https://developer.mautic.org/#remove-contact-from-a-company
@@ -1149,7 +1246,7 @@ class MauticAdvanced {
1149
1246
  responseData = await GenericFunctions_1.mauticApiRequest.call(this, 'POST', `/companies/${companyId}/contact/${contactId}/remove`, {});
1150
1247
  // responseData = responseData.company;
1151
1248
  // if (simple === true) {
1152
- // responseData = responseData.fields.all;
1249
+ // responseData = responseData.fields.all;
1153
1250
  // }
1154
1251
  }
1155
1252
  }
@@ -8,7 +8,7 @@ class MauticAdvancedTrigger {
8
8
  this.description = {
9
9
  displayName: 'Mautic Advanced Trigger',
10
10
  name: 'mauticAdvancedTrigger',
11
- icon: 'file:MauticAdvanced.svg',
11
+ icon: 'file:mauticadvanced.svg',
12
12
  group: ['trigger'],
13
13
  version: 1,
14
14
  description: 'Handle Mautic events via webhooks',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-mautic-advanced",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Enhanced n8n node for Mautic with comprehensive API coverage including tags, campaigns, categories, and advanced contact management",
5
5
  "keywords": [
6
6
  "n8n",