@ixiam/n8n-nodes-civicrm 3.2.4 → 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
- const credentials = await this.getCredentials('civiCrmApi');
26
- const baseUrl = String((credentials === null || credentials === void 0 ? void 0 : credentials.baseUrl) || 'default');
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 baseUrl = credentials.baseUrl.replace(/\/$/, '');
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
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixiam/n8n-nodes-civicrm",
3
- "version": "3.2.4",
3
+ "version": "3.3.0",
4
4
  "description": "Full-featured CiviCRM API v4 integration for n8n",
5
5
  "license": "MIT",
6
6
  "keywords": [