@ixiam/n8n-nodes-civicrm 0.3.9 → 0.4.4
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/dist/nodes/CiviCrm/CiviCrm.node.js +408 -85
- package/nodes/CiviCrm/CiviCrm.node.ts +560 -187
- package/package.json +1 -1
|
@@ -16,6 +16,41 @@ const ENTITY_MAP = {
|
|
|
16
16
|
email: 'Email',
|
|
17
17
|
activity: 'Activity',
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Caché global de location types
|
|
21
|
+
* Se rellena la primera vez que se usa y dura hasta que se reinicia n8n.
|
|
22
|
+
* key: normalizado (lowercase, sin espacios/ símbolos)
|
|
23
|
+
* value: option_value.name (para usar en location_type_id:name)
|
|
24
|
+
*/
|
|
25
|
+
let locationTypeCache = null;
|
|
26
|
+
function normalizeLocationKey(s) {
|
|
27
|
+
return String(s || '')
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
30
|
+
}
|
|
31
|
+
async function getLocationTypeMap() {
|
|
32
|
+
if (locationTypeCache)
|
|
33
|
+
return locationTypeCache;
|
|
34
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/OptionValue/get', {
|
|
35
|
+
where: [['option_group_id:name', '=', 'location_type']],
|
|
36
|
+
select: ['name', 'label'],
|
|
37
|
+
limit: 0,
|
|
38
|
+
});
|
|
39
|
+
const values = (res?.values || []);
|
|
40
|
+
const map = {};
|
|
41
|
+
for (const v of values) {
|
|
42
|
+
const name = v.name || '';
|
|
43
|
+
const label = v.label || '';
|
|
44
|
+
const normName = normalizeLocationKey(name);
|
|
45
|
+
const normLabel = normalizeLocationKey(label);
|
|
46
|
+
if (normName)
|
|
47
|
+
map[normName] = name;
|
|
48
|
+
if (normLabel)
|
|
49
|
+
map[normLabel] = name;
|
|
50
|
+
}
|
|
51
|
+
locationTypeCache = map;
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
19
54
|
/**
|
|
20
55
|
* Nodo principal CiviCRM para n8n
|
|
21
56
|
*/
|
|
@@ -27,7 +62,36 @@ class CiviCrm {
|
|
|
27
62
|
icon: 'file:civicrm.svg',
|
|
28
63
|
group: ['transform'],
|
|
29
64
|
version: 1,
|
|
30
|
-
description: 'Interact with CiviCRM API v4 (Civi-Go compatible)'
|
|
65
|
+
description: 'Interact with CiviCRM API v4 (Civi-Go compatible).\\n\\n' +
|
|
66
|
+
'Email / Phone / Address mapping:\\n' +
|
|
67
|
+
'- You can send simple fields: email, phone, address.city, address.country_id, etc.\\n' +
|
|
68
|
+
'- You can also use dynamic prefixes based on real CiviCRM Location Types.\\n' +
|
|
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]] ]',
|
|
31
95
|
defaults: { name: 'CiviCRM' },
|
|
32
96
|
inputs: ['main'],
|
|
33
97
|
outputs: ['main'],
|
|
@@ -45,10 +109,68 @@ class CiviCrm {
|
|
|
45
109
|
{ name: 'Organization', value: 'Organization' },
|
|
46
110
|
{ name: 'Household', value: 'Household' },
|
|
47
111
|
],
|
|
48
|
-
displayOptions: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
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
|
+
},
|
|
116
|
+
{
|
|
117
|
+
displayName: 'Email Location Type',
|
|
118
|
+
name: 'emailLocation',
|
|
119
|
+
type: 'options',
|
|
120
|
+
default: 'Work',
|
|
121
|
+
options: [
|
|
122
|
+
{ name: 'Home', value: 'Home' },
|
|
123
|
+
{ name: 'Work', value: 'Work' },
|
|
124
|
+
{ name: 'Other', value: 'Other' },
|
|
125
|
+
],
|
|
126
|
+
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
|
+
},
|
|
132
|
+
{
|
|
133
|
+
displayName: 'Phone Location Type',
|
|
134
|
+
name: 'phoneLocation',
|
|
135
|
+
type: 'options',
|
|
136
|
+
default: 'Work',
|
|
137
|
+
options: [
|
|
138
|
+
{ name: 'Home', value: 'Home' },
|
|
139
|
+
{ name: 'Work', value: 'Work' },
|
|
140
|
+
{ name: 'Mobile', value: 'Mobile' },
|
|
141
|
+
{ name: 'Other', value: 'Other' },
|
|
142
|
+
],
|
|
143
|
+
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
|
+
},
|
|
149
|
+
{
|
|
150
|
+
displayName: 'Address Location Type',
|
|
151
|
+
name: 'addressLocation',
|
|
152
|
+
type: 'options',
|
|
153
|
+
default: 'Home',
|
|
154
|
+
options: [
|
|
155
|
+
{ name: 'Home', value: 'Home' },
|
|
156
|
+
{ name: 'Work', value: 'Work' },
|
|
157
|
+
{ name: 'Billing', value: 'Billing' },
|
|
158
|
+
{ name: 'Other', value: 'Other' },
|
|
159
|
+
],
|
|
160
|
+
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
|
+
},
|
|
166
|
+
{
|
|
167
|
+
displayName: 'Mark as Primary',
|
|
168
|
+
name: 'isPrimary',
|
|
169
|
+
type: 'boolean',
|
|
170
|
+
default: true,
|
|
171
|
+
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.',
|
|
52
174
|
},
|
|
53
175
|
{
|
|
54
176
|
displayName: 'ID',
|
|
@@ -57,6 +179,7 @@ class CiviCrm {
|
|
|
57
179
|
default: 0,
|
|
58
180
|
required: true,
|
|
59
181
|
displayOptions: { show: { operation: ['get', 'update', 'delete'] } },
|
|
182
|
+
description: 'Record ID of the selected entity (Contact, Event, Case, etc.).',
|
|
60
183
|
},
|
|
61
184
|
...generic_1.genericFields,
|
|
62
185
|
...generic_1.upsertFields,
|
|
@@ -74,9 +197,7 @@ class CiviCrm {
|
|
|
74
197
|
'X-Civi-Auth': `Bearer ${apiToken}`,
|
|
75
198
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
76
199
|
},
|
|
77
|
-
body: {
|
|
78
|
-
params: JSON.stringify({ limit: 5 }),
|
|
79
|
-
},
|
|
200
|
+
body: { params: JSON.stringify({ limit: 5 }) },
|
|
80
201
|
json: true,
|
|
81
202
|
});
|
|
82
203
|
const values = (res?.values || []);
|
|
@@ -95,17 +216,20 @@ class CiviCrm {
|
|
|
95
216
|
const operation = this.getNodeParameter('operation', 0);
|
|
96
217
|
const entity = ENTITY_MAP[resource];
|
|
97
218
|
for (let i = 0; i < items.length; i++) {
|
|
98
|
-
|
|
219
|
+
const emailLocationParam = this.getNodeParameter('emailLocation', i, 'Work');
|
|
220
|
+
const phoneLocationParam = this.getNodeParameter('phoneLocation', i, 'Work');
|
|
221
|
+
const addressLocationParam = this.getNodeParameter('addressLocation', i, 'Home');
|
|
222
|
+
const isPrimary = this.getNodeParameter('isPrimary', i, true);
|
|
223
|
+
// Valores de location type efectivos (pueden ser sobreescritos por prefijo)
|
|
224
|
+
let emailLocationName = emailLocationParam;
|
|
225
|
+
let phoneLocationName = phoneLocationParam;
|
|
226
|
+
let addressLocationName = addressLocationParam;
|
|
227
|
+
// =========== GET ===========
|
|
99
228
|
if (operation === 'get') {
|
|
100
229
|
const id = this.getNodeParameter('id', i);
|
|
101
|
-
const contactType = this.getNodeParameter('contactType', i, '');
|
|
102
230
|
const where = [['id', '=', id]];
|
|
103
|
-
if (resource === 'contact' && contactType) {
|
|
104
|
-
where.push(['contact_type', '=', contactType]);
|
|
105
|
-
}
|
|
106
|
-
let params;
|
|
107
231
|
if (resource === 'contact') {
|
|
108
|
-
params = {
|
|
232
|
+
const params = {
|
|
109
233
|
where,
|
|
110
234
|
limit: 1,
|
|
111
235
|
select: [
|
|
@@ -114,40 +238,39 @@ class CiviCrm {
|
|
|
114
238
|
'first_name',
|
|
115
239
|
'last_name',
|
|
116
240
|
'contact_type',
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'address.country_id:label',
|
|
121
|
-
'address.postal_code',
|
|
122
|
-
'address.street_address',
|
|
123
|
-
],
|
|
124
|
-
join: [
|
|
125
|
-
['Email AS email', 'LEFT'],
|
|
126
|
-
['Phone AS phone', 'LEFT'],
|
|
127
|
-
['Address AS address', 'LEFT'],
|
|
241
|
+
'gender_id',
|
|
242
|
+
'gender_id:name',
|
|
243
|
+
'birth_date',
|
|
128
244
|
],
|
|
245
|
+
chain: {
|
|
246
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
247
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
248
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
249
|
+
},
|
|
129
250
|
};
|
|
251
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Contact/get', params);
|
|
252
|
+
out.push({ json: (res?.values?.[0] ?? {}) });
|
|
130
253
|
}
|
|
131
254
|
else {
|
|
132
|
-
|
|
133
|
-
params = {
|
|
255
|
+
const params = {
|
|
134
256
|
where,
|
|
135
257
|
limit: 1,
|
|
136
|
-
select: ['id', '
|
|
258
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
137
259
|
};
|
|
260
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
|
|
261
|
+
out.push({ json: (res?.values?.[0] ?? {}) });
|
|
138
262
|
}
|
|
139
|
-
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
|
|
140
|
-
out.push({ json: (res?.values?.[0] ?? {}) });
|
|
141
263
|
}
|
|
142
|
-
//
|
|
264
|
+
// =========== GET MANY ===========
|
|
143
265
|
if (operation === 'getMany') {
|
|
144
266
|
const returnAll = this.getNodeParameter('returnAll', i, false);
|
|
145
267
|
const limit = this.getNodeParameter('limit', i, 100);
|
|
146
268
|
const whereJson = this.getNodeParameter('whereJson', i, '');
|
|
147
|
-
const contactType = this.getNodeParameter('contactType', i, '');
|
|
148
269
|
let where = whereJson ? JSON.parse(whereJson) : [];
|
|
149
|
-
if (resource === 'contact'
|
|
150
|
-
|
|
270
|
+
if (resource === 'contact') {
|
|
271
|
+
const contactType = this.getNodeParameter('contactType', i, '');
|
|
272
|
+
if (contactType)
|
|
273
|
+
where.push(['contact_type', '=', contactType]);
|
|
151
274
|
}
|
|
152
275
|
let baseParams;
|
|
153
276
|
if (resource === 'contact') {
|
|
@@ -159,22 +282,22 @@ class CiviCrm {
|
|
|
159
282
|
'first_name',
|
|
160
283
|
'last_name',
|
|
161
284
|
'contact_type',
|
|
162
|
-
'
|
|
163
|
-
'
|
|
164
|
-
'
|
|
165
|
-
'address.country_id:label',
|
|
166
|
-
'address.postal_code',
|
|
167
|
-
'address.street_address',
|
|
168
|
-
],
|
|
169
|
-
join: [
|
|
170
|
-
['Email AS email', 'LEFT'],
|
|
171
|
-
['Phone AS phone', 'LEFT'],
|
|
172
|
-
['Address AS address', 'LEFT'],
|
|
285
|
+
'gender_id',
|
|
286
|
+
'gender_id:name',
|
|
287
|
+
'birth_date',
|
|
173
288
|
],
|
|
289
|
+
chain: {
|
|
290
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
291
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
292
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
293
|
+
},
|
|
174
294
|
};
|
|
175
295
|
}
|
|
176
296
|
else {
|
|
177
|
-
baseParams = {
|
|
297
|
+
baseParams = {
|
|
298
|
+
where,
|
|
299
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
300
|
+
};
|
|
178
301
|
}
|
|
179
302
|
if (returnAll) {
|
|
180
303
|
let offset = 0;
|
|
@@ -196,55 +319,256 @@ class CiviCrm {
|
|
|
196
319
|
out.push({ json: v });
|
|
197
320
|
}
|
|
198
321
|
}
|
|
199
|
-
//
|
|
200
|
-
|
|
322
|
+
// =========== CREATE / UPDATE ===========
|
|
323
|
+
const isCreate = operation === 'create';
|
|
324
|
+
if (isCreate || operation === 'update') {
|
|
325
|
+
const id = !isCreate ? this.getNodeParameter('id', i) : undefined;
|
|
201
326
|
const pairs = this.getNodeParameter('fields.field', i, []);
|
|
202
|
-
const values =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
327
|
+
const values = {};
|
|
328
|
+
const emailData = {};
|
|
329
|
+
const phoneData = {};
|
|
330
|
+
const addressData = {};
|
|
331
|
+
// Normaliza birth_date a YYYY-MM-DD con validación
|
|
332
|
+
function normalizeBirthDate(input) {
|
|
333
|
+
if (!input)
|
|
334
|
+
return input;
|
|
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;
|
|
208
362
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
363
|
+
// Cargar mapa de location types solo si estamos en contacto
|
|
364
|
+
let locationTypeMap = {};
|
|
365
|
+
if (resource === 'contact') {
|
|
366
|
+
locationTypeMap = await getLocationTypeMap.call(this);
|
|
367
|
+
}
|
|
368
|
+
// Field mapping con soporte para:
|
|
369
|
+
// - email, email.xxx
|
|
370
|
+
// - phone, phone.xxx
|
|
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;
|
|
465
|
+
}
|
|
466
|
+
// contact_type
|
|
220
467
|
const contactType = this.getNodeParameter('contactType', i, '');
|
|
221
468
|
if (resource === 'contact' && contactType) {
|
|
222
469
|
values.contact_type = contactType;
|
|
223
470
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
471
|
+
// --- CREATE / UPDATE CONTACT ---
|
|
472
|
+
let contactId = id;
|
|
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.');
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, {
|
|
481
|
+
values,
|
|
482
|
+
where: [['id', '=', contactId]],
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// --- SUBENTITIES (solo para contact) ---
|
|
486
|
+
if (resource === 'contact') {
|
|
487
|
+
// Si es primary, limpiar anteriores
|
|
488
|
+
if (isPrimary) {
|
|
489
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/delete', {
|
|
490
|
+
where: [
|
|
491
|
+
['contact_id', '=', contactId],
|
|
492
|
+
['is_primary', '=', true],
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/delete', {
|
|
496
|
+
where: [
|
|
497
|
+
['contact_id', '=', contactId],
|
|
498
|
+
['is_primary', '=', true],
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/delete', {
|
|
502
|
+
where: [
|
|
503
|
+
['contact_id', '=', contactId],
|
|
504
|
+
['is_primary', '=', true],
|
|
505
|
+
],
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// Email
|
|
509
|
+
if (Object.keys(emailData).length) {
|
|
510
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
|
|
511
|
+
values: {
|
|
512
|
+
...emailData,
|
|
513
|
+
contact_id: contactId,
|
|
514
|
+
is_primary: isPrimary,
|
|
515
|
+
'location_type_id:name': emailLocationName,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
// Phone
|
|
520
|
+
if (Object.keys(phoneData).length) {
|
|
521
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
|
|
522
|
+
values: {
|
|
523
|
+
...phoneData,
|
|
524
|
+
contact_id: contactId,
|
|
525
|
+
is_primary: isPrimary,
|
|
526
|
+
'location_type_id:name': phoneLocationName,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
// Address
|
|
531
|
+
if (Object.keys(addressData).length) {
|
|
532
|
+
await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
|
|
533
|
+
values: {
|
|
534
|
+
...addressData,
|
|
535
|
+
contact_id: contactId,
|
|
536
|
+
is_primary: isPrimary,
|
|
537
|
+
'location_type_id:name': addressLocationName,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// --- FINAL GET (con gender + birth_date + chain) ---
|
|
543
|
+
const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, {
|
|
544
|
+
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
|
+
});
|
|
565
|
+
out.push({ json: (res?.values?.[0] ?? {}) });
|
|
239
566
|
}
|
|
240
567
|
}
|
|
241
568
|
return [out];
|
|
242
569
|
}
|
|
243
570
|
}
|
|
244
571
|
exports.CiviCrm = CiviCrm;
|
|
245
|
-
/**
|
|
246
|
-
* Convierte string a número, boolean, objeto o deja string
|
|
247
|
-
*/
|
|
248
572
|
function convertValue(val) {
|
|
249
573
|
const t = String(val ?? '').trim();
|
|
250
574
|
if (t === '')
|
|
@@ -263,5 +587,4 @@ function convertValue(val) {
|
|
|
263
587
|
catch { }
|
|
264
588
|
return val;
|
|
265
589
|
}
|
|
266
|
-
// Requerido por n8n >=1.110
|
|
267
590
|
exports.default = CiviCrm;
|