@ixiam/n8n-nodes-civicrm 0.4.4 → 1.0.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 +105 -8
- package/credentials/CiviCrmApi.credentials.ts +50 -53
- package/dist/credentials/CiviCrmApi.credentials.d.ts +1 -5
- package/dist/credentials/CiviCrmApi.credentials.js +7 -10
- package/dist/nodes/CiviCrm/CiviCrm.node.d.ts +5 -5
- package/dist/nodes/CiviCrm/CiviCrm.node.js +302 -336
- package/dist/nodes/CiviCrm/descriptions/resources.js +11 -5
- package/jest.config.cjs +9 -0
- package/nodes/CiviCrm/CiviCrm.node.ts +385 -449
- package/nodes/CiviCrm/descriptions/resources.ts +11 -5
- package/package.json +21 -7
- package/test/helpers/mockServer.ts +27 -0
- package/test/nodes/CiviCrm.test.ts +67 -0
- package/test/tsconfig.json +10 -0
|
@@ -6,22 +6,14 @@ const resources_1 = require("./descriptions/resources");
|
|
|
6
6
|
const generic_1 = require("./descriptions/generic");
|
|
7
7
|
const ENTITY_MAP = {
|
|
8
8
|
contact: 'Contact',
|
|
9
|
-
event: 'Event',
|
|
10
|
-
case: 'Case',
|
|
11
|
-
contribution: 'Contribution',
|
|
12
9
|
membership: 'Membership',
|
|
13
|
-
participant: 'Participant',
|
|
14
10
|
group: 'Group',
|
|
15
11
|
relationship: 'Relationship',
|
|
16
|
-
email: 'Email',
|
|
17
12
|
activity: 'Activity',
|
|
18
13
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* key: normalizado (lowercase, sin espacios/ símbolos)
|
|
23
|
-
* value: option_value.name (para usar en location_type_id:name)
|
|
24
|
-
*/
|
|
14
|
+
/* ============================================================================
|
|
15
|
+
LOCATION TYPE CACHE
|
|
16
|
+
============================================================================ */
|
|
25
17
|
let locationTypeCache = null;
|
|
26
18
|
function normalizeLocationKey(s) {
|
|
27
19
|
return String(s || '')
|
|
@@ -36,24 +28,24 @@ async function getLocationTypeMap() {
|
|
|
36
28
|
select: ['name', 'label'],
|
|
37
29
|
limit: 0,
|
|
38
30
|
});
|
|
39
|
-
const values = (res?.values || []);
|
|
40
31
|
const map = {};
|
|
32
|
+
const values = (res?.values || []);
|
|
41
33
|
for (const v of values) {
|
|
42
34
|
const name = v.name || '';
|
|
43
35
|
const label = v.label || '';
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
map[
|
|
48
|
-
if (
|
|
49
|
-
map[
|
|
36
|
+
const k1 = normalizeLocationKey(name);
|
|
37
|
+
const k2 = normalizeLocationKey(label);
|
|
38
|
+
if (k1)
|
|
39
|
+
map[k1] = name;
|
|
40
|
+
if (k2)
|
|
41
|
+
map[k2] = name;
|
|
50
42
|
}
|
|
51
43
|
locationTypeCache = map;
|
|
52
44
|
return map;
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*/
|
|
46
|
+
/* ============================================================================
|
|
47
|
+
NODE DEFINITION
|
|
48
|
+
============================================================================ */
|
|
57
49
|
class CiviCrm {
|
|
58
50
|
constructor() {
|
|
59
51
|
this.description = {
|
|
@@ -62,43 +54,23 @@ class CiviCrm {
|
|
|
62
54
|
icon: 'file:civicrm.svg',
|
|
63
55
|
group: ['transform'],
|
|
64
56
|
version: 1,
|
|
65
|
-
description: 'Interact with CiviCRM API v4 (Civi-Go compatible)
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
' Example prefixes (from Location Types): Home, Work, Billing, Mobile, Oficina, etc.\\n' +
|
|
70
|
-
'- Prefix format: <location-prefix>.<entity>[.<subfield>]\\n' +
|
|
71
|
-
' Examples:\\n' +
|
|
72
|
-
' email = test@ixiam.com\\n' +
|
|
73
|
-
' phone = 600123123\\n' +
|
|
74
|
-
' address.city = Barcelona\\n' +
|
|
75
|
-
' work.email = test@company.com (Location Type = Work)\\n' +
|
|
76
|
-
' billing.address.postal_code = 28010 (Location Type = Billing)\\n' +
|
|
77
|
-
' home.phone.phone_type_id = 2 (Location Type = Home)\\n\\n' +
|
|
78
|
-
'Subfields examples:\\n' +
|
|
79
|
-
'- email.is_primary, email.signature_html, email.signature_text\\n' +
|
|
80
|
-
'- phone.phone_type_id, phone.phone_ext\\n' +
|
|
81
|
-
'- address.country_id, address.state_province_id, address.postal_code, address.street_address\\n\\n' +
|
|
82
|
-
'Prefixes and Location Types:\\n' +
|
|
83
|
-
'- The prefix (e.g. work, home, billing, oficina) is matched against CiviCRM Location Types (name/label).\\n' +
|
|
84
|
-
'- If the prefix matches, the node sets location_type_id:name automatically.\\n' +
|
|
85
|
-
'- If no prefix is used (e.g. email, phone, address.city), the node uses the Location Type selectors.\\n\\n' +
|
|
86
|
-
'birth_date:\\n' +
|
|
87
|
-
'- Accepted formats: YYYY-MM-DD, DD/MM/YYYY, DD-MM-YYYY, YYYY/MM/DD, YYYY.MM.DD.\\n' +
|
|
88
|
-
'- The node normalizes and validates the date before sending it to CiviCRM.\\n\\n' +
|
|
89
|
-
'Filters in GET MANY:\\n' +
|
|
90
|
-
'- Use API4 "where" JSON array format, e.g.:\\n' +
|
|
91
|
-
' [ ["contact_type","=","Individual"] ]\\n' +
|
|
92
|
-
' [ ["first_name","LIKE","Jul%"] ]\\n' +
|
|
93
|
-
' [ ["birth_date",">","1990-01-01"] ]\\n' +
|
|
94
|
-
' [ ["gender_id","IN",[1,2]] ]',
|
|
57
|
+
description: 'Interact with CiviCRM API v4 (Civi-Go compatible).\n\n' +
|
|
58
|
+
'Supports Contact, Membership, Group, Relationship and Activity entities.\n' +
|
|
59
|
+
'Includes dynamic mapping of email, phone, address and location types.\n' +
|
|
60
|
+
'Includes birth_date validation and JSON filters for GET MANY.\n',
|
|
95
61
|
defaults: { name: 'CiviCRM' },
|
|
96
62
|
inputs: ['main'],
|
|
97
63
|
outputs: ['main'],
|
|
98
64
|
credentials: [{ name: 'civiCrmApi', required: true }],
|
|
99
65
|
properties: [
|
|
66
|
+
//
|
|
67
|
+
// RESOURCE + OPERATION
|
|
68
|
+
//
|
|
100
69
|
resources_1.resourceProp,
|
|
101
70
|
resources_1.operationProp,
|
|
71
|
+
//
|
|
72
|
+
// CONTACT TYPE
|
|
73
|
+
//
|
|
102
74
|
{
|
|
103
75
|
displayName: 'Contact Type',
|
|
104
76
|
name: 'contactType',
|
|
@@ -110,9 +82,10 @@ class CiviCrm {
|
|
|
110
82
|
{ name: 'Household', value: 'Household' },
|
|
111
83
|
],
|
|
112
84
|
displayOptions: { show: { resource: ['contact'] } },
|
|
113
|
-
description: 'Contact type to filter (GET MANY) or assign (CREATE/UPDATE). ' +
|
|
114
|
-
'For GET MANY, if set, a condition [ "contact_type", "=", selectedType ] is added to the where.',
|
|
115
85
|
},
|
|
86
|
+
//
|
|
87
|
+
// LOCATION TYPES
|
|
88
|
+
//
|
|
116
89
|
{
|
|
117
90
|
displayName: 'Email Location Type',
|
|
118
91
|
name: 'emailLocation',
|
|
@@ -124,10 +97,6 @@ class CiviCrm {
|
|
|
124
97
|
{ name: 'Other', value: 'Other' },
|
|
125
98
|
],
|
|
126
99
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
127
|
-
description: 'Default Location Type used for email when no prefix is provided.\\n' +
|
|
128
|
-
'- If you use "email" or "email.xxx" as field name, this Location Type will be used.\\n' +
|
|
129
|
-
'- If you use a prefixed field like "work.email", "home.email", etc., ' +
|
|
130
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
131
100
|
},
|
|
132
101
|
{
|
|
133
102
|
displayName: 'Phone Location Type',
|
|
@@ -141,10 +110,6 @@ class CiviCrm {
|
|
|
141
110
|
{ name: 'Other', value: 'Other' },
|
|
142
111
|
],
|
|
143
112
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
144
|
-
description: 'Default Location Type used for phone when no prefix is provided.\\n' +
|
|
145
|
-
'- If you use "phone" or "phone.xxx" as field name, this Location Type will be used.\\n' +
|
|
146
|
-
'- If you use a prefixed field like "mobile.phone", "work.phone", etc., ' +
|
|
147
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
148
113
|
},
|
|
149
114
|
{
|
|
150
115
|
displayName: 'Address Location Type',
|
|
@@ -158,20 +123,20 @@ class CiviCrm {
|
|
|
158
123
|
{ name: 'Other', value: 'Other' },
|
|
159
124
|
],
|
|
160
125
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
161
|
-
description: 'Default Location Type used for address when no prefix is provided.\\n' +
|
|
162
|
-
'- If you use "address.city", "address.country_id", etc., this Location Type will be used.\\n' +
|
|
163
|
-
'- If you use a prefixed field like "billing.address.postal_code", "home.address.city", etc., ' +
|
|
164
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
165
126
|
},
|
|
127
|
+
//
|
|
128
|
+
// PRIMARY FLAG
|
|
129
|
+
//
|
|
166
130
|
{
|
|
167
131
|
displayName: 'Mark as Primary',
|
|
168
132
|
name: 'isPrimary',
|
|
169
133
|
type: 'boolean',
|
|
170
134
|
default: true,
|
|
171
135
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
172
|
-
description: 'If enabled, previous primary email/phone/address for this contact are deleted before creating the new ones. ' +
|
|
173
|
-
'Useful when you want to replace existing primary contact details.',
|
|
174
136
|
},
|
|
137
|
+
//
|
|
138
|
+
// ID for GET / UPDATE / DELETE
|
|
139
|
+
//
|
|
175
140
|
{
|
|
176
141
|
displayName: 'ID',
|
|
177
142
|
name: 'id',
|
|
@@ -179,25 +144,29 @@ class CiviCrm {
|
|
|
179
144
|
default: 0,
|
|
180
145
|
required: true,
|
|
181
146
|
displayOptions: { show: { operation: ['get', 'update', 'delete'] } },
|
|
182
|
-
description: 'Record ID of the selected entity (Contact, Event, Case, etc.).',
|
|
183
147
|
},
|
|
148
|
+
//
|
|
149
|
+
// DYNAMIC FIELDS
|
|
150
|
+
//
|
|
184
151
|
...generic_1.genericFields,
|
|
185
152
|
...generic_1.upsertFields,
|
|
186
153
|
],
|
|
187
154
|
};
|
|
155
|
+
/* ============================================================================
|
|
156
|
+
LOAD OPTIONS (required by n8n verification)
|
|
157
|
+
============================================================================ */
|
|
188
158
|
this.methods = {
|
|
189
159
|
loadOptions: {
|
|
190
|
-
async
|
|
160
|
+
async loadOptionValues() {
|
|
191
161
|
const { baseUrl, apiToken } = (await this.getCredentials('civiCrmApi'));
|
|
192
|
-
const url = `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`;
|
|
193
162
|
const res = await this.helpers.httpRequest({
|
|
194
163
|
method: 'POST',
|
|
195
|
-
url
|
|
164
|
+
url: `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`,
|
|
196
165
|
headers: {
|
|
197
166
|
'X-Civi-Auth': `Bearer ${apiToken}`,
|
|
198
167
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
199
168
|
},
|
|
200
|
-
body: { params: JSON.stringify({ limit:
|
|
169
|
+
body: { params: JSON.stringify({ limit: 50, select: ['id', 'label'] }) },
|
|
201
170
|
json: true,
|
|
202
171
|
});
|
|
203
172
|
const values = (res?.values || []);
|
|
@@ -209,6 +178,9 @@ class CiviCrm {
|
|
|
209
178
|
},
|
|
210
179
|
};
|
|
211
180
|
}
|
|
181
|
+
/* ============================================================================
|
|
182
|
+
EXECUTE
|
|
183
|
+
============================================================================ */
|
|
212
184
|
async execute() {
|
|
213
185
|
const items = this.getInputData();
|
|
214
186
|
const out = [];
|
|
@@ -220,17 +192,17 @@ class CiviCrm {
|
|
|
220
192
|
const phoneLocationParam = this.getNodeParameter('phoneLocation', i, 'Work');
|
|
221
193
|
const addressLocationParam = this.getNodeParameter('addressLocation', i, 'Home');
|
|
222
194
|
const isPrimary = this.getNodeParameter('isPrimary', i, true);
|
|
223
|
-
// Valores de location type efectivos (pueden ser sobreescritos por prefijo)
|
|
224
195
|
let emailLocationName = emailLocationParam;
|
|
225
196
|
let phoneLocationName = phoneLocationParam;
|
|
226
197
|
let addressLocationName = addressLocationParam;
|
|
227
|
-
|
|
198
|
+
/* ======================================================
|
|
199
|
+
GET
|
|
200
|
+
====================================================== */
|
|
228
201
|
if (operation === 'get') {
|
|
229
202
|
const id = this.getNodeParameter('id', i);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
where,
|
|
203
|
+
const params = resource === 'contact'
|
|
204
|
+
? {
|
|
205
|
+
where: [['id', '=', id]],
|
|
234
206
|
limit: 1,
|
|
235
207
|
select: [
|
|
236
208
|
'id',
|
|
@@ -247,34 +219,39 @@ class CiviCrm {
|
|
|
247
219
|
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
248
220
|
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
249
221
|
},
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
const params = {
|
|
256
|
-
where,
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
where: [['id', '=', id]],
|
|
257
225
|
limit: 1,
|
|
258
226
|
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
259
227
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
228
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
|
|
229
|
+
out.push({ json: res?.values?.[0] ?? {} });
|
|
230
|
+
continue;
|
|
263
231
|
}
|
|
264
|
-
|
|
232
|
+
/* ======================================================
|
|
233
|
+
GET MANY
|
|
234
|
+
====================================================== */
|
|
265
235
|
if (operation === 'getMany') {
|
|
266
|
-
const returnAll = this.getNodeParameter('returnAll', i, false);
|
|
267
236
|
const limit = this.getNodeParameter('limit', i, 100);
|
|
237
|
+
const returnAll = this.getNodeParameter('returnAll', i, false);
|
|
268
238
|
const whereJson = this.getNodeParameter('whereJson', i, '');
|
|
269
|
-
let where =
|
|
239
|
+
let where = [];
|
|
240
|
+
if (whereJson) {
|
|
241
|
+
try {
|
|
242
|
+
where = JSON.parse(whereJson);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new Error('Invalid JSON in whereJson');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
270
248
|
if (resource === 'contact') {
|
|
271
249
|
const contactType = this.getNodeParameter('contactType', i, '');
|
|
272
250
|
if (contactType)
|
|
273
251
|
where.push(['contact_type', '=', contactType]);
|
|
274
252
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
baseParams = {
|
|
253
|
+
const params = resource === 'contact'
|
|
254
|
+
? {
|
|
278
255
|
where,
|
|
279
256
|
select: [
|
|
280
257
|
'id',
|
|
@@ -291,20 +268,17 @@ class CiviCrm {
|
|
|
291
268
|
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
292
269
|
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
293
270
|
},
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
else {
|
|
297
|
-
baseParams = {
|
|
271
|
+
}
|
|
272
|
+
: {
|
|
298
273
|
where,
|
|
299
274
|
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
300
275
|
};
|
|
301
|
-
}
|
|
302
276
|
if (returnAll) {
|
|
303
277
|
let offset = 0;
|
|
304
278
|
const page = 500;
|
|
305
279
|
while (true) {
|
|
306
|
-
const
|
|
307
|
-
const vals =
|
|
280
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit: page, offset });
|
|
281
|
+
const vals = r?.values ?? [];
|
|
308
282
|
for (const v of vals)
|
|
309
283
|
out.push({ json: v });
|
|
310
284
|
if (vals.length < page)
|
|
@@ -313,262 +287,254 @@ class CiviCrm {
|
|
|
313
287
|
}
|
|
314
288
|
}
|
|
315
289
|
else {
|
|
316
|
-
const
|
|
317
|
-
const vals =
|
|
290
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit });
|
|
291
|
+
const vals = r?.values ?? [];
|
|
318
292
|
for (const v of vals)
|
|
319
293
|
out.push({ json: v });
|
|
320
294
|
}
|
|
295
|
+
continue;
|
|
321
296
|
}
|
|
322
|
-
|
|
297
|
+
/* ======================================================
|
|
298
|
+
DELETE
|
|
299
|
+
====================================================== */
|
|
300
|
+
if (operation === 'delete') {
|
|
301
|
+
const id = this.getNodeParameter('id', i);
|
|
302
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/delete`, {
|
|
303
|
+
where: [['id', '=', id]],
|
|
304
|
+
});
|
|
305
|
+
out.push({
|
|
306
|
+
json: {
|
|
307
|
+
success: true,
|
|
308
|
+
message: `${entity} ${id} deleted`,
|
|
309
|
+
deleted_id: id,
|
|
310
|
+
api_response: r,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
/* ======================================================
|
|
316
|
+
CREATE / UPDATE
|
|
317
|
+
====================================================== */
|
|
323
318
|
const isCreate = operation === 'create';
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let val = input.trim();
|
|
336
|
-
// YYYY-MM-DD
|
|
337
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(val))
|
|
338
|
-
return val;
|
|
339
|
-
// DD/MM/YYYY
|
|
340
|
-
if (/^\d{2}\/\d{2}\/\d{4}$/.test(val)) {
|
|
341
|
-
const [d, m, y] = val.split('/');
|
|
342
|
-
val = `${y}-${m}-${d}`;
|
|
343
|
-
}
|
|
344
|
-
// DD-MM-YYYY
|
|
345
|
-
else if (/^\d{2}-\d{2}-\d{4}$/.test(val)) {
|
|
346
|
-
const [d, m, y] = val.split('-');
|
|
347
|
-
val = `${y}-${m}-${d}`;
|
|
348
|
-
}
|
|
349
|
-
// YYYY/MM/DD
|
|
350
|
-
else if (/^\d{4}\/\d{2}\/\d{2}$/.test(val)) {
|
|
351
|
-
val = val.replace(/\//g, '-');
|
|
352
|
-
}
|
|
353
|
-
// YYYY.MM.DD
|
|
354
|
-
else if (/^\d{4}\.\d{2}\.\d{2}$/.test(val)) {
|
|
355
|
-
val = val.replace(/\./g, '-');
|
|
356
|
-
}
|
|
357
|
-
const date = new Date(val);
|
|
358
|
-
if (isNaN(date.getTime())) {
|
|
359
|
-
throw new Error(`Invalid birth_date format: ${input}`);
|
|
360
|
-
}
|
|
361
|
-
return val;
|
|
319
|
+
const id = !isCreate ? this.getNodeParameter('id', i) : undefined;
|
|
320
|
+
const pairs = this.getNodeParameter('fields.field', i, []);
|
|
321
|
+
const values = {};
|
|
322
|
+
const emailData = {};
|
|
323
|
+
const phoneData = {};
|
|
324
|
+
const addressData = {};
|
|
325
|
+
function normalizeBirthDate(input) {
|
|
326
|
+
if (!input)
|
|
327
|
+
return input;
|
|
328
|
+
let v = input.trim();
|
|
329
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
362
330
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
locationTypeMap = await getLocationTypeMap.call(this);
|
|
331
|
+
else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
|
|
332
|
+
const [d, m, y] = v.split('/');
|
|
333
|
+
v = `${y}-${m}-${d}`;
|
|
367
334
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// - address.xxx
|
|
372
|
-
// - <prefix>.email[.field]
|
|
373
|
-
// - <prefix>.phone[.field]
|
|
374
|
-
// - <prefix>.address[.field]
|
|
375
|
-
for (const p of pairs) {
|
|
376
|
-
if (!p.fieldName)
|
|
377
|
-
continue;
|
|
378
|
-
const key = p.fieldName.trim();
|
|
379
|
-
const rawVal = convertValue(p.fieldValue);
|
|
380
|
-
const val = rawVal;
|
|
381
|
-
// 1) Casos simples sin prefijo: email / email.xxx / phone / phone.xxx / address.xxx
|
|
382
|
-
// Email simple
|
|
383
|
-
if (key === 'email') {
|
|
384
|
-
emailData.email = val;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (key.startsWith('email.')) {
|
|
388
|
-
emailData[key.replace(/^email\./, '')] = val;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
// Phone simple
|
|
392
|
-
if (key === 'phone') {
|
|
393
|
-
phoneData.phone = val;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (key.startsWith('phone.')) {
|
|
397
|
-
phoneData[key.replace(/^phone\./, '')] = val;
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
// Address simple
|
|
401
|
-
if (key.startsWith('address.')) {
|
|
402
|
-
addressData[key.replace(/^address\./, '')] = val;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
// 2) Prefijo dinámico: <prefix>.email[.field], <prefix>.phone[.field], <prefix>.address[.field]
|
|
406
|
-
const segments = key.split('.');
|
|
407
|
-
if (segments.length >= 2 && resource === 'contact') {
|
|
408
|
-
const prefixRaw = segments[0];
|
|
409
|
-
const root = segments[1]; // email / phone / address
|
|
410
|
-
const subfield = segments.slice(2).join('.') || '';
|
|
411
|
-
const normalizedPrefix = normalizeLocationKey(prefixRaw);
|
|
412
|
-
const mappedLocationName = locationTypeMap[normalizedPrefix];
|
|
413
|
-
if (root === 'email' || root === 'phone' || root === 'address') {
|
|
414
|
-
// Si el prefijo coincide con un location type, sobreescribir el locationName correspondiente
|
|
415
|
-
if (mappedLocationName) {
|
|
416
|
-
if (root === 'email')
|
|
417
|
-
emailLocationName = mappedLocationName;
|
|
418
|
-
if (root === 'phone')
|
|
419
|
-
phoneLocationName = mappedLocationName;
|
|
420
|
-
if (root === 'address')
|
|
421
|
-
addressLocationName = mappedLocationName;
|
|
422
|
-
}
|
|
423
|
-
// Asignar campo
|
|
424
|
-
if (root === 'email') {
|
|
425
|
-
if (!subfield) {
|
|
426
|
-
emailData.email = val;
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
emailData[subfield] = val;
|
|
430
|
-
}
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
if (root === 'phone') {
|
|
434
|
-
if (!subfield) {
|
|
435
|
-
phoneData.phone = val;
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
phoneData[subfield] = val;
|
|
439
|
-
}
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (root === 'address') {
|
|
443
|
-
if (!subfield) {
|
|
444
|
-
// No existe campo address "plano", así que ignoramos si no hay subfield
|
|
445
|
-
}
|
|
446
|
-
else {
|
|
447
|
-
addressData[subfield] = val;
|
|
448
|
-
}
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
// 3) gender
|
|
454
|
-
if (key === 'gender' || key === 'gender_id') {
|
|
455
|
-
values.gender_id = val;
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
// 4) birth_date
|
|
459
|
-
if (key === 'birth_date' || key === 'birth') {
|
|
460
|
-
values.birth_date = normalizeBirthDate(String(val));
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
// 5) default → campo de Contact o de la entidad
|
|
464
|
-
values[key] = val;
|
|
335
|
+
else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
|
|
336
|
+
const [d, m, y] = v.split('-');
|
|
337
|
+
v = `${y}-${m}-${d}`;
|
|
465
338
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (resource === 'contact' && contactType) {
|
|
469
|
-
values.contact_type = contactType;
|
|
339
|
+
else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
|
|
340
|
+
v = v.replace(/\//g, '-');
|
|
470
341
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (isCreate) {
|
|
474
|
-
const contactRes = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Contact/create', { values });
|
|
475
|
-
contactId = contactRes?.values?.[0]?.id;
|
|
476
|
-
if (!contactId)
|
|
477
|
-
throw new Error('Failed to create contact.');
|
|
342
|
+
else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
|
|
343
|
+
v = v.replace(/\./g, '-');
|
|
478
344
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
345
|
+
const dt = new Date(v);
|
|
346
|
+
if (isNaN(dt.getTime()))
|
|
347
|
+
throw new Error(`Invalid birth_date: ${input}`);
|
|
348
|
+
return v;
|
|
349
|
+
}
|
|
350
|
+
let locationTypeMap = {};
|
|
351
|
+
if (resource === 'contact') {
|
|
352
|
+
locationTypeMap = await getLocationTypeMap.call(this);
|
|
353
|
+
}
|
|
354
|
+
for (const p of pairs) {
|
|
355
|
+
if (!p.fieldName)
|
|
356
|
+
continue;
|
|
357
|
+
const key = p.fieldName.trim();
|
|
358
|
+
const rawVal = convertValue(p.fieldValue);
|
|
359
|
+
const val = rawVal;
|
|
360
|
+
/* Email simple */
|
|
361
|
+
if (key === 'email') {
|
|
362
|
+
emailData.email = val;
|
|
363
|
+
continue;
|
|
484
364
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
});
|
|
365
|
+
if (key.startsWith('email.')) {
|
|
366
|
+
emailData[key.replace(/^email\./, '')] = val;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
/* Phone simple */
|
|
370
|
+
if (key === 'phone') {
|
|
371
|
+
phoneData.phone = val;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (key.startsWith('phone.')) {
|
|
375
|
+
phoneData[key.replace(/^phone\./, '')] = val;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
/* Address */
|
|
379
|
+
if (key.startsWith('address.')) {
|
|
380
|
+
addressData[key.replace(/^address\./, '')] = val;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
/* Prefijo dinámico */
|
|
384
|
+
const segments = key.split('.');
|
|
385
|
+
if (segments.length >= 2 && resource === 'contact') {
|
|
386
|
+
const prefixRaw = segments[0];
|
|
387
|
+
const root = segments[1];
|
|
388
|
+
const subfield = segments.slice(2).join('.') || '';
|
|
389
|
+
const normalizedPrefix = normalizeLocationKey(prefixRaw);
|
|
390
|
+
const mappedLocationName = locationTypeMap[normalizedPrefix];
|
|
391
|
+
if (root === 'email' || root === 'phone' || root === 'address') {
|
|
392
|
+
if (mappedLocationName) {
|
|
393
|
+
if (root === 'email')
|
|
394
|
+
emailLocationName = mappedLocationName;
|
|
395
|
+
if (root === 'phone')
|
|
396
|
+
phoneLocationName = mappedLocationName;
|
|
397
|
+
if (root === 'address')
|
|
398
|
+
addressLocationName = mappedLocationName;
|
|
399
|
+
}
|
|
400
|
+
if (root === 'email') {
|
|
401
|
+
if (!subfield)
|
|
402
|
+
emailData.email = val;
|
|
403
|
+
else
|
|
404
|
+
emailData[subfield] = val;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (root === 'phone') {
|
|
408
|
+
if (!subfield)
|
|
409
|
+
phoneData.phone = val;
|
|
410
|
+
else
|
|
411
|
+
phoneData[subfield] = val;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (root === 'address') {
|
|
415
|
+
if (subfield)
|
|
416
|
+
addressData[subfield] = val;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
540
419
|
}
|
|
541
420
|
}
|
|
542
|
-
|
|
543
|
-
|
|
421
|
+
/* gender */
|
|
422
|
+
if (key === 'gender' || key === 'gender_id') {
|
|
423
|
+
values.gender_id = val;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
/* birth_date */
|
|
427
|
+
if (key === 'birth_date' || key === 'birth') {
|
|
428
|
+
values.birth_date = normalizeBirthDate(String(val));
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
/* default */
|
|
432
|
+
values[key] = val;
|
|
433
|
+
}
|
|
434
|
+
/* Contact type */
|
|
435
|
+
const contactType = this.getNodeParameter('contactType', i, '');
|
|
436
|
+
if (resource === 'contact' && contactType) {
|
|
437
|
+
values.contact_type = contactType;
|
|
438
|
+
}
|
|
439
|
+
/* CREATE/UPDATE contact */
|
|
440
|
+
let contactId = id;
|
|
441
|
+
if (isCreate) {
|
|
442
|
+
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Contact/create', { values });
|
|
443
|
+
contactId = r?.values?.[0]?.id;
|
|
444
|
+
if (!contactId)
|
|
445
|
+
throw new Error('Failed to create contact.');
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, {
|
|
449
|
+
values,
|
|
544
450
|
where: [['id', '=', contactId]],
|
|
545
|
-
select: [
|
|
546
|
-
'id',
|
|
547
|
-
'display_name',
|
|
548
|
-
'first_name',
|
|
549
|
-
'last_name',
|
|
550
|
-
'contact_type',
|
|
551
|
-
'gender_id',
|
|
552
|
-
'gender_id:name',
|
|
553
|
-
'birth_date',
|
|
554
|
-
],
|
|
555
|
-
...(resource === 'contact'
|
|
556
|
-
? {
|
|
557
|
-
chain: {
|
|
558
|
-
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
559
|
-
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
560
|
-
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
561
|
-
},
|
|
562
|
-
}
|
|
563
|
-
: {}),
|
|
564
451
|
});
|
|
565
|
-
out.push({ json: (res?.values?.[0] ?? {}) });
|
|
566
452
|
}
|
|
453
|
+
/* SUBENTITIES */
|
|
454
|
+
if (resource === 'contact') {
|
|
455
|
+
if (isPrimary) {
|
|
456
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/delete', {
|
|
457
|
+
where: [
|
|
458
|
+
['contact_id', '=', contactId],
|
|
459
|
+
['is_primary', '=', true],
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/delete', {
|
|
463
|
+
where: [
|
|
464
|
+
['contact_id', '=', contactId],
|
|
465
|
+
['is_primary', '=', true],
|
|
466
|
+
],
|
|
467
|
+
});
|
|
468
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/delete', {
|
|
469
|
+
where: [
|
|
470
|
+
['contact_id', '=', contactId],
|
|
471
|
+
['is_primary', '=', true],
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (Object.keys(emailData).length) {
|
|
476
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
|
|
477
|
+
values: {
|
|
478
|
+
...emailData,
|
|
479
|
+
contact_id: contactId,
|
|
480
|
+
is_primary: isPrimary,
|
|
481
|
+
'location_type_id:name': emailLocationName,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
if (Object.keys(phoneData).length) {
|
|
486
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
|
|
487
|
+
values: {
|
|
488
|
+
...phoneData,
|
|
489
|
+
contact_id: contactId,
|
|
490
|
+
is_primary: isPrimary,
|
|
491
|
+
'location_type_id:name': phoneLocationName,
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (Object.keys(addressData).length) {
|
|
496
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
|
|
497
|
+
values: {
|
|
498
|
+
...addressData,
|
|
499
|
+
contact_id: contactId,
|
|
500
|
+
is_primary: isPrimary,
|
|
501
|
+
'location_type_id:name': addressLocationName,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/* FINAL GET */
|
|
507
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, {
|
|
508
|
+
where: [['id', '=', contactId]],
|
|
509
|
+
select: [
|
|
510
|
+
'id',
|
|
511
|
+
'display_name',
|
|
512
|
+
'first_name',
|
|
513
|
+
'last_name',
|
|
514
|
+
'contact_type',
|
|
515
|
+
'gender_id',
|
|
516
|
+
'gender_id:name',
|
|
517
|
+
'birth_date',
|
|
518
|
+
],
|
|
519
|
+
...(resource === 'contact'
|
|
520
|
+
? {
|
|
521
|
+
chain: {
|
|
522
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
523
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
524
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
: {}),
|
|
528
|
+
});
|
|
529
|
+
out.push({ json: res?.values?.[0] ?? {} });
|
|
567
530
|
}
|
|
568
531
|
return [out];
|
|
569
532
|
}
|
|
570
533
|
}
|
|
571
534
|
exports.CiviCrm = CiviCrm;
|
|
535
|
+
/* ============================================================================
|
|
536
|
+
UTILS
|
|
537
|
+
============================================================================ */
|
|
572
538
|
function convertValue(val) {
|
|
573
539
|
const t = String(val ?? '').trim();
|
|
574
540
|
if (t === '')
|