n8n-nodes-mautic-advanced 1.1.0 → 1.2.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.
@@ -226,6 +226,16 @@ exports.companyFields = [
226
226
  type: 'number',
227
227
  default: 0,
228
228
  },
229
+ {
230
+ displayName: 'Owner Name or ID',
231
+ name: 'owner',
232
+ type: 'options',
233
+ description: 'User to assign as company owner. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
234
+ typeOptions: {
235
+ loadOptionsMethod: 'getOwners',
236
+ },
237
+ default: '',
238
+ },
229
239
  {
230
240
  displayName: 'Overwrite With Blank',
231
241
  name: 'overwriteWithBlank',
@@ -431,6 +441,16 @@ exports.companyFields = [
431
441
  type: 'number',
432
442
  default: 0,
433
443
  },
444
+ {
445
+ displayName: 'Owner Name or ID',
446
+ name: 'owner',
447
+ type: 'options',
448
+ description: 'User to assign as company owner. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
449
+ typeOptions: {
450
+ loadOptionsMethod: 'getOwners',
451
+ },
452
+ default: '',
453
+ },
434
454
  {
435
455
  displayName: 'Overwrite With Blank',
436
456
  name: 'overwriteWithBlank',
@@ -446,11 +446,14 @@ exports.contactFields = [
446
446
  default: '',
447
447
  },
448
448
  {
449
- displayName: 'Owner ID',
450
- name: 'ownerId',
451
- type: 'string',
449
+ displayName: 'Owner Name or ID',
450
+ name: 'owner',
451
+ type: 'options',
452
+ description: 'User to assign as contact owner. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
453
+ typeOptions: {
454
+ loadOptionsMethod: 'getOwners',
455
+ },
452
456
  default: '',
453
- description: 'ID of a Mautic user to assign this contact to',
454
457
  },
455
458
  {
456
459
  displayName: 'Phone',
@@ -1002,16 +1005,19 @@ exports.contactFields = [
1002
1005
  default: '',
1003
1006
  },
1004
1007
  {
1005
- displayName: 'Owner ID',
1006
- name: 'ownerId',
1007
- type: 'string',
1008
+ displayName: 'Owner Name or ID',
1009
+ name: 'owner',
1010
+ type: 'options',
1011
+ description: 'User to assign as contact owner. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
1012
+ typeOptions: {
1013
+ loadOptionsMethod: 'getOwners',
1014
+ },
1008
1015
  displayOptions: {
1009
1016
  show: {
1010
1017
  '/jsonParameters': [false],
1011
1018
  },
1012
1019
  },
1013
1020
  default: '',
1014
- description: 'ID of a Mautic user to assign this contact to',
1015
1021
  },
1016
1022
  {
1017
1023
  displayName: 'Phone',
@@ -1392,7 +1398,7 @@ exports.contactFields = [
1392
1398
  name: 'fieldsToReturn',
1393
1399
  type: 'multiOptions',
1394
1400
  typeOptions: {
1395
- loadOptionsMethod: 'getContactFields',
1401
+ loadOptionsMethod: 'getContactFieldsToReturn',
1396
1402
  },
1397
1403
  default: [],
1398
1404
  description: 'Select which fields to include in the output. Leave empty to return all fields.',
@@ -1487,7 +1493,7 @@ exports.contactFields = [
1487
1493
  name: 'fieldsToReturn',
1488
1494
  type: 'multiOptions',
1489
1495
  typeOptions: {
1490
- loadOptionsMethod: 'getContactFields',
1496
+ loadOptionsMethod: 'getContactFieldsToReturn',
1491
1497
  },
1492
1498
  default: [],
1493
1499
  description: 'Select which fields to include in the output. Leave empty to return all fields.',
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.mauticApiRequest = void 0;
3
+ exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const authenticatedRequest_1 = require("./utils/authenticatedRequest");
6
6
  async function mauticApiRequest(method, endpoint, body = {}, query, uri, headers) {
@@ -40,40 +40,62 @@ exports.mauticApiRequest = mauticApiRequest;
40
40
  * Make an API request to paginated mautic endpoint
41
41
  * and return all results
42
42
  */
43
- // Optional: Slightly improved error handling
43
+ exports.DEFAULT_MAUTIC_PAGE_SIZE = 100;
44
+ function toPositiveInteger(value) {
45
+ const numericValue = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : undefined;
46
+ if (numericValue === undefined || !Number.isFinite(numericValue) || numericValue <= 0) {
47
+ return undefined;
48
+ }
49
+ return Math.floor(numericValue);
50
+ }
51
+ function getPaginationStart(value) {
52
+ const start = toPositiveInteger(value);
53
+ return start ?? 0;
54
+ }
55
+ function getResponseItems(responseData, propertyName) {
56
+ const responseProperty = responseData[propertyName];
57
+ if (!responseProperty || typeof responseProperty !== 'object') {
58
+ return [];
59
+ }
60
+ if (Array.isArray(responseProperty)) {
61
+ return responseProperty;
62
+ }
63
+ return Object.values(responseProperty);
64
+ }
44
65
  async function mauticApiRequestAllItems(propertyName, method, endpoint, body = {}, query = {}, maxResults) {
45
66
  const returnData = [];
46
- let responseData;
47
- query.limit = 30;
48
- query.start = 0;
49
- while (true) {
67
+ const baseQuery = { ...query };
68
+ const requestedStart = getPaginationStart(baseQuery.start);
69
+ const totalLimit = toPositiveInteger(maxResults);
70
+ delete baseQuery.start;
71
+ delete baseQuery.limit;
72
+ let currentStart = requestedStart;
73
+ while (totalLimit === undefined || returnData.length < totalLimit) {
74
+ const remaining = totalLimit === undefined ? undefined : totalLimit - returnData.length;
75
+ const pageLimit = remaining === undefined
76
+ ? exports.DEFAULT_MAUTIC_PAGE_SIZE
77
+ : Math.min(exports.DEFAULT_MAUTIC_PAGE_SIZE, remaining);
78
+ const pageQuery = {
79
+ ...baseQuery,
80
+ limit: pageLimit,
81
+ start: currentStart,
82
+ };
50
83
  try {
51
- responseData = await mauticApiRequest.call(this, method, endpoint, body, query);
84
+ const responseData = await mauticApiRequest.call(this, method, endpoint, body, pageQuery);
52
85
  if (responseData.errors) {
53
86
  throw new n8n_workflow_1.NodeApiError(this.getNode(), responseData);
54
87
  }
55
- const pageItems = responseData[propertyName]
56
- ? Object.values(responseData[propertyName])
57
- : [];
88
+ const pageItems = getResponseItems(responseData, propertyName);
58
89
  if (!pageItems.length) {
59
90
  break;
60
91
  }
61
- if (maxResults !== undefined && returnData.length + pageItems.length > maxResults) {
62
- const needed = maxResults - returnData.length;
63
- returnData.push(...pageItems.slice(0, needed));
64
- break;
65
- }
66
- else {
67
- returnData.push(...pageItems);
68
- }
69
- query.start = Number(query.start) + pageItems.length;
70
- // If less than limit returned, no more data
71
- if (pageItems.length < Number(query.limit)) {
92
+ returnData.push(...pageItems);
93
+ if (pageItems.length < pageLimit) {
72
94
  break;
73
95
  }
96
+ currentStart += pageItems.length;
74
97
  }
75
98
  catch (error) {
76
- // Optional: Only wrap non-NodeApiError errors
77
99
  if (error instanceof n8n_workflow_1.NodeApiError || error instanceof n8n_workflow_1.NodeOperationError) {
78
100
  throw error;
79
101
  }
@@ -25,6 +25,7 @@ const ContactSegmentDescription_1 = require("./ContactSegmentDescription");
25
25
  const EmailDescription_1 = require("./EmailDescription");
26
26
  const FieldDescription_1 = require("./FieldDescription");
27
27
  const GenericFunctions_1 = require("./GenericFunctions");
28
+ const loadOptionsCache_1 = require("./utils/loadOptionsCache");
28
29
  const NoteDescription_1 = require("./NoteDescription");
29
30
  const NotificationDescription_1 = require("./NotificationDescription");
30
31
  const SegmentEmailDescription_1 = require("./SegmentEmailDescription");
@@ -230,7 +231,7 @@ class MauticAdvanced {
230
231
  // select them easily
231
232
  async getTags() {
232
233
  const returnData = [];
233
- const tags = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'tags', 'GET', '/tags');
234
+ const tags = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'tags', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'tags', 'GET', '/tags'));
234
235
  for (const tag of tags) {
235
236
  returnData.push({
236
237
  name: tag.tag,
@@ -243,7 +244,7 @@ class MauticAdvanced {
243
244
  // select them easily
244
245
  async getStages() {
245
246
  const returnData = [];
246
- const stages = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'stages', 'GET', '/stages');
247
+ const stages = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'stages', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'stages', 'GET', '/stages'));
247
248
  for (const stage of stages) {
248
249
  returnData.push({
249
250
  name: stage.name,
@@ -256,7 +257,7 @@ class MauticAdvanced {
256
257
  // select them easily
257
258
  async getCompanyFields() {
258
259
  const returnData = [];
259
- const fields = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/company');
260
+ const fields = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'fields:company', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/company'));
260
261
  for (const field of fields) {
261
262
  returnData.push({
262
263
  name: field.label,
@@ -267,7 +268,7 @@ class MauticAdvanced {
267
268
  },
268
269
  async getIndustries() {
269
270
  const returnData = [];
270
- const fields = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/company');
271
+ const fields = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'fields:company', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/company'));
271
272
  for (const field of fields) {
272
273
  if (field.alias === 'companyindustry') {
273
274
  for (const { label, value } of field.properties.list) {
@@ -284,6 +285,15 @@ class MauticAdvanced {
284
285
  // select them easily
285
286
  async getContactFields() {
286
287
  const returnData = [];
288
+ const seen = new Set();
289
+ const addField = (field) => {
290
+ const value = String(field.value);
291
+ if (seen.has(value)) {
292
+ return;
293
+ }
294
+ seen.add(value);
295
+ returnData.push(field);
296
+ };
287
297
  // Add key system fields manually (except last_active, which is already present)
288
298
  const systemFields = [
289
299
  { name: 'Date Added', value: 'date_added' },
@@ -291,11 +301,57 @@ class MauticAdvanced {
291
301
  { name: 'ID', value: 'id' },
292
302
  { name: 'Owner ID', value: 'owner_id' },
293
303
  ];
294
- returnData.push(...systemFields);
304
+ systemFields.forEach(addField);
295
305
  // Fetch custom and other fields from Mautic
296
- const fields = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/contact');
306
+ const fields = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'fields:contact', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/contact'));
297
307
  for (const field of fields) {
298
- returnData.push({
308
+ addField({
309
+ name: field.label,
310
+ value: field.alias,
311
+ });
312
+ }
313
+ return returnData;
314
+ },
315
+ // Get all contact output fields, including raw top-level properties that are
316
+ // not returned by Mautic's contact fields endpoint.
317
+ async getContactFieldsToReturn() {
318
+ const returnData = [];
319
+ const seen = new Set();
320
+ const addField = (field) => {
321
+ const value = String(field.value);
322
+ if (seen.has(value)) {
323
+ return;
324
+ }
325
+ seen.add(value);
326
+ returnData.push(field);
327
+ };
328
+ const rawContactFields = [
329
+ { name: 'Date Added', value: 'date_added' },
330
+ { name: 'Date Modified', value: 'date_modified' },
331
+ { name: 'ID', value: 'id' },
332
+ { name: 'Owner ID', value: 'owner_id' },
333
+ { name: 'Color', value: 'color' },
334
+ { name: 'Created By', value: 'createdBy' },
335
+ { name: 'Created By User', value: 'createdByUser' },
336
+ { name: 'Date Identified', value: 'date_identified' },
337
+ { name: 'Do Not Contact', value: 'doNotContact' },
338
+ { name: 'Frequency Rules', value: 'frequencyRules' },
339
+ { name: 'IP Addresses', value: 'ipAddresses' },
340
+ { name: 'Is Published', value: 'isPublished' },
341
+ { name: 'Last Active', value: 'last_active' },
342
+ { name: 'Modified By', value: 'modifiedBy' },
343
+ { name: 'Modified By User', value: 'modifiedByUser' },
344
+ { name: 'Owner', value: 'owner' },
345
+ { name: 'Points', value: 'points' },
346
+ { name: 'Preferred Profile Image', value: 'preferredProfileImage' },
347
+ { name: 'Stage', value: 'stage' },
348
+ { name: 'Tags', value: 'tags' },
349
+ { name: 'UTM Tags', value: 'utmtags' },
350
+ ];
351
+ rawContactFields.forEach(addField);
352
+ const fields = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'fields:contact', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'fields', 'GET', '/fields/contact'));
353
+ for (const field of fields) {
354
+ addField({
299
355
  name: field.label,
300
356
  value: field.alias,
301
357
  });
@@ -306,7 +362,7 @@ class MauticAdvanced {
306
362
  // select them easily
307
363
  async getSegments() {
308
364
  const returnData = [];
309
- const segments = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', 'GET', '/segments');
365
+ const segments = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'segments', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', 'GET', '/segments'));
310
366
  for (const segment of segments) {
311
367
  returnData.push({
312
368
  name: segment.name,
@@ -318,7 +374,7 @@ class MauticAdvanced {
318
374
  // Get all the available segments (by alias) for search filtering
319
375
  async getSegmentAliases() {
320
376
  const returnData = [];
321
- const segments = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', 'GET', '/segments');
377
+ const segments = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'segments', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'lists', 'GET', '/segments'));
322
378
  for (const segment of segments) {
323
379
  // Use alias if available, otherwise skip (alias is required for search syntax)
324
380
  const alias = segment.alias || segment.id?.toString();
@@ -334,15 +390,13 @@ class MauticAdvanced {
334
390
  // Get all the available owners (users) for filtering
335
391
  async getOwners() {
336
392
  const returnData = [];
337
- const response = await GenericFunctions_1.mauticApiRequest.call(this, 'GET', '/contacts/list/owners');
338
- const owners = response || [];
339
- for (const owner of owners) {
340
- returnData.push({
341
- name: `${owner.firstName || ''} ${owner.lastName || ''}`.trim() ||
342
- owner.username ||
343
- `User ${owner.id}`,
344
- value: owner.id,
345
- });
393
+ const response = await GenericFunctions_1.mauticApiRequest.call(this, 'GET', '/users');
394
+ const users = response?.users ? Object.values(response.users) : [];
395
+ for (const user of users) {
396
+ const fullName = `${user.firstName || ''} ${user.lastName || ''}`.trim();
397
+ const display = fullName || user.username || `User ${user.id}`;
398
+ const label = user.email ? `${display} (${user.email})` : display;
399
+ returnData.push({ name: label, value: user.id });
346
400
  }
347
401
  return returnData;
348
402
  },
@@ -350,7 +404,7 @@ class MauticAdvanced {
350
404
  // select them easily
351
405
  async getCampaigns() {
352
406
  const returnData = [];
353
- const campaings = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'campaigns', 'GET', '/campaigns');
407
+ const campaings = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'campaigns', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'campaigns', 'GET', '/campaigns'));
354
408
  for (const campaign of campaings) {
355
409
  returnData.push({
356
410
  name: campaign.name,
@@ -363,7 +417,7 @@ class MauticAdvanced {
363
417
  // select them easily
364
418
  async getEmails() {
365
419
  const returnData = [];
366
- const emails = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails');
420
+ const emails = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'emails', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails'));
367
421
  for (const email of emails) {
368
422
  returnData.push({
369
423
  name: email.name,
@@ -376,7 +430,7 @@ class MauticAdvanced {
376
430
  // select them easily
377
431
  async getSegmentEmails() {
378
432
  const returnData = [];
379
- const emails = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails');
433
+ const emails = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'emails', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails'));
380
434
  for (const email of emails) {
381
435
  if (email.emailType === 'list') {
382
436
  returnData.push({
@@ -391,7 +445,7 @@ class MauticAdvanced {
391
445
  // select them easily
392
446
  async getCampaignEmails() {
393
447
  const returnData = [];
394
- const emails = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails');
448
+ const emails = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'emails', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'emails', 'GET', '/emails'));
395
449
  for (const email of emails) {
396
450
  if (email.emailType === 'template') {
397
451
  returnData.push({
@@ -406,7 +460,7 @@ class MauticAdvanced {
406
460
  // select them easily
407
461
  async getEmailCategories() {
408
462
  const returnData = [];
409
- const categories = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'categories', 'GET', '/categories');
463
+ const categories = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'categories', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'categories', 'GET', '/categories'));
410
464
  for (const category of categories) {
411
465
  // Filter for email bundle categories
412
466
  if (category.bundle === 'email' || category.bundle === 'global') {
@@ -422,7 +476,7 @@ class MauticAdvanced {
422
476
  // select them easily
423
477
  async getForms() {
424
478
  const returnData = [];
425
- const forms = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'forms', 'GET', '/forms');
479
+ const forms = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'forms', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'forms', 'GET', '/forms'));
426
480
  for (const form of forms) {
427
481
  returnData.push({
428
482
  name: form.name,
@@ -435,7 +489,7 @@ class MauticAdvanced {
435
489
  // select them easily
436
490
  async getAssets() {
437
491
  const returnData = [];
438
- const assets = await GenericFunctions_1.mauticApiRequestAllItems.call(this, 'assets', 'GET', '/assets');
492
+ const assets = await (0, loadOptionsCache_1.getCachedMauticLoadOptions)(this, 'assets', () => GenericFunctions_1.mauticApiRequestAllItems.call(this, 'assets', 'GET', '/assets'));
439
493
  for (const asset of assets) {
440
494
  returnData.push({
441
495
  name: asset.title,
@@ -38,7 +38,7 @@ async function createCompany(context, itemIndex) {
38
38
  const name = (0, ApiHelpers_1.getRequiredParam)(context, 'name', itemIndex);
39
39
  const body = { companyname: name };
40
40
  const additionalFields = (0, ApiHelpers_1.getOptionalParam)(context, 'additionalFields', itemIndex, {});
41
- const { addressUi, customFieldsUi, companyEmail, fax, industry, numberOfEmployees, phone, website, annualRevenue, description, ...rest } = additionalFields;
41
+ const { addressUi, customFieldsUi, companyEmail, fax, industry, numberOfEmployees, owner, phone, website, annualRevenue, description, ...rest } = additionalFields;
42
42
  if (addressUi?.addressValues) {
43
43
  const { addressValues } = addressUi;
44
44
  body.companyaddress1 = addressValues.address1;
@@ -56,6 +56,8 @@ async function createCompany(context, itemIndex) {
56
56
  body.companyindustry = industry;
57
57
  if (numberOfEmployees)
58
58
  body.companynumber_of_employees = numberOfEmployees;
59
+ if (owner)
60
+ body.owner = owner;
59
61
  if (phone)
60
62
  body.companyphone = phone;
61
63
  if (website)
@@ -73,7 +75,7 @@ async function createCompany(context, itemIndex) {
73
75
  const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'POST', '/companies/new', body);
74
76
  let result = response.company;
75
77
  if (simple) {
76
- result = result.fields.all;
78
+ result = toSimpleCompany(result);
77
79
  }
78
80
  return result;
79
81
  }
@@ -82,7 +84,7 @@ async function updateCompany(context, itemIndex) {
82
84
  const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
83
85
  const body = {};
84
86
  const updateFields = (0, ApiHelpers_1.getOptionalParam)(context, 'updateFields', itemIndex, {});
85
- const { addressUi, customFieldsUi, companyEmail, name, fax, industry, numberOfEmployees, phone, website, annualRevenue, description, ...rest } = updateFields;
87
+ const { addressUi, customFieldsUi, companyEmail, name, fax, industry, numberOfEmployees, owner, phone, website, annualRevenue, description, ...rest } = updateFields;
86
88
  if (addressUi?.addressValues) {
87
89
  const { addressValues } = addressUi;
88
90
  body.companyaddress1 = addressValues.address1;
@@ -102,6 +104,8 @@ async function updateCompany(context, itemIndex) {
102
104
  body.companyindustry = industry;
103
105
  if (numberOfEmployees)
104
106
  body.companynumber_of_employees = numberOfEmployees;
107
+ if (owner)
108
+ body.owner = owner;
105
109
  if (phone)
106
110
  body.companyphone = phone;
107
111
  if (website)
@@ -119,7 +123,7 @@ async function updateCompany(context, itemIndex) {
119
123
  const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'PATCH', `/companies/${companyId}/edit`, body);
120
124
  let result = response.company;
121
125
  if (simple) {
122
- result = result.fields.all;
126
+ result = toSimpleCompany(result);
123
127
  }
124
128
  return result;
125
129
  }
@@ -129,7 +133,7 @@ async function getCompany(context, itemIndex) {
129
133
  const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/companies/${companyId}`);
130
134
  let result = response.company;
131
135
  if (simple) {
132
- result = result.fields.all;
136
+ result = toSimpleCompany(result);
133
137
  }
134
138
  return (0, DataHelpers_1.convertNumericStrings)(result);
135
139
  }
@@ -154,7 +158,7 @@ async function getAllCompanies(context, itemIndex) {
154
158
  responseData = (response.companies ? Object.values(response.companies) : []);
155
159
  }
156
160
  if (simple) {
157
- responseData = responseData.map((item) => item.fields.all);
161
+ responseData = responseData.map((item) => toSimpleCompany(item));
158
162
  }
159
163
  return (0, DataHelpers_1.convertNumericStrings)(responseData);
160
164
  }
@@ -164,7 +168,14 @@ async function deleteCompany(context, itemIndex) {
164
168
  const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'DELETE', `/companies/${companyId}/delete`);
165
169
  let result = response.company;
166
170
  if (simple) {
167
- result = result.fields.all;
171
+ result = toSimpleCompany(result);
168
172
  }
169
173
  return result;
170
174
  }
175
+ function toSimpleCompany(company) {
176
+ return {
177
+ id: company.id,
178
+ owner: company.owner ?? null,
179
+ ...company.fields.all,
180
+ };
181
+ }
@@ -5,6 +5,7 @@ const n8n_workflow_1 = require("n8n-workflow");
5
5
  const change_case_1 = require("change-case");
6
6
  const ApiHelpers_1 = require("../utils/ApiHelpers");
7
7
  const DataHelpers_1 = require("../utils/DataHelpers");
8
+ const GenericFunctions_1 = require("../GenericFunctions");
8
9
  async function executeContactOperation(context, operation, i) {
9
10
  let responseData;
10
11
  try {
@@ -245,7 +246,8 @@ async function getAllContacts(context, itemIndex) {
245
246
  const anyDncOnly = options.anyDncOnly === true;
246
247
  const useDncPostFilter = emailDncOnly || smsDncOnly || anyDncOnly;
247
248
  if (useDncPostFilter) {
248
- responseData = await getContactsWithDncFilter(context, qs, options, returnAll ? undefined : options.limit);
249
+ const limit = returnAll ? undefined : (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
250
+ responseData = await getContactsWithDncFilter(context, qs, options, limit);
249
251
  }
250
252
  else {
251
253
  if (returnAll) {
@@ -622,13 +624,15 @@ async function getContactsWithDncFilter(context, qs, options, limit) {
622
624
  const emailDncOnly = options.emailDncOnly === true;
623
625
  const smsDncOnly = options.smsDncOnly === true;
624
626
  const anyDncOnly = options.anyDncOnly === true;
625
- const start = qs.start || 0;
626
- const finalLimit = limit || qs.limit || 30;
627
+ const requestedStart = Number(qs.start ?? 0);
628
+ const maxResults = typeof limit === 'number' && limit > 0 ? Math.floor(limit) : undefined;
627
629
  const contacts = [];
628
- let remaining = finalLimit;
629
- let currentStart = start;
630
- while (remaining > 0) {
631
- const pageLimit = Math.min(remaining, 30);
630
+ let remaining = maxResults;
631
+ let currentStart = Number.isFinite(requestedStart) && requestedStart > 0 ? requestedStart : 0;
632
+ while (remaining === undefined || remaining > 0) {
633
+ const pageLimit = remaining === undefined
634
+ ? GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE
635
+ : Math.min(remaining, GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE);
632
636
  const pageQs = { ...qs, start: currentStart, limit: pageLimit };
633
637
  const pageResponse = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, pageQs);
634
638
  const pageContacts = pageResponse.contacts ? Object.values(pageResponse.contacts) : [];
@@ -644,17 +648,19 @@ async function getContactsWithDncFilter(context, qs, options, limit) {
644
648
  return hasEmailDnc || hasSmsDnc;
645
649
  return true;
646
650
  });
647
- const toAdd = filtered.slice(0, remaining);
651
+ const toAdd = remaining === undefined ? filtered : filtered.slice(0, remaining);
648
652
  contacts.push(...toAdd);
653
+ if (remaining !== undefined) {
654
+ remaining -= toAdd.length;
655
+ }
649
656
  if (pageContacts.length < pageLimit)
650
657
  break;
651
- remaining -= toAdd.length;
652
- currentStart += pageLimit;
658
+ currentStart += pageContacts.length;
653
659
  }
654
660
  return contacts;
655
661
  }
656
662
  async function getContactOwners(context) {
657
- const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts/list/owners');
663
+ const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/users');
658
664
  return (0, DataHelpers_1.convertNumericStrings)(response);
659
665
  }
660
666
  async function getContactFields(context) {
@@ -17,10 +17,24 @@ async function makeApiRequest(context, method, endpoint, body = {}, query = {},
17
17
  }
18
18
  }
19
19
  exports.makeApiRequest = makeApiRequest;
20
+ function toOptionalPositiveInteger(value) {
21
+ const numericValue = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : undefined;
22
+ if (numericValue === undefined || !Number.isFinite(numericValue) || numericValue <= 0) {
23
+ return undefined;
24
+ }
25
+ return Math.floor(numericValue);
26
+ }
27
+ function preparePaginatedQuery(query, explicitLimit) {
28
+ const queryForPagination = { ...query };
29
+ const limit = explicitLimit ?? toOptionalPositiveInteger(queryForPagination.limit);
30
+ delete queryForPagination.limit;
31
+ return { query: queryForPagination, limit };
32
+ }
20
33
  // Standardized paginated API request wrapper
21
34
  async function makePaginatedRequest(context, resource, method, endpoint, body = {}, query = {}, limit) {
35
+ const pagination = preparePaginatedQuery(query, limit);
22
36
  try {
23
- return await GenericFunctions_1.mauticApiRequestAllItems.call(context, resource, method, endpoint, body, query, limit);
37
+ return await GenericFunctions_1.mauticApiRequestAllItems.call(context, resource, method, endpoint, body, pagination.query, pagination.limit);
24
38
  }
25
39
  catch (error) {
26
40
  if (error instanceof n8n_workflow_1.NodeApiError || error instanceof n8n_workflow_1.NodeOperationError) {
@@ -25,27 +25,77 @@ function wrapSingleItem(data) {
25
25
  exports.wrapSingleItem = wrapSingleItem;
26
26
  // Process contact fields data for simple mode
27
27
  function processContactFields(responseData, options, fieldsToReturn) {
28
+ const sourceData = Array.isArray(responseData) ? responseData : [responseData];
29
+ const filterFields = fieldsToReturn ?? options.fieldsToReturn;
28
30
  if (options.rawData === true) {
29
- return responseData;
31
+ if (Array.isArray(filterFields) && filterFields.length > 0) {
32
+ return sourceData.map((item) => filterContactFields(item, item, filterFields));
33
+ }
34
+ return sourceData;
30
35
  }
31
- const processedData = Array.isArray(responseData)
32
- ? responseData.map((item) => item.fields?.all || item)
33
- : [responseData.fields?.all || responseData];
34
- const filterFields = fieldsToReturn || options.fieldsToReturn;
36
+ const processedData = sourceData.map((item) => {
37
+ const all = item.fields?.all;
38
+ if (!all)
39
+ return item;
40
+ return { id: item.id, owner: item.owner ?? null, ...all };
41
+ });
35
42
  if (Array.isArray(filterFields) && filterFields.length > 0) {
36
- return processedData.map((item) => {
37
- const filtered = {};
38
- for (const field of filterFields) {
39
- if (Object.prototype.hasOwnProperty.call(item, field)) {
40
- filtered[field] = item[field];
41
- }
42
- }
43
- return filtered;
44
- });
43
+ return processedData.map((item, index) => filterContactFields(item, sourceData[index], filterFields));
45
44
  }
46
45
  return processedData;
47
46
  }
48
47
  exports.processContactFields = processContactFields;
48
+ function filterContactFields(preferredSource, originalSource, filterFields) {
49
+ const filtered = {};
50
+ for (const field of filterFields) {
51
+ const preferredLookup = readContactField(preferredSource, field);
52
+ const lookup = preferredLookup.found
53
+ ? preferredLookup
54
+ : readContactField(originalSource, field);
55
+ if (lookup.found && lookup.value !== undefined) {
56
+ filtered[field] = lookup.value;
57
+ }
58
+ }
59
+ return filtered;
60
+ }
61
+ function readContactField(source, field) {
62
+ if (!source) {
63
+ return { found: false };
64
+ }
65
+ if (Object.prototype.hasOwnProperty.call(source, field)) {
66
+ return { found: true, value: source[field] };
67
+ }
68
+ const rawField = readRawContactFieldAlias(source, field);
69
+ if (rawField.found) {
70
+ return rawField;
71
+ }
72
+ const fieldsAll = source.fields?.all;
73
+ if (fieldsAll && Object.prototype.hasOwnProperty.call(fieldsAll, field)) {
74
+ return { found: true, value: fieldsAll[field] };
75
+ }
76
+ return { found: false };
77
+ }
78
+ function readRawContactFieldAlias(source, field) {
79
+ const rawAliasMap = {
80
+ date_added: ['dateAdded'],
81
+ date_identified: ['dateIdentified'],
82
+ date_modified: ['dateModified'],
83
+ last_active: ['lastActive'],
84
+ };
85
+ for (const alias of rawAliasMap[field] || []) {
86
+ if (Object.prototype.hasOwnProperty.call(source, alias)) {
87
+ return { found: true, value: source[alias] };
88
+ }
89
+ }
90
+ if (field === 'owner_id' && Object.prototype.hasOwnProperty.call(source, 'owner')) {
91
+ const owner = source.owner;
92
+ if (owner && typeof owner === 'object' && Object.prototype.hasOwnProperty.call(owner, 'id')) {
93
+ return { found: true, value: owner.id };
94
+ }
95
+ return { found: true, value: owner };
96
+ }
97
+ return { found: false };
98
+ }
49
99
  // Validate and parse JSON input parameters
50
100
  function validateJsonParameter(context, paramName, itemIndex) {
51
101
  const jsonString = context.getNodeParameter(paramName, itemIndex);
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.requestMauticAuthenticated = void 0;
4
4
  const oauthRequestQueue_1 = require("./oauthRequestQueue");
5
+ const authErrorHelpers_1 = require("./authErrorHelpers");
5
6
  function trimTrailingSlash(url) {
6
7
  return url.endsWith('/') ? url.slice(0, -1) : url;
7
8
  }
@@ -34,6 +35,11 @@ function getOAuthCredentialQueueKey(node) {
34
35
  const keyPart = credentialRefPart(credentialRef) ?? 'unknown';
35
36
  return `mauticAdvancedOAuth2Api:${keyPart}`;
36
37
  }
38
+ async function requestOAuth2WithMauticOptions(context, helperContext, requestOptions) {
39
+ return (await helperContext.helpers.requestOAuth2.call(context, 'mauticAdvancedOAuth2Api', requestOptions, {
40
+ includeCredentialsOnRefreshOnBody: true,
41
+ }));
42
+ }
37
43
  async function requestMauticAuthenticated(context, authenticationMethod, options) {
38
44
  const credentialType = authenticationMethod === 'credentials' ? 'mauticAdvancedApi' : 'mauticAdvancedOAuth2Api';
39
45
  const credentials = await context.getCredentials(credentialType);
@@ -43,10 +49,18 @@ async function requestMauticAuthenticated(context, authenticationMethod, options
43
49
  return (await helperContext.helpers.requestWithAuthentication.call(context, 'mauticAdvancedApi', requestOptions));
44
50
  }
45
51
  const queueKey = getOAuthCredentialQueueKey(context.getNode());
46
- return await (0, oauthRequestQueue_1.runWithCredentialLock)(queueKey, async () => {
47
- return (await helperContext.helpers.requestOAuth2.call(context, 'mauticAdvancedOAuth2Api', requestOptions, {
48
- includeCredentialsOnRefreshOnBody: true,
49
- }));
50
- });
52
+ try {
53
+ return await (0, oauthRequestQueue_1.runTrackedCredentialRequest)(queueKey, async () => requestOAuth2WithMauticOptions(context, helperContext, requestOptions));
54
+ }
55
+ catch (error) {
56
+ if (!(0, authErrorHelpers_1.isInvalidGrantError)(error)) {
57
+ throw error;
58
+ }
59
+ await (0, oauthRequestQueue_1.waitForTrackedCredentialRequests)(queueKey);
60
+ return await (0, oauthRequestQueue_1.runWithCredentialLock)(queueKey, async () => {
61
+ await (0, oauthRequestQueue_1.waitForTrackedCredentialRequests)(queueKey);
62
+ return await requestOAuth2WithMauticOptions(context, helperContext, requestOptions);
63
+ });
64
+ }
51
65
  }
52
66
  exports.requestMauticAuthenticated = requestMauticAuthenticated;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearMauticLoadOptionsCacheForTests = exports.getCachedMauticLoadOptions = exports.MAUTIC_LOAD_OPTIONS_CACHE_TTL_MS = void 0;
4
+ exports.MAUTIC_LOAD_OPTIONS_CACHE_TTL_MS = 60000;
5
+ const loadOptionsCache = new Map();
6
+ function getCredentialType(authenticationMethod) {
7
+ return authenticationMethod === 'credentials' ? 'mauticAdvancedApi' : 'mauticAdvancedOAuth2Api';
8
+ }
9
+ function withoutTrailingSlashes(value) {
10
+ return value.replace(/\/+$/, '');
11
+ }
12
+ function normalizeCredentialUrl(value) {
13
+ if (typeof value !== 'string') {
14
+ return '';
15
+ }
16
+ const trimmed = value.trim();
17
+ if (!trimmed) {
18
+ return '';
19
+ }
20
+ try {
21
+ const url = new URL(trimmed);
22
+ url.protocol = url.protocol.toLowerCase();
23
+ url.hostname = url.hostname.toLowerCase();
24
+ url.hash = '';
25
+ url.search = '';
26
+ url.pathname = withoutTrailingSlashes(url.pathname);
27
+ return withoutTrailingSlashes(url.toString());
28
+ }
29
+ catch {
30
+ return withoutTrailingSlashes(trimmed);
31
+ }
32
+ }
33
+ function getMauticVersion(credentials) {
34
+ return String(credentials.mauticVersion || 'v6');
35
+ }
36
+ function credentialRefPart(value) {
37
+ if (typeof value === 'string' && value) {
38
+ return value;
39
+ }
40
+ if (typeof value !== 'object' || value === null) {
41
+ return undefined;
42
+ }
43
+ const credentialRef = value;
44
+ if (credentialRef.id) {
45
+ return credentialRef.id;
46
+ }
47
+ if (credentialRef.name) {
48
+ return credentialRef.name;
49
+ }
50
+ return undefined;
51
+ }
52
+ function getCredentialReference(context, credentialType) {
53
+ const contextWithNode = context;
54
+ const credentialRef = contextWithNode.getNode?.().credentials?.[credentialType];
55
+ const nodeCredentialPart = credentialRefPart(credentialRef);
56
+ if (nodeCredentialPart) {
57
+ return nodeCredentialPart;
58
+ }
59
+ return 'unknown';
60
+ }
61
+ function getCacheKey(authenticationMethod, credentialType, credentialReference, credentials, datasetKey) {
62
+ return JSON.stringify([
63
+ authenticationMethod,
64
+ credentialType,
65
+ credentialReference,
66
+ normalizeCredentialUrl(credentials.url),
67
+ getMauticVersion(credentials),
68
+ datasetKey,
69
+ ]);
70
+ }
71
+ async function getCachedMauticLoadOptions(context, datasetKey, loadOptions, ttlMs = exports.MAUTIC_LOAD_OPTIONS_CACHE_TTL_MS) {
72
+ const authenticationMethod = context.getNodeParameter('authentication', 'credentials');
73
+ const credentialType = getCredentialType(authenticationMethod);
74
+ const credentials = await context.getCredentials(credentialType);
75
+ const credentialReference = getCredentialReference(context, credentialType);
76
+ const cacheKey = getCacheKey(authenticationMethod, credentialType, credentialReference, credentials, datasetKey);
77
+ const now = Date.now();
78
+ const cachedEntry = loadOptionsCache.get(cacheKey);
79
+ if (cachedEntry && cachedEntry.expiresAt > now) {
80
+ return await cachedEntry.promise;
81
+ }
82
+ if (cachedEntry) {
83
+ loadOptionsCache.delete(cacheKey);
84
+ }
85
+ const promise = (async () => loadOptions())().catch((error) => {
86
+ const latestEntry = loadOptionsCache.get(cacheKey);
87
+ if (latestEntry?.promise === promise) {
88
+ loadOptionsCache.delete(cacheKey);
89
+ }
90
+ throw error;
91
+ });
92
+ loadOptionsCache.set(cacheKey, {
93
+ expiresAt: now + ttlMs,
94
+ promise,
95
+ });
96
+ return await promise;
97
+ }
98
+ exports.getCachedMauticLoadOptions = getCachedMauticLoadOptions;
99
+ function clearMauticLoadOptionsCacheForTests() {
100
+ loadOptionsCache.clear();
101
+ }
102
+ exports.clearMauticLoadOptionsCacheForTests = clearMauticLoadOptionsCacheForTests;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._getOAuthRequestQueueSizeForTests = exports.runWithCredentialLock = void 0;
3
+ exports._getTrackedOAuthRequestCountForTests = exports._getOAuthRequestQueueSizeForTests = exports.waitForTrackedCredentialRequests = exports.runTrackedCredentialRequest = exports.runWithCredentialLock = void 0;
4
4
  const oauthRequestQueue = new Map();
5
+ const oauthInFlightRequests = new Map();
5
6
  async function runWithCredentialLock(key, fn) {
6
7
  const previous = oauthRequestQueue.get(key) ?? Promise.resolve();
7
8
  const run = (async () => {
@@ -20,7 +21,33 @@ async function runWithCredentialLock(key, fn) {
20
21
  }
21
22
  }
22
23
  exports.runWithCredentialLock = runWithCredentialLock;
24
+ async function runTrackedCredentialRequest(key, fn) {
25
+ const run = Promise.resolve().then(fn);
26
+ const tracker = run.then(() => undefined, () => undefined);
27
+ const requests = oauthInFlightRequests.get(key) ?? new Set();
28
+ requests.add(tracker);
29
+ oauthInFlightRequests.set(key, requests);
30
+ try {
31
+ return await run;
32
+ }
33
+ finally {
34
+ requests.delete(tracker);
35
+ if (requests.size === 0) {
36
+ oauthInFlightRequests.delete(key);
37
+ }
38
+ }
39
+ }
40
+ exports.runTrackedCredentialRequest = runTrackedCredentialRequest;
41
+ async function waitForTrackedCredentialRequests(key) {
42
+ const requests = Array.from(oauthInFlightRequests.get(key) ?? []);
43
+ await Promise.all(requests.map(async (request) => await request.catch(() => undefined)));
44
+ }
45
+ exports.waitForTrackedCredentialRequests = waitForTrackedCredentialRequests;
23
46
  function _getOAuthRequestQueueSizeForTests() {
24
47
  return oauthRequestQueue.size;
25
48
  }
26
49
  exports._getOAuthRequestQueueSizeForTests = _getOAuthRequestQueueSizeForTests;
50
+ function _getTrackedOAuthRequestCountForTests(key) {
51
+ return oauthInFlightRequests.get(key)?.size ?? 0;
52
+ }
53
+ exports._getTrackedOAuthRequestCountForTests = _getTrackedOAuthRequestCountForTests;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-mautic-advanced",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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",