@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.
@@ -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
- show: { resource: ['contact'] },
50
- },
51
- description: 'Filter or assign contact type',
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
- // === GET ===
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
- 'email.email',
118
- 'phone.phone',
119
- 'address.city',
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
- // otras entidades: select básico
133
- params = {
255
+ const params = {
134
256
  where,
135
257
  limit: 1,
136
- select: ['id', 'title', 'subject', 'display_name', 'name'],
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
- // === GET MANY ===
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' && contactType) {
150
- where.push(['contact_type', '=', contactType]);
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
- 'email.email',
163
- 'phone.phone',
164
- 'address.city',
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 = { where, select: ['id', 'title', 'subject', 'display_name', 'name'] };
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
- // === CREATE ===
200
- if (operation === 'create') {
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 = Object.fromEntries(pairs
203
- .filter((p) => p.fieldName)
204
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]));
205
- const contactType = this.getNodeParameter('contactType', i, '');
206
- if (resource === 'contact' && contactType) {
207
- values.contact_type = contactType;
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
- const params = { values };
210
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/create`, params);
211
- out.push({ json: res });
212
- }
213
- // === UPDATE ===
214
- if (operation === 'update') {
215
- const id = this.getNodeParameter('id', i);
216
- const pairs = this.getNodeParameter('fields.field', i, []);
217
- const values = Object.fromEntries(pairs
218
- .filter((p) => p.fieldName)
219
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]));
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
- const params = {
225
- values,
226
- where: [['id', '=', id]],
227
- };
228
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, params);
229
- out.push({ json: res });
230
- }
231
- // === DELETE ===
232
- if (operation === 'delete') {
233
- const id = this.getNodeParameter('id', i);
234
- const params = {
235
- where: [['id', '=', id]],
236
- };
237
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/delete`, params);
238
- out.push({ json: res });
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;