@ixiam/n8n-nodes-civicrm 3.2.3 → 3.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.
package/README.md
CHANGED
|
@@ -157,6 +157,21 @@ Option 2: Regenerate API Key (CiviCRM)
|
|
|
157
157
|
|
|
158
158
|
After entering credentials, click **Test credentials** to validate the connection.
|
|
159
159
|
|
|
160
|
+
### **Base URL Override (v3.3.0+)**
|
|
161
|
+
|
|
162
|
+
One credential can target many CiviCRM instances that share the same API key. Set the node's **Base URL Override** field, usually with an expression, and every request for that item goes to that URL instead of the credential's Base URL.
|
|
163
|
+
|
|
164
|
+
| Field | Description |
|
|
165
|
+
|-------|-------------|
|
|
166
|
+
| **Base URL Override** | Optional. Site root, without `/civicrm`. Example: `={{ $json.instance_url }}` |
|
|
167
|
+
|
|
168
|
+
- Normalized per item: trimmed, trailing slashes removed, `https://` added when no scheme is given. Only `http://` and `https://` are accepted; anything else fails that item (honors *Continue On Fail*).
|
|
169
|
+
- Applies to every request for the item, including sub-requests, the JWT flow and the API key fallback. JWT and contact ID caches are keyed by the effective URL, so a token from one instance is never sent to another.
|
|
170
|
+
- Empty (the default, and every workflow saved before 3.3.0): the credential's Base URL is used, exactly as before.
|
|
171
|
+
- Dropdowns in the editor (`loadOptions`) have no item context and always use the credential's Base URL.
|
|
172
|
+
|
|
173
|
+
> ⚠️ The credential's API key is sent to whatever URL the override resolves to. Only feed it URLs your workflow controls (a fixed list, or a source only trusted people can edit).
|
|
174
|
+
|
|
160
175
|
---
|
|
161
176
|
|
|
162
177
|
## 📦 Supported Entities
|
|
@@ -21,16 +21,21 @@ function normalizeLocationKey(s) {
|
|
|
21
21
|
.toLowerCase()
|
|
22
22
|
.replace(/[^a-z0-9]+/g, '');
|
|
23
23
|
}
|
|
24
|
-
async function getLocationTypeMap() {
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
async function getLocationTypeMap(baseUrlOverride) {
|
|
25
|
+
// Cache keyed by the effective base URL (override if set, else credential)
|
|
26
|
+
// so location types of one CiviCRM instance are never reused for another.
|
|
27
|
+
let baseUrl = baseUrlOverride;
|
|
28
|
+
if (!baseUrl) {
|
|
29
|
+
const credentials = await this.getCredentials('civiCrmApi');
|
|
30
|
+
baseUrl = String((credentials === null || credentials === void 0 ? void 0 : credentials.baseUrl) || 'default');
|
|
31
|
+
}
|
|
27
32
|
if (locationTypeCache[baseUrl])
|
|
28
33
|
return locationTypeCache[baseUrl];
|
|
29
34
|
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/OptionValue/get', {
|
|
30
35
|
where: [['option_group_id:name', '=', 'location_type']],
|
|
31
36
|
select: ['name', 'label'],
|
|
32
37
|
limit: 0,
|
|
33
|
-
});
|
|
38
|
+
}, undefined, baseUrlOverride);
|
|
34
39
|
const map = {};
|
|
35
40
|
const values = ((res === null || res === void 0 ? void 0 : res.values) || []);
|
|
36
41
|
for (const v of values) {
|
|
@@ -708,6 +713,21 @@ class CiviCrm {
|
|
|
708
713
|
},
|
|
709
714
|
},
|
|
710
715
|
//
|
|
716
|
+
// BASE URL OVERRIDE (per item: one credential / API key shared by
|
|
717
|
+
// several CiviCRM instances - see GenericFunctions.civicrmApiRequest)
|
|
718
|
+
//
|
|
719
|
+
{
|
|
720
|
+
displayName: 'Base URL Override',
|
|
721
|
+
name: 'baseUrlOverride',
|
|
722
|
+
type: 'string',
|
|
723
|
+
default: '',
|
|
724
|
+
placeholder: 'https://example.org',
|
|
725
|
+
description: "Optional. Replaces the credential's Base URL for this item (site root, without /civicrm). " +
|
|
726
|
+
'Useful for many instances sharing the same API key. Can be set by expression, e.g. ' +
|
|
727
|
+
'={{ $json.instance_url }}. If no scheme is given, https:// is assumed; only http(s) is allowed. ' +
|
|
728
|
+
"Leave empty to use the credential's Base URL.",
|
|
729
|
+
},
|
|
730
|
+
//
|
|
711
731
|
// DYNAMIC FIELDS
|
|
712
732
|
//
|
|
713
733
|
...generic_1.genericFields,
|
|
@@ -764,6 +784,11 @@ class CiviCrm {
|
|
|
764
784
|
// actual CiviCRM permissions, defeating the point of issue #25 for
|
|
765
785
|
// every write operation.
|
|
766
786
|
const runtimeBearerToken = this.getNodeParameter('runtimeBearerToken', i, '').trim() || undefined;
|
|
787
|
+
// Per-item Base URL Override: same credential/API key, different
|
|
788
|
+
// CiviCRM instance per item. Normalized/validated here (inside the
|
|
789
|
+
// per-item try so continueOnFail applies) and passed to every
|
|
790
|
+
// civicrmApiRequest call for this item. Empty -> credential Base URL.
|
|
791
|
+
const baseUrlOverride = (0, GenericFunctions_1.normalizeBaseUrlOverride)(this.getNode(), this.getNodeParameter('baseUrlOverride', i, ''), i);
|
|
767
792
|
// Custom API resource: raw passthrough, plus first-class List Fields /
|
|
768
793
|
// Dynamic Search operations for any API4 entity (not just the 5 fixed
|
|
769
794
|
// resources above).
|
|
@@ -778,7 +803,7 @@ class CiviCrm {
|
|
|
778
803
|
-------------------------------------------------------- */
|
|
779
804
|
if (customOperation === 'getFields') {
|
|
780
805
|
const actionContext = this.getNodeParameter('getFieldsAction', i, 'get');
|
|
781
|
-
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/getFields`, { action: actionContext, loadOptions: true }, runtimeBearerToken);
|
|
806
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/getFields`, { action: actionContext, loadOptions: true }, runtimeBearerToken, baseUrlOverride);
|
|
782
807
|
const vals = ((_a = res === null || res === void 0 ? void 0 : res.values) !== null && _a !== void 0 ? _a : []);
|
|
783
808
|
if (Array.isArray(vals) && vals.length) {
|
|
784
809
|
for (const v of vals) {
|
|
@@ -821,7 +846,7 @@ class CiviCrm {
|
|
|
821
846
|
const page = 500;
|
|
822
847
|
let hasMore = true;
|
|
823
848
|
while (hasMore) {
|
|
824
|
-
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/get`, { select, where, limit: page, offset }, runtimeBearerToken);
|
|
849
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/get`, { select, where, limit: page, offset }, runtimeBearerToken, baseUrlOverride);
|
|
825
850
|
const vals = ((_c = r === null || r === void 0 ? void 0 : r.values) !== null && _c !== void 0 ? _c : []);
|
|
826
851
|
for (const v of vals) {
|
|
827
852
|
out.push({ json: v, pairedItem: { item: i } });
|
|
@@ -835,7 +860,7 @@ class CiviCrm {
|
|
|
835
860
|
}
|
|
836
861
|
}
|
|
837
862
|
else {
|
|
838
|
-
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/get`, { select, where, limit }, runtimeBearerToken);
|
|
863
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/get`, { select, where, limit }, runtimeBearerToken, baseUrlOverride);
|
|
839
864
|
const vals = ((_d = r === null || r === void 0 ? void 0 : r.values) !== null && _d !== void 0 ? _d : []);
|
|
840
865
|
for (const v of vals) {
|
|
841
866
|
out.push({ json: v, pairedItem: { item: i } });
|
|
@@ -857,7 +882,7 @@ class CiviCrm {
|
|
|
857
882
|
throw new Error('Invalid JSON in "Params (JSON)"');
|
|
858
883
|
}
|
|
859
884
|
}
|
|
860
|
-
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/${action}`, params, runtimeBearerToken);
|
|
885
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${customEntity}/${action}`, params, runtimeBearerToken, baseUrlOverride);
|
|
861
886
|
// Return the raw API4 response so advanced users can work with values and metadata
|
|
862
887
|
out.push({
|
|
863
888
|
json: res,
|
|
@@ -906,7 +931,7 @@ class CiviCrm {
|
|
|
906
931
|
limit: 1,
|
|
907
932
|
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
908
933
|
};
|
|
909
|
-
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params, runtimeBearerToken);
|
|
934
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params, runtimeBearerToken, baseUrlOverride);
|
|
910
935
|
out.push({
|
|
911
936
|
json: (_f = (_e = res === null || res === void 0 ? void 0 : res.values) === null || _e === void 0 ? void 0 : _e[0]) !== null && _f !== void 0 ? _f : {},
|
|
912
937
|
pairedItem: { item: i },
|
|
@@ -964,7 +989,7 @@ class CiviCrm {
|
|
|
964
989
|
const page = 500;
|
|
965
990
|
let hasMore = true;
|
|
966
991
|
while (hasMore) {
|
|
967
|
-
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit: page, offset }, runtimeBearerToken);
|
|
992
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit: page, offset }, runtimeBearerToken, baseUrlOverride);
|
|
968
993
|
const vals = (_g = r === null || r === void 0 ? void 0 : r.values) !== null && _g !== void 0 ? _g : [];
|
|
969
994
|
for (const v of vals) {
|
|
970
995
|
out.push({
|
|
@@ -981,7 +1006,7 @@ class CiviCrm {
|
|
|
981
1006
|
}
|
|
982
1007
|
}
|
|
983
1008
|
else {
|
|
984
|
-
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit }, runtimeBearerToken);
|
|
1009
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit }, runtimeBearerToken, baseUrlOverride);
|
|
985
1010
|
const vals = (_h = r === null || r === void 0 ? void 0 : r.values) !== null && _h !== void 0 ? _h : [];
|
|
986
1011
|
for (const v of vals) {
|
|
987
1012
|
out.push({
|
|
@@ -999,7 +1024,7 @@ class CiviCrm {
|
|
|
999
1024
|
const id = this.getNodeParameter('id', i);
|
|
1000
1025
|
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/delete`, {
|
|
1001
1026
|
where: [['id', '=', id]],
|
|
1002
|
-
}, runtimeBearerToken);
|
|
1027
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1003
1028
|
out.push({
|
|
1004
1029
|
json: {
|
|
1005
1030
|
success: true,
|
|
@@ -1023,7 +1048,7 @@ class CiviCrm {
|
|
|
1023
1048
|
const addressData = {};
|
|
1024
1049
|
let locationTypeMap = {};
|
|
1025
1050
|
if (resource === 'contact') {
|
|
1026
|
-
locationTypeMap = await getLocationTypeMap.call(this);
|
|
1051
|
+
locationTypeMap = await getLocationTypeMap.call(this, baseUrlOverride);
|
|
1027
1052
|
}
|
|
1028
1053
|
for (const p of pairs) {
|
|
1029
1054
|
if (!p.fieldName)
|
|
@@ -1135,7 +1160,7 @@ class CiviCrm {
|
|
|
1135
1160
|
/* CREATE/UPDATE contact */
|
|
1136
1161
|
let contactId = id;
|
|
1137
1162
|
if (isCreate) {
|
|
1138
|
-
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/create`, { values }, runtimeBearerToken);
|
|
1163
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/create`, { values }, runtimeBearerToken, baseUrlOverride);
|
|
1139
1164
|
contactId = (_k = (_j = r === null || r === void 0 ? void 0 : r.values) === null || _j === void 0 ? void 0 : _j[0]) === null || _k === void 0 ? void 0 : _k.id;
|
|
1140
1165
|
if (!contactId)
|
|
1141
1166
|
throw new Error('Failed to create contact.');
|
|
@@ -1144,7 +1169,7 @@ class CiviCrm {
|
|
|
1144
1169
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, {
|
|
1145
1170
|
values,
|
|
1146
1171
|
where: [['id', '=', contactId]],
|
|
1147
|
-
}, runtimeBearerToken);
|
|
1172
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1148
1173
|
}
|
|
1149
1174
|
/* SUBENTITIES */
|
|
1150
1175
|
if (resource === 'contact') {
|
|
@@ -1154,7 +1179,7 @@ class CiviCrm {
|
|
|
1154
1179
|
['contact_id', '=', contactId],
|
|
1155
1180
|
['is_primary', '=', true],
|
|
1156
1181
|
],
|
|
1157
|
-
}, runtimeBearerToken);
|
|
1182
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1158
1183
|
}
|
|
1159
1184
|
if (isCreate && isPrimaryPhone) {
|
|
1160
1185
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/delete', {
|
|
@@ -1162,7 +1187,7 @@ class CiviCrm {
|
|
|
1162
1187
|
['contact_id', '=', contactId],
|
|
1163
1188
|
['is_primary', '=', true],
|
|
1164
1189
|
],
|
|
1165
|
-
}, runtimeBearerToken);
|
|
1190
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1166
1191
|
}
|
|
1167
1192
|
if (isCreate && isPrimaryAddress) {
|
|
1168
1193
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/delete', {
|
|
@@ -1170,7 +1195,7 @@ class CiviCrm {
|
|
|
1170
1195
|
['contact_id', '=', contactId],
|
|
1171
1196
|
['is_primary', '=', true],
|
|
1172
1197
|
],
|
|
1173
|
-
}, runtimeBearerToken);
|
|
1198
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1174
1199
|
}
|
|
1175
1200
|
if (Object.keys(emailData).length) {
|
|
1176
1201
|
if (isCreate) {
|
|
@@ -1181,7 +1206,7 @@ class CiviCrm {
|
|
|
1181
1206
|
is_primary: isPrimaryEmail,
|
|
1182
1207
|
'location_type_id:name': emailLocationName,
|
|
1183
1208
|
},
|
|
1184
|
-
}, runtimeBearerToken);
|
|
1209
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1185
1210
|
}
|
|
1186
1211
|
else {
|
|
1187
1212
|
const existingEmail = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/get', {
|
|
@@ -1191,7 +1216,7 @@ class CiviCrm {
|
|
|
1191
1216
|
],
|
|
1192
1217
|
limit: 1,
|
|
1193
1218
|
select: ['id'],
|
|
1194
|
-
}, runtimeBearerToken);
|
|
1219
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1195
1220
|
const existingEmailId = (_m = (_l = existingEmail === null || existingEmail === void 0 ? void 0 : existingEmail.values) === null || _l === void 0 ? void 0 : _l[0]) === null || _m === void 0 ? void 0 : _m.id;
|
|
1196
1221
|
if (existingEmailId) {
|
|
1197
1222
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/update', {
|
|
@@ -1201,7 +1226,7 @@ class CiviCrm {
|
|
|
1201
1226
|
contact_id: contactId,
|
|
1202
1227
|
is_primary: isPrimaryEmail,
|
|
1203
1228
|
},
|
|
1204
|
-
}, runtimeBearerToken);
|
|
1229
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1205
1230
|
}
|
|
1206
1231
|
else {
|
|
1207
1232
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
|
|
@@ -1211,7 +1236,7 @@ class CiviCrm {
|
|
|
1211
1236
|
is_primary: isPrimaryEmail,
|
|
1212
1237
|
'location_type_id:name': emailLocationName,
|
|
1213
1238
|
},
|
|
1214
|
-
}, runtimeBearerToken);
|
|
1239
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1215
1240
|
}
|
|
1216
1241
|
}
|
|
1217
1242
|
}
|
|
@@ -1224,7 +1249,7 @@ class CiviCrm {
|
|
|
1224
1249
|
is_primary: isPrimaryPhone,
|
|
1225
1250
|
'location_type_id:name': phoneLocationName,
|
|
1226
1251
|
},
|
|
1227
|
-
}, runtimeBearerToken);
|
|
1252
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1228
1253
|
}
|
|
1229
1254
|
else {
|
|
1230
1255
|
const existingPhone = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/get', {
|
|
@@ -1234,7 +1259,7 @@ class CiviCrm {
|
|
|
1234
1259
|
],
|
|
1235
1260
|
limit: 1,
|
|
1236
1261
|
select: ['id'],
|
|
1237
|
-
}, runtimeBearerToken);
|
|
1262
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1238
1263
|
const existingPhoneId = (_p = (_o = existingPhone === null || existingPhone === void 0 ? void 0 : existingPhone.values) === null || _o === void 0 ? void 0 : _o[0]) === null || _p === void 0 ? void 0 : _p.id;
|
|
1239
1264
|
if (existingPhoneId) {
|
|
1240
1265
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/update', {
|
|
@@ -1244,7 +1269,7 @@ class CiviCrm {
|
|
|
1244
1269
|
contact_id: contactId,
|
|
1245
1270
|
is_primary: isPrimaryPhone,
|
|
1246
1271
|
},
|
|
1247
|
-
}, runtimeBearerToken);
|
|
1272
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1248
1273
|
}
|
|
1249
1274
|
else {
|
|
1250
1275
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
|
|
@@ -1254,7 +1279,7 @@ class CiviCrm {
|
|
|
1254
1279
|
is_primary: isPrimaryPhone,
|
|
1255
1280
|
'location_type_id:name': phoneLocationName,
|
|
1256
1281
|
},
|
|
1257
|
-
}, runtimeBearerToken);
|
|
1282
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1258
1283
|
}
|
|
1259
1284
|
}
|
|
1260
1285
|
}
|
|
@@ -1267,7 +1292,7 @@ class CiviCrm {
|
|
|
1267
1292
|
is_primary: isPrimaryAddress,
|
|
1268
1293
|
'location_type_id:name': addressLocationName,
|
|
1269
1294
|
},
|
|
1270
|
-
}, runtimeBearerToken);
|
|
1295
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1271
1296
|
}
|
|
1272
1297
|
else {
|
|
1273
1298
|
const existingAddress = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/get', {
|
|
@@ -1277,7 +1302,7 @@ class CiviCrm {
|
|
|
1277
1302
|
],
|
|
1278
1303
|
limit: 1,
|
|
1279
1304
|
select: ['id'],
|
|
1280
|
-
}, runtimeBearerToken);
|
|
1305
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1281
1306
|
const existingAddressId = (_r = (_q = existingAddress === null || existingAddress === void 0 ? void 0 : existingAddress.values) === null || _q === void 0 ? void 0 : _q[0]) === null || _r === void 0 ? void 0 : _r.id;
|
|
1282
1307
|
if (existingAddressId) {
|
|
1283
1308
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/update', {
|
|
@@ -1287,7 +1312,7 @@ class CiviCrm {
|
|
|
1287
1312
|
contact_id: contactId,
|
|
1288
1313
|
is_primary: isPrimaryAddress,
|
|
1289
1314
|
},
|
|
1290
|
-
}, runtimeBearerToken);
|
|
1315
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1291
1316
|
}
|
|
1292
1317
|
else {
|
|
1293
1318
|
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
|
|
@@ -1297,7 +1322,7 @@ class CiviCrm {
|
|
|
1297
1322
|
is_primary: isPrimaryAddress,
|
|
1298
1323
|
'location_type_id:name': addressLocationName,
|
|
1299
1324
|
},
|
|
1300
|
-
}, runtimeBearerToken);
|
|
1325
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1301
1326
|
}
|
|
1302
1327
|
}
|
|
1303
1328
|
}
|
|
@@ -1326,7 +1351,7 @@ class CiviCrm {
|
|
|
1326
1351
|
},
|
|
1327
1352
|
}
|
|
1328
1353
|
: {}),
|
|
1329
|
-
}, runtimeBearerToken);
|
|
1354
|
+
}, runtimeBearerToken, baseUrlOverride);
|
|
1330
1355
|
out.push({
|
|
1331
1356
|
json: (_t = (_s = res === null || res === void 0 ? void 0 : res.values) === null || _s === void 0 ? void 0 : _s[0]) !== null && _t !== void 0 ? _t : {},
|
|
1332
1357
|
pairedItem: { item: i },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeBaseUrlOverride = normalizeBaseUrlOverride;
|
|
3
4
|
exports.buildCiviAuthHeaders = buildCiviAuthHeaders;
|
|
4
5
|
exports.civicrmApiRequest = civicrmApiRequest;
|
|
5
6
|
exports.api4 = api4;
|
|
@@ -37,6 +38,46 @@ function getHttpErrorDetails(error) {
|
|
|
37
38
|
}
|
|
38
39
|
return status ? `${message} | HTTP ${status}` : message;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Normalizes the optional per-item "Base URL Override" parameter.
|
|
43
|
+
*
|
|
44
|
+
* - Empty / whitespace-only -> `undefined` (caller uses the credential's Base URL).
|
|
45
|
+
* - Trims, strips trailing slashes, prepends `https://` when no scheme is given.
|
|
46
|
+
* - Rejects any scheme other than http/https, and anything that does not parse
|
|
47
|
+
* as a URL with a host, with a NodeOperationError.
|
|
48
|
+
*
|
|
49
|
+
* Idempotent: normalizing an already-normalized value returns the same string,
|
|
50
|
+
* so it is safe to call both in the node and in the transport.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeBaseUrlOverride(node, rawValue, itemIndex) {
|
|
53
|
+
if (rawValue === undefined || rawValue === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
let value = String(rawValue).trim();
|
|
56
|
+
if (!value)
|
|
57
|
+
return undefined;
|
|
58
|
+
value = value.replace(/\/+$/, '');
|
|
59
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):\/\//i.exec(value);
|
|
60
|
+
if (schemeMatch) {
|
|
61
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
62
|
+
if (scheme !== 'http' && scheme !== 'https') {
|
|
63
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Invalid Base URL Override "${value}": only http:// and https:// URLs are allowed (got "${scheme}://").`, { itemIndex });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
value = `https://${value}`;
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = new URL(value);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Invalid Base URL Override "${String(rawValue).trim()}": not a valid URL. Expected the site root, e.g. https://example.org`, { itemIndex });
|
|
75
|
+
}
|
|
76
|
+
if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') || !parsed.hostname) {
|
|
77
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Invalid Base URL Override "${String(rawValue).trim()}": only http:// and https:// URLs with a host are allowed.`, { itemIndex });
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
40
81
|
function buildCiviAuthHeaders(credentials, baseUrl, jwtToken) {
|
|
41
82
|
const headers = {
|
|
42
83
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -64,11 +105,20 @@ function buildCiviAuthHeaders(credentials, baseUrl, jwtToken) {
|
|
|
64
105
|
* (JWT auto-resolve via `getServerIssuedJwt`/`resolveContactId`, or the
|
|
65
106
|
* empty-response/error fallback to the credential's API key) runs at all.
|
|
66
107
|
* See the early-return block right below for why.
|
|
108
|
+
*
|
|
109
|
+
* `baseUrlOverride` (optional) replaces the credential's Base URL for this
|
|
110
|
+
* call (per-item "Base URL Override" parameter), so a single credential/API key
|
|
111
|
+
* can target several CiviCRM instances. It is used for every request made by
|
|
112
|
+
* this call, including the JWT paths (Contact/get auto-resolve,
|
|
113
|
+
* AuthxCredential/create) and the API key fallback. The JWT caches in JwtAuth
|
|
114
|
+
* are keyed by this effective base URL, so a token issued by one instance is
|
|
115
|
+
* never sent to another. Empty/undefined -> credential Base URL, unchanged.
|
|
67
116
|
*/
|
|
68
|
-
async function civicrmApiRequest(method, path, body, runtimeBearerToken) {
|
|
117
|
+
async function civicrmApiRequest(method, path, body, runtimeBearerToken, baseUrlOverride) {
|
|
69
118
|
var _a, _b, _c, _d;
|
|
70
119
|
const credentials = (await this.getCredentials('civiCrmApi'));
|
|
71
|
-
const
|
|
120
|
+
const normalizedOverride = normalizeBaseUrlOverride(this.getNode(), baseUrlOverride);
|
|
121
|
+
const baseUrl = normalizedOverride !== null && normalizedOverride !== void 0 ? normalizedOverride : credentials.baseUrl.replace(/\/$/, '');
|
|
72
122
|
const apiToken = credentials.apiToken;
|
|
73
123
|
// Runtime bearer token path: used exactly as given, with no fallback.
|
|
74
124
|
//
|
|
@@ -81,11 +131,18 @@ async function civicrmApiRequest(method, path, body, runtimeBearerToken) {
|
|
|
81
131
|
// path below has (empty JWT response -> retry with the plaintext API key),
|
|
82
132
|
// and it is exactly what must NOT happen here, so this path never falls
|
|
83
133
|
// through into that logic.
|
|
134
|
+
//
|
|
135
|
+
// The token is sent with the same header(s) as the credential's JWT path
|
|
136
|
+
// (`jwtHeaderMode`, default `xheader` = X-Civi-Auth). A hardcoded
|
|
137
|
+
// `Authorization` header is not enough: many nginx + PHP-FPM setups do not
|
|
138
|
+
// pass it to PHP, so CiviCRM sees an unauthenticated request and rejects it
|
|
139
|
+
// ("SECURITY ALERT: Ajax requests can only be issued by javascript
|
|
140
|
+
// clients"). X-Civi-Auth always reaches CiviCRM.
|
|
84
141
|
if (runtimeBearerToken) {
|
|
85
142
|
const headers = {
|
|
86
143
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
87
|
-
Authorization: `Bearer ${runtimeBearerToken}`,
|
|
88
144
|
};
|
|
145
|
+
applyJwtHeaders(headers, runtimeBearerToken, getJwtHeaderMode(credentials));
|
|
89
146
|
const options = {
|
|
90
147
|
method,
|
|
91
148
|
url: `${baseUrl}${path}`,
|