n8n-nodes-mautic-advanced 1.3.0 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/nodes/MauticAdvanced/ContactDescription.js +1 -1
- package/dist/nodes/MauticAdvanced/GenericFunctions.js +29 -9
- package/dist/nodes/MauticAdvanced/operations/CompanyOperations.js +139 -67
- package/dist/nodes/MauticAdvanced/operations/ContactOperations.js +44 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,6 +193,11 @@ npm install n8n-nodes-mautic-advanced
|
|
|
193
193
|
3. Enter your Mautic URL
|
|
194
194
|
4. Follow the OAuth2 authorization flow
|
|
195
195
|
|
|
196
|
+
> **Auth method and Company Owner (v7 enrichment):** The Mautic version (v6 vs v7) is auto-detected by probing the v2 API (API Platform). Company **owner** is a v7-only field, populated by enriching Company *Get* / *Get Many* via the v2 API. The v2 API requires an auth method the server accepts:
|
|
197
|
+
>
|
|
198
|
+
> - **Basic auth** — confirmed working with the v2 API; owner enrichment populates `owner: { id }`.
|
|
199
|
+
> - **OAuth2** — depends on the Mautic server allowing v2 access for the bearer token. Many Mautic installs reject v1-style OAuth2 tokens at the v2 API Platform firewall (HTTP 401), in which case the node falls back to the v1 API for all company operations and **owner cannot be enriched** (`owner: null`). When this happens, a warning is logged explaining the cause. Custom fields and all other company data are unaffected. To populate owner, switch the credential to Basic auth, or enable v2 API access for OAuth2 on the Mautic server.
|
|
200
|
+
|
|
196
201
|
## Advanced Features
|
|
197
202
|
|
|
198
203
|
### Where Filters
|
|
@@ -1561,7 +1561,7 @@ exports.contactFields = [
|
|
|
1561
1561
|
},
|
|
1562
1562
|
},
|
|
1563
1563
|
default: [],
|
|
1564
|
-
description: 'Filter contacts by owner (assigned user).
|
|
1564
|
+
description: 'Filter contacts by owner (assigned user). Matched on the returned owner ID. Note: Mautic has no server-side owner-by-ID search, so this is applied client-side over the result set — combine with other filters (e.g. segment/tag/DNC) to narrow the set first.',
|
|
1565
1565
|
},
|
|
1566
1566
|
{
|
|
1567
1567
|
displayName: 'Published Only',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = exports.getMauticVersion = void 0;
|
|
3
|
+
exports.validateJSON = exports.serialiseMauticWhere = exports.mauticApiRequestAllItems = exports.DEFAULT_MAUTIC_PAGE_SIZE = exports.mauticApiRequest = exports.getMauticV2Status = exports.getMauticVersion = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
5
|
const authenticatedRequest_1 = require("./utils/authenticatedRequest");
|
|
6
6
|
const versionCache = new Map();
|
|
@@ -22,7 +22,7 @@ function normalizeInstanceUrl(raw) {
|
|
|
22
22
|
return trimmed.replace(/\/+$/, '');
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
async function
|
|
25
|
+
async function detectMauticV2(context) {
|
|
26
26
|
const authMethod = context.getNodeParameter('authentication', 0, 'credentials');
|
|
27
27
|
const credentialType = authMethod === 'credentials' ? 'mauticAdvancedApi' : 'mauticAdvancedOAuth2Api';
|
|
28
28
|
const credentials = await context.getCredentials(credentialType);
|
|
@@ -30,23 +30,43 @@ async function getMauticVersion(context) {
|
|
|
30
30
|
const cacheKey = `${credentialType}:${baseUrl}`;
|
|
31
31
|
const cached = versionCache.get(cacheKey);
|
|
32
32
|
if (cached && Date.now() < cached.expiresAt) {
|
|
33
|
-
return cached.version;
|
|
33
|
+
return { version: cached.version, v2Status: cached.v2Status };
|
|
34
34
|
}
|
|
35
|
-
let
|
|
35
|
+
let v2Status;
|
|
36
36
|
try {
|
|
37
37
|
await mauticApiRequest.call(context, 'GET', '/v2/companies', {}, { page: 1 }, undefined, {
|
|
38
38
|
Accept: 'application/json',
|
|
39
39
|
});
|
|
40
|
-
|
|
40
|
+
v2Status = 'usable';
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const httpCode = String(error?.httpCode ?? '');
|
|
44
|
+
// 401/403 = v2 route exists but the credential is rejected/forbidden there. Mautic's v2 API
|
|
45
|
+
// Platform firewall commonly rejects v1-style OAuth2 bearer tokens (Basic auth works). The
|
|
46
|
+
// route exists, but this credential cannot use it — fall back to v1 for routing.
|
|
47
|
+
// 404 / parse errors / network = v2 route absent → genuine v6 instance.
|
|
48
|
+
v2Status = httpCode === '401' || httpCode === '403' ? 'unauthorized' : 'absent';
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// Routing version: only treat as v7 when v2 is actually usable, so that operations route to the
|
|
51
|
+
// v1 endpoints (which work under this credential) whenever v2 is unreachable. Owner enrichment,
|
|
52
|
+
// a v7-only feature, is gated separately on `v2Status === 'usable'`.
|
|
53
|
+
const version = v2Status === 'usable' ? 'v7' : 'v6';
|
|
54
|
+
versionCache.set(cacheKey, { version, v2Status, expiresAt: Date.now() + VERSION_CACHE_TTL_MS });
|
|
55
|
+
return { version, v2Status };
|
|
56
|
+
}
|
|
57
|
+
async function getMauticVersion(context) {
|
|
58
|
+
return (await detectMauticV2(context)).version;
|
|
48
59
|
}
|
|
49
60
|
exports.getMauticVersion = getMauticVersion;
|
|
61
|
+
/**
|
|
62
|
+
* Returns whether the Mautic v2 (API Platform) endpoints are usable with the active credential.
|
|
63
|
+
* Callers use this to decide whether v7-only enrichment is possible and to warn actionably when a
|
|
64
|
+
* v2 route exists but the credential is rejected there (`unauthorized`).
|
|
65
|
+
*/
|
|
66
|
+
async function getMauticV2Status(context) {
|
|
67
|
+
return (await detectMauticV2(context)).v2Status;
|
|
68
|
+
}
|
|
69
|
+
exports.getMauticV2Status = getMauticV2Status;
|
|
50
70
|
async function mauticApiRequest(method, endpoint, body = {}, query, uri, headers) {
|
|
51
71
|
const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials');
|
|
52
72
|
const options = {
|
|
@@ -223,92 +223,164 @@ async function updateCompany(context, itemIndex) {
|
|
|
223
223
|
}
|
|
224
224
|
return result;
|
|
225
225
|
}
|
|
226
|
+
// Logged once per Get/Get Many call when the v2 API exists but rejected the credential, so the
|
|
227
|
+
// reason owner is null is visible in the n8n logs instead of failing silently.
|
|
228
|
+
const V2_UNAUTHORIZED_OWNER_WARNING = 'Mautic company owner enrichment skipped: the v2 API (API Platform) rejected this credential (401/403). ' +
|
|
229
|
+
'Owner is a v7-only field that requires an auth method the v2 API accepts — Basic auth is confirmed working; ' +
|
|
230
|
+
'OAuth2 depends on the Mautic server allowing v2 access for the token. Returning owner: null. ' +
|
|
231
|
+
'Switch the credential to Basic auth (or enable v2 access for OAuth2 on the Mautic server) to populate owner.';
|
|
232
|
+
// Force API Platform to return JSON-LD/Hydra so the owner is serialised with its `@id` IRI
|
|
233
|
+
// (/api/v2/users/{id}). The default `application/json` representation omits the IRI, leaving only
|
|
234
|
+
// FormEntity fields (isPublished/dateAdded/dateModified) with no way to resolve the owner's user ID.
|
|
235
|
+
const LD_JSON_HEADERS = { Accept: 'application/ld+json' };
|
|
236
|
+
// Extract the owner's user ID from a v7 (JSON-LD) company object.
|
|
237
|
+
// JSON-LD embeds the owner with `@id: "/api/v2/users/{id}"`; some shapes also expose a plain `id`.
|
|
238
|
+
function extractOwnerFromV7(v7Item) {
|
|
239
|
+
const owner = v7Item?.owner;
|
|
240
|
+
if (owner === null || owner === undefined)
|
|
241
|
+
return null;
|
|
242
|
+
// Owner may be embedded as an object (with @id), or serialised as a bare IRI string.
|
|
243
|
+
const iri = typeof owner === 'string' ? owner : owner['@id'];
|
|
244
|
+
if (iri) {
|
|
245
|
+
const match = /\/(\d+)$/.exec(iri);
|
|
246
|
+
if (match)
|
|
247
|
+
return { id: Number(match[1]) };
|
|
248
|
+
}
|
|
249
|
+
if (typeof owner === 'object' && owner.id !== undefined && owner.id !== null) {
|
|
250
|
+
return { id: Number(owner.id) };
|
|
251
|
+
}
|
|
252
|
+
// No IRI and no id (plain-JSON owner) — return the partial FormEntity data as-is
|
|
253
|
+
return owner;
|
|
254
|
+
}
|
|
255
|
+
// Requested page size. NOTE: stock Mautic disables client control of page size
|
|
256
|
+
// (pagination_client_items_per_page = false) and hard-caps at 30, so this is currently a no-op on
|
|
257
|
+
// default installs — the server returns 30/page regardless. Kept because it is harmless and engages
|
|
258
|
+
// automatically if an instance enables client page size; termination does not rely on it (we count
|
|
259
|
+
// actual items returned, see the loop below).
|
|
260
|
+
const V7_OWNER_PAGE_SIZE = 100;
|
|
261
|
+
// Backstop only — the loop normally exits when every needed owner is found or the collection ends.
|
|
262
|
+
const V7_OWNER_MAX_PAGES = 1000;
|
|
263
|
+
// Read the collection members from a v2 list response, tolerating both the legacy Hydra key
|
|
264
|
+
// (`hydra:member`) and the newer API Platform 4.x form (`member`), plus a bare JSON array.
|
|
265
|
+
function getV2CollectionItems(response) {
|
|
266
|
+
if (Array.isArray(response))
|
|
267
|
+
return response;
|
|
268
|
+
return response?.['hydra:member'] ?? response?.member ?? [];
|
|
269
|
+
}
|
|
270
|
+
function getV2TotalItems(response) {
|
|
271
|
+
const total = response?.['hydra:totalItems'] ?? response?.totalItems;
|
|
272
|
+
return typeof total === 'number' ? total : undefined;
|
|
273
|
+
}
|
|
274
|
+
// Build a map of companyId → owner for ONLY the given company IDs, by paging the v2 collection
|
|
275
|
+
// (JSON-LD, so the owner `@id` IRI is present) and stopping as soon as every needed owner is
|
|
276
|
+
// resolved. Mautic's v2 collection defaults to ORDER BY id ASC, so a limited Get Many (whose v1
|
|
277
|
+
// result is the lowest ids, also id ASC) resolves its owners in the first page(s) rather than
|
|
278
|
+
// scanning the whole instance. Returns an empty map when no IDs are needed.
|
|
279
|
+
async function buildV7OwnerMap(context, neededIds) {
|
|
280
|
+
const ownerMap = new Map();
|
|
281
|
+
if (neededIds.size === 0)
|
|
282
|
+
return ownerMap;
|
|
283
|
+
let page = 1;
|
|
284
|
+
let itemsSeen = 0;
|
|
285
|
+
while (page <= V7_OWNER_MAX_PAGES) {
|
|
286
|
+
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {},
|
|
287
|
+
// order[id]=asc is a no-op on stock Mautic (no OrderFilter on Company), but the v2 collection
|
|
288
|
+
// already defaults to ORDER BY id ASC, so this just makes the relied-on ordering explicit.
|
|
289
|
+
{ page, itemsPerPage: V7_OWNER_PAGE_SIZE, 'order[id]': 'asc' }, undefined, LD_JSON_HEADERS);
|
|
290
|
+
const items = getV2CollectionItems(response);
|
|
291
|
+
if (!items.length)
|
|
292
|
+
break;
|
|
293
|
+
itemsSeen += items.length;
|
|
294
|
+
for (const item of items) {
|
|
295
|
+
const id = Number(item.id);
|
|
296
|
+
if (id && neededIds.has(id))
|
|
297
|
+
ownerMap.set(id, extractOwnerFromV7(item));
|
|
298
|
+
}
|
|
299
|
+
// Stop once every needed owner is resolved.
|
|
300
|
+
if (ownerMap.size >= neededIds.size)
|
|
301
|
+
break;
|
|
302
|
+
// Stop at the end of the collection (covers needed IDs that no longer exist). Compare against
|
|
303
|
+
// the actual number of items seen, not an assumed page size — the server may cap itemsPerPage.
|
|
304
|
+
const total = getV2TotalItems(response);
|
|
305
|
+
if (total !== undefined && itemsSeen >= total)
|
|
306
|
+
break;
|
|
307
|
+
page++;
|
|
308
|
+
}
|
|
309
|
+
return ownerMap;
|
|
310
|
+
}
|
|
226
311
|
async function getCompany(context, itemIndex) {
|
|
227
312
|
const companyId = (0, ApiHelpers_1.getRequiredParam)(context, 'companyId', itemIndex);
|
|
228
313
|
const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
314
|
+
const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context);
|
|
315
|
+
// v1: custom fields via fields.all
|
|
316
|
+
const v1Response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/companies/${companyId}`);
|
|
317
|
+
let result = v1Response.company;
|
|
318
|
+
if (v2Status === 'usable') {
|
|
319
|
+
try {
|
|
320
|
+
// JSON-LD response includes the owner @id IRI for user-ID extraction
|
|
321
|
+
const v7Company = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', `/v2/companies/${companyId}`, {}, {}, undefined, LD_JSON_HEADERS);
|
|
322
|
+
result = { ...result, owner: extractOwnerFromV7(v7Company) };
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
// Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of
|
|
326
|
+
// silently degrading to owner: null, so the cause is visible in the logs.
|
|
327
|
+
context.logger.warn(`Mautic company owner enrichment failed for company ${companyId}: ${error?.message ?? error}. Returning owner: null.`);
|
|
328
|
+
}
|
|
235
329
|
}
|
|
236
|
-
else {
|
|
237
|
-
|
|
238
|
-
|
|
330
|
+
else if (v2Status === 'unauthorized') {
|
|
331
|
+
// v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once.
|
|
332
|
+
context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING);
|
|
239
333
|
}
|
|
240
|
-
if (simple)
|
|
334
|
+
if (simple)
|
|
241
335
|
result = toSimpleCompany(result);
|
|
242
|
-
|
|
243
|
-
// v7 returns proper JSON types already; convertNumericStrings only needed for v1 string responses
|
|
244
|
-
return mauticVersion === 'v7' ? result : (0, DataHelpers_1.convertNumericStrings)(result);
|
|
336
|
+
return (0, DataHelpers_1.convertNumericStrings)(result);
|
|
245
337
|
}
|
|
246
338
|
async function getAllCompanies(context, itemIndex) {
|
|
247
339
|
const returnAll = (0, ApiHelpers_1.getOptionalParam)(context, 'returnAll', itemIndex, false);
|
|
248
340
|
const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
|
|
249
|
-
const
|
|
341
|
+
const v2Status = await (0, GenericFunctions_1.getMauticV2Status)(context);
|
|
342
|
+
const additionalFields = (0, ApiHelpers_1.getOptionalParam)(context, 'additionalFields', itemIndex, {});
|
|
343
|
+
const qs = (0, DataHelpers_1.buildQueryFromOptions)(additionalFields);
|
|
344
|
+
if (!qs.orderBy)
|
|
345
|
+
qs.orderBy = 'id';
|
|
346
|
+
if (!qs.orderByDir)
|
|
347
|
+
qs.orderByDir = 'asc';
|
|
348
|
+
// v1: custom fields via fields.all
|
|
250
349
|
let responseData;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, undefined);
|
|
255
|
-
const allItems = [];
|
|
256
|
-
let page = 1;
|
|
257
|
-
while (true) {
|
|
258
|
-
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {}, { page }, undefined, { Accept: 'application/json' });
|
|
259
|
-
const pageItems = Array.isArray(response)
|
|
260
|
-
? response
|
|
261
|
-
: (response?.['hydra:member'] ?? []);
|
|
262
|
-
if (!pageItems.length)
|
|
263
|
-
break;
|
|
264
|
-
allItems.push(...pageItems);
|
|
265
|
-
if (limit !== undefined && allItems.length >= limit)
|
|
266
|
-
break;
|
|
267
|
-
page++;
|
|
268
|
-
}
|
|
269
|
-
responseData = limit !== undefined ? allItems.slice(0, limit) : allItems;
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
// page through until limit satisfied — v7 page size is fixed (~30), limit may exceed it
|
|
273
|
-
const limit = (0, ApiHelpers_1.getRequiredParam)(context, 'limit', itemIndex);
|
|
274
|
-
const allItems = [];
|
|
275
|
-
let page = 1;
|
|
276
|
-
while (allItems.length < limit) {
|
|
277
|
-
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/v2/companies', {}, { page }, undefined, { Accept: 'application/json' });
|
|
278
|
-
const pageItems = Array.isArray(response)
|
|
279
|
-
? response
|
|
280
|
-
: (response?.['hydra:member'] ?? []);
|
|
281
|
-
if (!pageItems.length)
|
|
282
|
-
break;
|
|
283
|
-
allItems.push(...pageItems);
|
|
284
|
-
page++;
|
|
285
|
-
}
|
|
286
|
-
responseData = allItems.slice(0, limit);
|
|
287
|
-
}
|
|
350
|
+
if (returnAll) {
|
|
351
|
+
const limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, undefined);
|
|
352
|
+
responseData = await (0, ApiHelpers_1.makePaginatedRequest)(context, 'companies', 'GET', '/companies', {}, qs, limit);
|
|
288
353
|
}
|
|
289
354
|
else {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
355
|
+
const limit = (0, ApiHelpers_1.getRequiredParam)(context, 'limit', itemIndex);
|
|
356
|
+
qs.limit = limit;
|
|
357
|
+
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/companies', {}, qs);
|
|
358
|
+
responseData = (response.companies ? Object.values(response.companies) : []);
|
|
359
|
+
}
|
|
360
|
+
if (v2Status === 'usable' && responseData.length > 0) {
|
|
361
|
+
try {
|
|
362
|
+
// v7 enrichment: resolve owners for ONLY the companies v1 returned, then overlay them.
|
|
363
|
+
const neededIds = new Set(responseData.map((company) => Number(company.id)).filter((id) => Number.isFinite(id)));
|
|
364
|
+
const ownerMap = await buildV7OwnerMap(context, neededIds);
|
|
365
|
+
responseData = responseData.map((company) => ({
|
|
366
|
+
...company,
|
|
367
|
+
owner: ownerMap.get(Number(company.id)) ?? null,
|
|
368
|
+
}));
|
|
299
369
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
responseData = (response.companies ? Object.values(response.companies) : []);
|
|
370
|
+
catch (error) {
|
|
371
|
+
// Enrichment failed unexpectedly (v2 was usable at probe time). Surface it instead of
|
|
372
|
+
// silently degrading to owner: null, so the cause is visible in the logs.
|
|
373
|
+
context.logger.warn(`Mautic company owner enrichment failed for Get Many: ${error?.message ?? error}. Returning owner: null for all rows.`);
|
|
305
374
|
}
|
|
306
375
|
}
|
|
376
|
+
else if (v2Status === 'unauthorized') {
|
|
377
|
+
// v2 route exists but the credential is rejected there — owner cannot be enriched. Warn once.
|
|
378
|
+
context.logger.warn(V2_UNAUTHORIZED_OWNER_WARNING);
|
|
379
|
+
}
|
|
307
380
|
if (simple) {
|
|
308
381
|
responseData = responseData.map((item) => toSimpleCompany(item));
|
|
309
382
|
}
|
|
310
|
-
|
|
311
|
-
return mauticVersion === 'v7' ? responseData : (0, DataHelpers_1.convertNumericStrings)(responseData);
|
|
383
|
+
return (0, DataHelpers_1.convertNumericStrings)(responseData);
|
|
312
384
|
}
|
|
313
385
|
async function deleteCompany(context, itemIndex) {
|
|
314
386
|
const simple = (0, ApiHelpers_1.getOptionalParam)(context, 'simple', itemIndex, false);
|
|
@@ -191,12 +191,34 @@ async function getAllContacts(context, itemIndex) {
|
|
|
191
191
|
if (filterExpr)
|
|
192
192
|
searchParts.push(filterExpr);
|
|
193
193
|
}
|
|
194
|
-
// Owner filter
|
|
194
|
+
// Owner filter — applied CLIENT-SIDE on the contact's owner.id.
|
|
195
|
+
// Mautic's `owner:` search command matches the owner's first/last NAME (LIKE), not the user id,
|
|
196
|
+
// so sending `owner:<id>` (what the Owner(s) dropdown provides) silently returns nothing. The v1
|
|
197
|
+
// contact body already includes `owner.id`, so we filter on that instead.
|
|
195
198
|
const owners = options.owners;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
const ownerIdFilter = Array.isArray(owners) && owners.length > 0
|
|
200
|
+
? new Set(owners.map((o) => Number(o)).filter((n) => Number.isFinite(n)))
|
|
201
|
+
: undefined;
|
|
202
|
+
// Do-Not-Contact filter — applied SERVER-SIDE via Mautic's `dnc:` search command, so only
|
|
203
|
+
// matching contacts are fetched (replaces the previous client-side page-and-discard scan).
|
|
204
|
+
// `dnc:<channel>` = IS on DNC for that channel; `dnc:any` = on any channel.
|
|
205
|
+
const emailDncOnly = options.emailDncOnly === true;
|
|
206
|
+
const smsDncOnly = options.smsDncOnly === true;
|
|
207
|
+
const anyDncOnly = options.anyDncOnly === true;
|
|
208
|
+
if (anyDncOnly) {
|
|
209
|
+
searchParts.push('dnc:any');
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const dncTokens = [];
|
|
213
|
+
if (emailDncOnly)
|
|
214
|
+
dncTokens.push('dnc:email');
|
|
215
|
+
if (smsDncOnly)
|
|
216
|
+
dncTokens.push('dnc:sms');
|
|
217
|
+
// Multiple specific channels = union (on email OR sms DNC); one token if only one is set.
|
|
218
|
+
if (dncTokens.length === 1)
|
|
219
|
+
searchParts.push(dncTokens[0]);
|
|
220
|
+
else if (dncTokens.length > 1)
|
|
221
|
+
searchParts.push(dncTokens.join(' OR '));
|
|
200
222
|
}
|
|
201
223
|
// Stage filter (always OR for multiple stages - a contact can only be in one stage)
|
|
202
224
|
const stages = options.stages;
|
|
@@ -241,23 +263,19 @@ async function getAllContacts(context, itemIndex) {
|
|
|
241
263
|
}
|
|
242
264
|
}
|
|
243
265
|
let responseData;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const useDncPostFilter = emailDncOnly || smsDncOnly || anyDncOnly;
|
|
248
|
-
if (useDncPostFilter) {
|
|
266
|
+
if (ownerIdFilter) {
|
|
267
|
+
// Owner filter has no server-side equivalent, so page through the (already server-filtered)
|
|
268
|
+
// results and keep only those whose owner.id matches, until the limit is met or results run out.
|
|
249
269
|
const limit = returnAll ? undefined : (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
|
|
250
|
-
responseData = await
|
|
270
|
+
responseData = await getContactsWithClientFilter(context, qs, (contact) => ownerIdFilter.has(Number(contact?.owner?.id)), limit);
|
|
271
|
+
}
|
|
272
|
+
else if (returnAll) {
|
|
273
|
+
responseData = await (0, ApiHelpers_1.makePaginatedRequest)(context, 'contacts', 'GET', '/contacts', {}, qs);
|
|
251
274
|
}
|
|
252
275
|
else {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
else {
|
|
257
|
-
qs.limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
|
|
258
|
-
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, qs);
|
|
259
|
-
responseData = response.contacts ? Object.values(response.contacts) : [];
|
|
260
|
-
}
|
|
276
|
+
qs.limit = (0, ApiHelpers_1.getOptionalParam)(context, 'limit', itemIndex, 30);
|
|
277
|
+
const response = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, qs);
|
|
278
|
+
responseData = response.contacts ? Object.values(response.contacts) : [];
|
|
261
279
|
}
|
|
262
280
|
const processedData = (0, DataHelpers_1.processContactFields)(responseData, options, options.fieldsToReturn);
|
|
263
281
|
return (0, DataHelpers_1.convertNumericStrings)(processedData);
|
|
@@ -620,34 +638,22 @@ function addContactFields(body, fields) {
|
|
|
620
638
|
Object.assign(body, data);
|
|
621
639
|
}
|
|
622
640
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
641
|
+
// Page through /contacts (already narrowed by any server-side search in `qs`) and keep only the
|
|
642
|
+
// contacts that satisfy `predicate`, stopping once `limit` matches are collected or results run out.
|
|
643
|
+
// Used for filters that have no server-side equivalent (e.g. owner-by-id).
|
|
644
|
+
async function getContactsWithClientFilter(context, qs, predicate, limit) {
|
|
627
645
|
const requestedStart = Number(qs.start ?? 0);
|
|
628
646
|
const maxResults = typeof limit === 'number' && limit > 0 ? Math.floor(limit) : undefined;
|
|
629
647
|
const contacts = [];
|
|
630
648
|
let remaining = maxResults;
|
|
631
649
|
let currentStart = Number.isFinite(requestedStart) && requestedStart > 0 ? requestedStart : 0;
|
|
632
650
|
while (remaining === undefined || remaining > 0) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
: Math.min(remaining, GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE);
|
|
651
|
+
// Always fetch full pages so the scan terminates promptly; `remaining` only caps what we keep.
|
|
652
|
+
const pageLimit = GenericFunctions_1.DEFAULT_MAUTIC_PAGE_SIZE;
|
|
636
653
|
const pageQs = { ...qs, start: currentStart, limit: pageLimit };
|
|
637
654
|
const pageResponse = await (0, ApiHelpers_1.makeApiRequest)(context, 'GET', '/contacts', {}, pageQs);
|
|
638
655
|
const pageContacts = pageResponse.contacts ? Object.values(pageResponse.contacts) : [];
|
|
639
|
-
const filtered = pageContacts.filter(
|
|
640
|
-
const dnc = contact.doNotContact || [];
|
|
641
|
-
const hasEmailDnc = dnc.some((d) => d.channel === 'email');
|
|
642
|
-
const hasSmsDnc = dnc.some((d) => d.channel === 'sms');
|
|
643
|
-
if (emailDncOnly)
|
|
644
|
-
return hasEmailDnc;
|
|
645
|
-
if (smsDncOnly)
|
|
646
|
-
return hasSmsDnc;
|
|
647
|
-
if (anyDncOnly)
|
|
648
|
-
return hasEmailDnc || hasSmsDnc;
|
|
649
|
-
return true;
|
|
650
|
-
});
|
|
656
|
+
const filtered = pageContacts.filter(predicate);
|
|
651
657
|
const toAdd = remaining === undefined ? filtered : filtered.slice(0, remaining);
|
|
652
658
|
contacts.push(...toAdd);
|
|
653
659
|
if (remaining !== undefined) {
|
package/package.json
CHANGED