@ixiam/n8n-nodes-civicrm 1.1.41 → 2.1.2

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.
@@ -1,1257 +0,0 @@
1
- import type {
2
- IExecuteFunctions,
3
- ILoadOptionsFunctions,
4
- INodeExecutionData,
5
- INodePropertyOptions,
6
- INodeType,
7
- INodeTypeDescription,
8
- IDataObject,
9
- } from 'n8n-workflow';
10
-
11
- import { civicrmApiRequest } from '../transport/GenericFunctions';
12
- import { resourceProp, operationProp } from './descriptions/resources';
13
- import { genericFields, upsertFields } from './descriptions/generic';
14
-
15
- /* ============================================================================
16
- RESOURCES
17
- ============================================================================ */
18
-
19
- type Resource =
20
- | 'contact'
21
- | 'membership'
22
- | 'group'
23
- | 'relationship'
24
- | 'activity'
25
- | 'customApi';
26
-
27
- type Operation = 'get' | 'getMany' | 'create' | 'update' | 'delete';
28
-
29
- const ENTITY_MAP: Record<Exclude<Resource, 'customApi'>, string> = {
30
- contact: 'Contact',
31
- membership: 'Membership',
32
- group: 'Group',
33
- relationship: 'Relationship',
34
- activity: 'Activity',
35
- };
36
-
37
- /* ============================================================================
38
- LOCATION TYPE CACHE
39
- ============================================================================ */
40
-
41
- let locationTypeCache: Record<string, string> | null = null;
42
-
43
- function normalizeLocationKey(s: string): string {
44
- return String(s || '')
45
- .toLowerCase()
46
- .replace(/[^a-z0-9]+/g, '');
47
- }
48
-
49
- async function getLocationTypeMap(this: IExecuteFunctions): Promise<Record<string, string>> {
50
- if (locationTypeCache) return locationTypeCache;
51
-
52
- const res = await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/OptionValue/get', {
53
- where: [['option_group_id:name', '=', 'location_type']],
54
- select: ['name', 'label'],
55
- limit: 0,
56
- });
57
-
58
- const map: Record<string, string> = {};
59
- const values = (res?.values || []) as Array<{ name?: string; label?: string }>;
60
-
61
- for (const v of values) {
62
- const name = v.name || '';
63
- const label = v.label || '';
64
- const k1 = normalizeLocationKey(name);
65
- const k2 = normalizeLocationKey(label);
66
- if (k1) map[k1] = name;
67
- if (k2) map[k2] = name;
68
- }
69
-
70
- locationTypeCache = map;
71
- return map;
72
- }
73
-
74
- /* ============================================================================
75
- NODE DEFINITION
76
- ============================================================================ */
77
-
78
- export class CiviCrm implements INodeType {
79
- description: INodeTypeDescription = {
80
- displayName: 'CiviCRM',
81
- name: 'civiCrm',
82
- icon: 'file:civicrm.svg',
83
- group: ['transform'],
84
- version: 1,
85
- description:
86
- 'Interact with CiviCRM API v4 (Civi-Go compatible).\n\n' +
87
- 'Supports Contact, Membership, Group, Relationship and Activity entities.\n' +
88
- 'Includes dynamic mapping of email, phone, address and location types.\n' +
89
- 'Includes birth_date validation and JSON filters for GET MANY.\n',
90
- defaults: { name: 'CiviCRM' },
91
- inputs: ['main'],
92
- outputs: ['main'],
93
-
94
- // @ts-ignore
95
- usableAsTool: true,
96
- // @ts-ignore
97
- actions: [
98
- // ======================================================================
99
- // CONTACT
100
- // ======================================================================
101
- {
102
- displayName: 'Get Contact',
103
- name: 'getContact',
104
- action: 'Get a contact',
105
- description: 'Retrieve a contact from CiviCRM by ID or email.',
106
- displayOptions: { show: { resource: ['contact'], operation: ['get'] } },
107
- properties: [
108
- {
109
- displayName: 'ID',
110
- name: 'id',
111
- type: 'number',
112
- default: 0,
113
- description: 'The internal CiviCRM ID of the contact to retrieve.',
114
- },
115
- ],
116
- },
117
- {
118
- displayName: 'Get Many Contacts',
119
- name: 'getManyContacts',
120
- action: 'Get many contacts',
121
- description: 'Retrieve multiple contacts from CiviCRM with filtering.',
122
- displayOptions: { show: { resource: ['contact'], operation: ['getMany'] } },
123
- properties: [...genericFields],
124
- },
125
- {
126
- displayName: 'Create Contact',
127
- name: 'createContact',
128
- action: 'Create a contact',
129
- description: 'Create a new contact in CiviCRM.',
130
- displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
131
- properties: [...upsertFields],
132
- },
133
- {
134
- displayName: 'Update Contact',
135
- name: 'updateContact',
136
- action: 'Update a contact',
137
- description: 'Update an existing contact in CiviCRM.',
138
- displayOptions: { show: { resource: ['contact'], operation: ['update'] } },
139
- properties: [
140
- {
141
- displayName: 'ID',
142
- name: 'id',
143
- type: 'number',
144
- default: 0,
145
- description: 'The ID of the contact to update.',
146
- },
147
- ...upsertFields,
148
- ],
149
- },
150
- {
151
- displayName: 'Delete Contact',
152
- name: 'deleteContact',
153
- action: 'Delete a contact',
154
- description: 'Delete a contact from CiviCRM.',
155
- displayOptions: { show: { resource: ['contact'], operation: ['delete'] } },
156
- properties: [
157
- {
158
- displayName: 'ID',
159
- name: 'id',
160
- type: 'number',
161
- default: 0,
162
- description: 'The ID of the contact to delete.',
163
- },
164
- ],
165
- },
166
-
167
- // ======================================================================
168
- // MEMBERSHIP
169
- // ======================================================================
170
- {
171
- displayName: 'Get Membership',
172
- name: 'getMembership',
173
- action: 'Get a membership',
174
- description: 'Retrieve a membership by ID.',
175
- displayOptions: { show: { resource: ['membership'], operation: ['get'] } },
176
- properties: [
177
- {
178
- displayName: 'ID',
179
- name: 'id',
180
- type: 'number',
181
- default: 0,
182
- description: 'The internal CiviCRM ID of the membership.',
183
- },
184
- ],
185
- },
186
- {
187
- displayName: 'Get Many Memberships',
188
- name: 'getManyMemberships',
189
- action: 'Get many memberships',
190
- description: 'Retrieve multiple memberships.',
191
- displayOptions: { show: { resource: ['membership'], operation: ['getMany'] } },
192
- properties: [...genericFields],
193
- },
194
- {
195
- displayName: 'Create Membership',
196
- name: 'createMembership',
197
- action: 'Create a membership',
198
- description: 'Create a new membership.',
199
- displayOptions: { show: { resource: ['membership'], operation: ['create'] } },
200
- properties: [...upsertFields],
201
- },
202
- {
203
- displayName: 'Update Membership',
204
- name: 'updateMembership',
205
- action: 'Update a membership',
206
- description: 'Update an existing membership.',
207
- displayOptions: { show: { resource: ['membership'], operation: ['update'] } },
208
- properties: [
209
- {
210
- displayName: 'ID',
211
- name: 'id',
212
- type: 'number',
213
- default: 0,
214
- description: 'The ID of the membership to update.',
215
- },
216
- ...upsertFields,
217
- ],
218
- },
219
- {
220
- displayName: 'Delete Membership',
221
- name: 'deleteMembership',
222
- action: 'Delete a membership',
223
- description: 'Delete a membership.',
224
- displayOptions: { show: { resource: ['membership'], operation: ['delete'] } },
225
- properties: [
226
- {
227
- displayName: 'ID',
228
- name: 'id',
229
- type: 'number',
230
- default: 0,
231
- description: 'The ID of the membership to delete.',
232
- },
233
- ],
234
- },
235
-
236
- // ======================================================================
237
- // GROUP
238
- // ======================================================================
239
- {
240
- displayName: 'Get Group',
241
- name: 'getGroup',
242
- action: 'Get a group',
243
- description: 'Retrieve a group by ID.',
244
- displayOptions: { show: { resource: ['group'], operation: ['get'] } },
245
- properties: [
246
- {
247
- displayName: 'ID',
248
- name: 'id',
249
- type: 'number',
250
- default: 0,
251
- description: 'The internal CiviCRM ID of the group.',
252
- },
253
- ],
254
- },
255
- {
256
- displayName: 'Get Many Groups',
257
- name: 'getManyGroups',
258
- action: 'Get many groups',
259
- description: 'Retrieve multiple groups.',
260
- displayOptions: { show: { resource: ['group'], operation: ['getMany'] } },
261
- properties: [...genericFields],
262
- },
263
- {
264
- displayName: 'Create Group',
265
- name: 'createGroup',
266
- action: 'Create a group',
267
- description: 'Create a new group.',
268
- displayOptions: { show: { resource: ['group'], operation: ['create'] } },
269
- properties: [...upsertFields],
270
- },
271
- {
272
- displayName: 'Update Group',
273
- name: 'updateGroup',
274
- action: 'Update a group',
275
- description: 'Update an existing group.',
276
- displayOptions: { show: { resource: ['group'], operation: ['update'] } },
277
- properties: [
278
- {
279
- displayName: 'ID',
280
- name: 'id',
281
- type: 'number',
282
- default: 0,
283
- description: 'The ID of the group to update.',
284
- },
285
- ...upsertFields,
286
- ],
287
- },
288
- {
289
- displayName: 'Delete Group',
290
- name: 'deleteGroup',
291
- action: 'Delete a group',
292
- description: 'Delete a group.',
293
- displayOptions: { show: { resource: ['group'], operation: ['delete'] } },
294
- properties: [
295
- {
296
- displayName: 'ID',
297
- name: 'id',
298
- type: 'number',
299
- default: 0,
300
- description: 'The ID of the group to delete.',
301
- },
302
- ],
303
- },
304
-
305
- // ======================================================================
306
- // RELATIONSHIP
307
- // ======================================================================
308
- {
309
- displayName: 'Get Relationship',
310
- name: 'getRelationship',
311
- action: 'Get a relationship',
312
- description: 'Retrieve a relationship by ID.',
313
- displayOptions: { show: { resource: ['relationship'], operation: ['get'] } },
314
- properties: [
315
- {
316
- displayName: 'ID',
317
- name: 'id',
318
- type: 'number',
319
- default: 0,
320
- description: 'The internal CiviCRM ID of the relationship.',
321
- },
322
- ],
323
- },
324
- {
325
- displayName: 'Get Many Relationships',
326
- name: 'getManyRelationships',
327
- action: 'Get many relationships',
328
- description: 'Retrieve multiple relationships.',
329
- displayOptions: { show: { resource: ['relationship'], operation: ['getMany'] } },
330
- properties: [...genericFields],
331
- },
332
- {
333
- displayName: 'Create Relationship',
334
- name: 'createRelationship',
335
- action: 'Create a relationship',
336
- description: 'Create a new relationship.',
337
- displayOptions: { show: { resource: ['relationship'], operation: ['create'] } },
338
- properties: [...upsertFields],
339
- },
340
- {
341
- displayName: 'Update Relationship',
342
- name: 'updateRelationship',
343
- action: 'Update a relationship',
344
- description: 'Update an existing relationship.',
345
- displayOptions: { show: { resource: ['relationship'], operation: ['update'] } },
346
- properties: [
347
- {
348
- displayName: 'ID',
349
- name: 'id',
350
- type: 'number',
351
- default: 0,
352
- description: 'The ID of the relationship to update.',
353
- },
354
- ...upsertFields,
355
- ],
356
- },
357
- {
358
- displayName: 'Delete Relationship',
359
- name: 'deleteRelationship',
360
- action: 'Delete a relationship',
361
- description: 'Delete a relationship.',
362
- displayOptions: { show: { resource: ['relationship'], operation: ['delete'] } },
363
- properties: [
364
- {
365
- displayName: 'ID',
366
- name: 'id',
367
- type: 'number',
368
- default: 0,
369
- description: 'The ID of the relationship to delete.',
370
- },
371
- ],
372
- },
373
-
374
- // ======================================================================
375
- // ACTIVITY
376
- // ======================================================================
377
- {
378
- displayName: 'Get Activity',
379
- name: 'getActivity',
380
- action: 'Get an activity',
381
- description: 'Retrieve an activity by ID.',
382
- displayOptions: { show: { resource: ['activity'], operation: ['get'] } },
383
- properties: [
384
- {
385
- displayName: 'ID',
386
- name: 'id',
387
- type: 'number',
388
- default: 0,
389
- description: 'The internal CiviCRM ID of the activity.',
390
- },
391
- ],
392
- },
393
- {
394
- displayName: 'Get Many Activities',
395
- name: 'getManyActivities',
396
- action: 'Get many activities',
397
- description: 'Retrieve multiple activities.',
398
- displayOptions: { show: { resource: ['activity'], operation: ['getMany'] } },
399
- properties: [...genericFields],
400
- },
401
- {
402
- displayName: 'Create Activity',
403
- name: 'createActivity',
404
- action: 'Create an activity',
405
- description: 'Create a new activity.',
406
- displayOptions: { show: { resource: ['activity'], operation: ['create'] } },
407
- properties: [...upsertFields],
408
- },
409
- {
410
- displayName: 'Update Activity',
411
- name: 'updateActivity',
412
- action: 'Update an activity',
413
- description: 'Update an existing activity.',
414
- displayOptions: { show: { resource: ['activity'], operation: ['update'] } },
415
- properties: [
416
- {
417
- displayName: 'ID',
418
- name: 'id',
419
- type: 'number',
420
- default: 0,
421
- description: 'The ID of the activity to update.',
422
- },
423
- ...upsertFields,
424
- ],
425
- },
426
- {
427
- displayName: 'Delete Activity',
428
- name: 'deleteActivity',
429
- action: 'Delete an activity',
430
- description: 'Delete an activity.',
431
- displayOptions: { show: { resource: ['activity'], operation: ['delete'] } },
432
- properties: [
433
- {
434
- displayName: 'ID',
435
- name: 'id',
436
- type: 'number',
437
- default: 0,
438
- description: 'The ID of the activity to delete.',
439
- },
440
- ],
441
- },
442
- ],
443
- credentials: [{ name: 'civiCrmApi', required: true }],
444
-
445
- properties: [
446
- //
447
- // RESOURCE + OPERATION
448
- //
449
- resourceProp,
450
- operationProp,
451
-
452
- //
453
- // CONTACT TYPE
454
- //
455
- {
456
- displayName: 'Contact Type',
457
- name: 'contactType',
458
- type: 'options',
459
- default: 'Individual',
460
- description: 'Type of contact to create or search for.',
461
- options: [
462
- { name: 'Individual', value: 'Individual' },
463
- { name: 'Organization', value: 'Organization' },
464
- { name: 'Household', value: 'Household' },
465
- ],
466
- displayOptions: { show: { resource: ['contact'] } },
467
- },
468
-
469
- //
470
- // LOCATION TYPES
471
- //
472
- {
473
- displayName: 'Email Location Type',
474
- name: 'emailLocation',
475
- type: 'options',
476
- default: 'Work',
477
- description: 'Location type for the email address.',
478
- options: [
479
- { name: 'Home', value: 'Home' },
480
- { name: 'Work', value: 'Work' },
481
- { name: 'Other', value: 'Other' },
482
- ],
483
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
484
- },
485
- {
486
- displayName: 'Mark Email as Primary',
487
- name: 'isPrimaryEmail',
488
- type: 'boolean',
489
- default: true,
490
- description: 'Whether this email should be the primary email for the contact.',
491
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
492
- },
493
- {
494
- displayName: 'Phone Location Type',
495
- name: 'phoneLocation',
496
- type: 'options',
497
- default: 'Work',
498
- description: 'Location type for the phone number.',
499
- options: [
500
- { name: 'Home', value: 'Home' },
501
- { name: 'Work', value: 'Work' },
502
- { name: 'Mobile', value: 'Mobile' },
503
- { name: 'Other', value: 'Other' },
504
- ],
505
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
506
- },
507
- {
508
- displayName: 'Mark Phone as Primary',
509
- name: 'isPrimaryPhone',
510
- type: 'boolean',
511
- default: true,
512
- description: 'Whether this phone should be the primary phone for the contact.',
513
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
514
- },
515
- {
516
- displayName: 'Address Location Type',
517
- name: 'addressLocation',
518
- type: 'options',
519
- default: 'Home',
520
- description: 'Location type for the address.',
521
- options: [
522
- { name: 'Home', value: 'Home' },
523
- { name: 'Work', value: 'Work' },
524
- { name: 'Billing', value: 'Billing' },
525
- { name: 'Other', value: 'Other' },
526
- ],
527
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
528
- },
529
- {
530
- displayName: 'Mark Address as Primary',
531
- name: 'isPrimaryAddress',
532
- type: 'boolean',
533
- default: true,
534
- description: 'Whether this address should be the primary address for the contact.',
535
- displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
536
- },
537
-
538
- //
539
- // ID for GET / UPDATE / DELETE
540
- //
541
- {
542
- displayName: 'ID',
543
- name: 'id',
544
- type: 'number',
545
- default: 0,
546
- required: true,
547
- description: 'The internal CiviCRM ID of the entity.',
548
- displayOptions: {
549
- show: {
550
- operation: ['get', 'update', 'delete'],
551
- resource: ['contact', 'membership', 'group', 'relationship', 'activity'],
552
- },
553
- },
554
- },
555
-
556
- //
557
- // CUSTOM API CALL
558
- //
559
- {
560
- displayName: 'Entity',
561
- name: 'customEntity',
562
- type: 'string',
563
- default: 'Contact',
564
- required: true,
565
- description:
566
- 'CiviCRM API4 entity, for example Contact, Membership, Group, Activity, CustomValue, etc.',
567
- displayOptions: { show: { resource: ['customApi'] } },
568
- },
569
- {
570
- displayName: 'Action',
571
- name: 'customAction',
572
- type: 'string',
573
- default: 'get',
574
- required: true,
575
- description:
576
- 'CiviCRM API4 action, for example get, getFields, create, update, delete, getOne, etc.',
577
- displayOptions: { show: { resource: ['customApi'] } },
578
- },
579
- {
580
- displayName: 'Params (JSON)',
581
- name: 'customParamsJson',
582
- type: 'string',
583
- typeOptions: {
584
- rows: 4,
585
- },
586
- default: '{\n "limit": 25\n}',
587
- description:
588
- 'Raw API4 params JSON passed as-is to CiviCRM. It must be a valid JSON object (no trailing commas).',
589
- displayOptions: { show: { resource: ['customApi'] } },
590
- },
591
-
592
- //
593
- // DYNAMIC FIELDS
594
- //
595
- ...genericFields,
596
- ...upsertFields,
597
- ],
598
- };
599
-
600
- /* ============================================================================
601
- LOAD OPTIONS (required by n8n verification)
602
- ============================================================================ */
603
-
604
- methods = {
605
- loadOptions: {
606
- async loadOptionValues(this: ILoadOptionsFunctions) {
607
- const { url, apiKey } = (await this.getCredentials('civiCrmApi')) as {
608
- url: string;
609
- apiKey: string;
610
- };
611
-
612
- const res = await this.helpers.httpRequest({
613
- method: 'POST',
614
- url: `${url.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`,
615
- headers: {
616
- Authorization: `Bearer ${apiKey}`,
617
- 'Content-Type': 'application/x-www-form-urlencoded',
618
- },
619
- body: { params: JSON.stringify({ limit: 50, select: ['id', 'label'] }) },
620
- json: true,
621
- });
622
-
623
- const values = (res?.values || []) as Array<{ id: number; label: string }>;
624
-
625
- return values.map((v) => ({
626
- name: v.label,
627
- value: v.id,
628
- }));
629
- },
630
- },
631
- };
632
-
633
- /* ============================================================================
634
- EXECUTE
635
- ============================================================================ */
636
-
637
- async execute(this: IExecuteFunctions) {
638
- const items = this.getInputData();
639
- const out: INodeExecutionData[] = [];
640
-
641
- const resource = this.getNodeParameter('resource', 0) as Resource;
642
- const operation = this.getNodeParameter('operation', 0) as Operation;
643
-
644
- // Custom API call: generic passthrough to any API4 entity/action
645
- if (resource === 'customApi') {
646
- const entity = this.getNodeParameter('customEntity', 0) as string;
647
- const action = this.getNodeParameter('customAction', 0) as string;
648
- const paramsJson = this.getNodeParameter('customParamsJson', 0, '') as string;
649
-
650
- let params: Record<string, unknown> = {};
651
- if (paramsJson) {
652
- try {
653
- params = JSON.parse(paramsJson);
654
- } catch (error) {
655
- throw new Error('Invalid JSON in "Params (JSON)"');
656
- }
657
- }
658
-
659
- const res = await civicrmApiRequest.call(
660
- this,
661
- 'POST',
662
- `/civicrm/ajax/api4/${entity}/${action}`,
663
- params,
664
- );
665
-
666
- // Return the raw API4 response so advanced users can work with values and metadata
667
- out.push({
668
- json: res as IDataObject,
669
- pairedItem: { item: 0 },
670
- });
671
- return [out];
672
- }
673
-
674
- const entity = ENTITY_MAP[resource];
675
-
676
- for (let i = 0; i < items.length; i++) {
677
- const emailLocationParam = this.getNodeParameter('emailLocation', i, 'Work') as string;
678
- const phoneLocationParam = this.getNodeParameter('phoneLocation', i, 'Work') as string;
679
- const addressLocationParam = this.getNodeParameter('addressLocation', i, 'Home') as string;
680
- const isPrimaryEmail = this.getNodeParameter('isPrimaryEmail', i, true) as boolean;
681
- const isPrimaryPhone = this.getNodeParameter('isPrimaryPhone', i, true) as boolean;
682
- const isPrimaryAddress = this.getNodeParameter('isPrimaryAddress', i, true) as boolean;
683
-
684
- let emailLocationName = emailLocationParam;
685
- let phoneLocationName = phoneLocationParam;
686
- let addressLocationName = addressLocationParam;
687
-
688
- /* ======================================================
689
- GET
690
- ====================================================== */
691
- if (operation === 'get') {
692
- const id = this.getNodeParameter('id', i) as number;
693
-
694
- const params =
695
- resource === 'contact'
696
- ? {
697
- where: [['id', '=', id]],
698
- limit: 1,
699
- select: [
700
- 'id',
701
- 'display_name',
702
- 'first_name',
703
- 'last_name',
704
- 'organization_name',
705
- 'legal_name',
706
- 'contact_type',
707
- 'gender_id',
708
- 'gender_id:name',
709
- 'birth_date',
710
- ],
711
- chain: {
712
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
713
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
714
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
715
- },
716
- }
717
- : {
718
- where: [['id', '=', id]],
719
- limit: 1,
720
- select: ['id', 'name', 'title', 'subject', 'display_name'],
721
- };
722
-
723
- const res = await civicrmApiRequest.call(
724
- this,
725
- 'POST',
726
- `/civicrm/ajax/api4/${entity}/get`,
727
- params,
728
- );
729
-
730
- out.push({
731
- json: res?.values?.[0] ?? {},
732
- pairedItem: { item: i },
733
- });
734
- continue;
735
- }
736
-
737
- /* ======================================================
738
- GET MANY
739
- ====================================================== */
740
- if (operation === 'getMany') {
741
- const limit = this.getNodeParameter('limit', i, 100) as number;
742
- const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
743
- const whereJson = this.getNodeParameter('whereJson', i, '') as string;
744
-
745
- let where: any[] = [];
746
- if (whereJson) {
747
- try {
748
- where = JSON.parse(whereJson);
749
- } catch (error) {
750
- throw new Error('Invalid JSON in whereJson');
751
- }
752
- }
753
-
754
- if (resource === 'contact') {
755
- const contactType = this.getNodeParameter('contactType', i, '') as string;
756
- if (contactType) where.push(['contact_type', '=', contactType]);
757
- }
758
-
759
- const params =
760
- resource === 'contact'
761
- ? {
762
- where,
763
- select: [
764
- 'id',
765
- 'display_name',
766
- 'first_name',
767
- 'last_name',
768
- 'organization_name',
769
- 'legal_name',
770
- 'contact_type',
771
- 'gender_id',
772
- 'gender_id:name',
773
- 'birth_date',
774
- ],
775
- chain: {
776
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
777
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
778
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
779
- },
780
- }
781
- : {
782
- where,
783
- select: ['id', 'name', 'title', 'subject', 'display_name'],
784
- };
785
-
786
- if (returnAll) {
787
- let offset = 0;
788
- const page = 500;
789
- let hasMore = true;
790
-
791
- while (hasMore) {
792
- const r = await civicrmApiRequest.call(
793
- this,
794
- 'POST',
795
- `/civicrm/ajax/api4/${entity}/get`,
796
- { ...params, limit: page, offset },
797
- );
798
-
799
- const vals = r?.values ?? [];
800
- for (const v of vals) {
801
- out.push({
802
- json: v,
803
- pairedItem: { item: i },
804
- });
805
- }
806
-
807
- if (vals.length < page) {
808
- hasMore = false;
809
- } else {
810
- offset += page;
811
- }
812
- }
813
- } else {
814
- const r = await civicrmApiRequest.call(
815
- this,
816
- 'POST',
817
- `/civicrm/ajax/api4/${entity}/get`,
818
- { ...params, limit },
819
- );
820
-
821
- const vals = r?.values ?? [];
822
- for (const v of vals) {
823
- out.push({
824
- json: v,
825
- pairedItem: { item: i },
826
- });
827
- }
828
- }
829
-
830
- continue;
831
- }
832
-
833
- /* ======================================================
834
- DELETE
835
- ====================================================== */
836
- if (operation === 'delete') {
837
- const id = this.getNodeParameter('id', i) as number;
838
-
839
- const r = await civicrmApiRequest.call(
840
- this,
841
- 'POST',
842
- `/civicrm/ajax/api4/${entity}/delete`,
843
- {
844
- where: [['id', '=', id]],
845
- },
846
- );
847
-
848
- out.push({
849
- json: {
850
- success: true,
851
- message: `${entity} ${id} deleted`,
852
- deleted_id: id,
853
- api_response: r,
854
- },
855
- pairedItem: { item: i },
856
- });
857
-
858
- continue;
859
- }
860
-
861
- /* ======================================================
862
- CREATE / UPDATE
863
- ====================================================== */
864
-
865
- const isCreate = operation === 'create';
866
- const id = !isCreate ? (this.getNodeParameter('id', i) as number) : undefined;
867
-
868
- const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
869
- fieldName: string;
870
- fieldValue: string;
871
- }>;
872
-
873
- const values: Record<string, unknown> = {};
874
- const emailData: Record<string, unknown> = {};
875
- const phoneData: Record<string, unknown> = {};
876
- const addressData: Record<string, unknown> = {};
877
-
878
- let locationTypeMap: Record<string, string> = {};
879
- if (resource === 'contact') {
880
- locationTypeMap = await getLocationTypeMap.call(this);
881
- }
882
-
883
- for (const p of pairs) {
884
- if (!p.fieldName) continue;
885
-
886
- const key = p.fieldName.trim();
887
- const rawVal = convertValue(p.fieldValue);
888
- const val = rawVal;
889
-
890
- /* Email simple */
891
- if (key === 'email') {
892
- if (val !== '' && val !== null && val !== undefined) {
893
- emailData.email = val;
894
- }
895
- continue;
896
- }
897
- if (key.startsWith('email.')) {
898
- if (val !== '' && val !== null && val !== undefined) {
899
- emailData[key.replace(/^email\./, '')] = val;
900
- }
901
- continue;
902
- }
903
-
904
- /* Phone simple */
905
- if (key === 'phone') {
906
- if (val !== '' && val !== null && val !== undefined) {
907
- phoneData.phone = val;
908
- }
909
- continue;
910
- }
911
- if (key.startsWith('phone.')) {
912
- if (val !== '' && val !== null && val !== undefined) {
913
- phoneData[key.replace(/^phone\./, '')] = val;
914
- }
915
- continue;
916
- }
917
-
918
- /* Address */
919
- if (key.startsWith('address.')) {
920
- if (val !== '' && val !== null && val !== undefined) {
921
- addressData[key.replace(/^address\./, '')] = val;
922
- }
923
- continue;
924
- }
925
-
926
- /* Prefijo dinámico */
927
- const segments = key.split('.');
928
- if (segments.length >= 2 && resource === 'contact') {
929
- const prefixRaw = segments[0];
930
- const root = segments[1];
931
- const subfield = segments.slice(2).join('.') || '';
932
-
933
- const normalizedPrefix = normalizeLocationKey(prefixRaw);
934
- const mappedLocationName = locationTypeMap[normalizedPrefix];
935
-
936
- if (root === 'email' || root === 'phone' || root === 'address') {
937
- if (mappedLocationName) {
938
- if (root === 'email') emailLocationName = mappedLocationName;
939
- if (root === 'phone') phoneLocationName = mappedLocationName;
940
- if (root === 'address') addressLocationName = mappedLocationName;
941
- }
942
-
943
- if (root === 'email') {
944
- if (val !== '' && val !== null && val !== undefined) {
945
- if (!subfield) emailData.email = val;
946
- else emailData[subfield] = val;
947
- }
948
- continue;
949
- }
950
-
951
- if (root === 'phone') {
952
- if (val !== '' && val !== null && val !== undefined) {
953
- if (!subfield) phoneData.phone = val;
954
- else phoneData[subfield] = val;
955
- }
956
- continue;
957
- }
958
-
959
- if (root === 'address') {
960
- if (val !== '' && val !== null && val !== undefined) {
961
- if (subfield) addressData[subfield] = val;
962
- }
963
- continue;
964
- }
965
- }
966
- }
967
-
968
- /* gender */
969
- if (key === 'gender' || key === 'gender_id') {
970
- if (val !== '' && val !== null && val !== undefined) {
971
- values.gender_id = val;
972
- }
973
- continue;
974
- }
975
-
976
- /* birth_date */
977
- if (key === 'birth_date' || key === 'birth') {
978
- if (val !== '' && val !== null && val !== undefined) {
979
- values.birth_date = normalizeBirthDate(String(val));
980
- }
981
- continue;
982
- }
983
-
984
- /* default */
985
- if (val !== '' && val !== null && val !== undefined) {
986
- values[key] = val;
987
- }
988
- }
989
-
990
- /* Contact type */
991
- const contactType = this.getNodeParameter('contactType', i, '') as string;
992
- if (resource === 'contact' && contactType) {
993
- values.contact_type = contactType;
994
- }
995
-
996
- /* CREATE/UPDATE contact */
997
- let contactId = id;
998
- if (isCreate) {
999
- const r = await civicrmApiRequest.call(
1000
- this,
1001
- 'POST',
1002
- '/civicrm/ajax/api4/Contact/create',
1003
- { values },
1004
- );
1005
- contactId = r?.values?.[0]?.id;
1006
- if (!contactId) throw new Error('Failed to create contact.');
1007
- } else {
1008
- await civicrmApiRequest.call(
1009
- this,
1010
- 'POST',
1011
- `/civicrm/ajax/api4/${entity}/update`,
1012
- {
1013
- values,
1014
- where: [['id', '=', contactId]],
1015
- },
1016
- );
1017
- }
1018
-
1019
- /* SUBENTITIES */
1020
- if (resource === 'contact') {
1021
- if (isCreate && isPrimaryEmail) {
1022
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/delete', {
1023
- where: [
1024
- ['contact_id', '=', contactId],
1025
- ['is_primary', '=', true],
1026
- ],
1027
- });
1028
- }
1029
- if (isCreate && isPrimaryPhone) {
1030
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/delete', {
1031
- where: [
1032
- ['contact_id', '=', contactId],
1033
- ['is_primary', '=', true],
1034
- ],
1035
- });
1036
- }
1037
- if (isCreate && isPrimaryAddress) {
1038
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/delete', {
1039
- where: [
1040
- ['contact_id', '=', contactId],
1041
- ['is_primary', '=', true],
1042
- ],
1043
- });
1044
- }
1045
-
1046
- if (Object.keys(emailData).length) {
1047
- if (isCreate) {
1048
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
1049
- values: {
1050
- ...emailData,
1051
- contact_id: contactId,
1052
- is_primary: isPrimaryEmail,
1053
- 'location_type_id:name': emailLocationName,
1054
- },
1055
- });
1056
- } else {
1057
- const existingEmail = await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/get', {
1058
- where: [
1059
- ['contact_id', '=', contactId],
1060
- ['location_type_id:name', '=', emailLocationName],
1061
- ],
1062
- limit: 1,
1063
- select: ['id'],
1064
- });
1065
- const existingEmailId = existingEmail?.values?.[0]?.id as number | undefined;
1066
- if (existingEmailId) {
1067
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/update', {
1068
- values: {
1069
- id: existingEmailId,
1070
- ...emailData,
1071
- contact_id: contactId,
1072
- is_primary: isPrimaryEmail,
1073
- },
1074
- });
1075
- } else {
1076
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
1077
- values: {
1078
- ...emailData,
1079
- contact_id: contactId,
1080
- is_primary: isPrimaryEmail,
1081
- 'location_type_id:name': emailLocationName,
1082
- },
1083
- });
1084
- }
1085
- }
1086
- }
1087
-
1088
- if (Object.keys(phoneData).length) {
1089
- if (isCreate) {
1090
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
1091
- values: {
1092
- ...phoneData,
1093
- contact_id: contactId,
1094
- is_primary: isPrimaryPhone,
1095
- 'location_type_id:name': phoneLocationName,
1096
- },
1097
- });
1098
- } else {
1099
- const existingPhone = await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/get', {
1100
- where: [
1101
- ['contact_id', '=', contactId],
1102
- ['location_type_id:name', '=', phoneLocationName],
1103
- ],
1104
- limit: 1,
1105
- select: ['id'],
1106
- });
1107
- const existingPhoneId = existingPhone?.values?.[0]?.id as number | undefined;
1108
- if (existingPhoneId) {
1109
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/update', {
1110
- values: {
1111
- id: existingPhoneId,
1112
- ...phoneData,
1113
- contact_id: contactId,
1114
- is_primary: isPrimaryPhone,
1115
- },
1116
- });
1117
- } else {
1118
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
1119
- values: {
1120
- ...phoneData,
1121
- contact_id: contactId,
1122
- is_primary: isPrimaryPhone,
1123
- 'location_type_id:name': phoneLocationName,
1124
- },
1125
- });
1126
- }
1127
- }
1128
- }
1129
-
1130
-
1131
- if (Object.keys(addressData).length) {
1132
- if (isCreate) {
1133
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
1134
- values: {
1135
- ...addressData,
1136
- contact_id: contactId,
1137
- is_primary: isPrimaryAddress,
1138
- 'location_type_id:name': addressLocationName,
1139
- },
1140
- });
1141
- } else {
1142
- const existingAddress = await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/get', {
1143
- where: [
1144
- ['contact_id', '=', contactId],
1145
- ['location_type_id:name', '=', addressLocationName],
1146
- ],
1147
- limit: 1,
1148
- select: ['id'],
1149
- });
1150
- const existingAddressId = existingAddress?.values?.[0]?.id as number | undefined;
1151
- if (existingAddressId) {
1152
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/update', {
1153
- values: {
1154
- id: existingAddressId,
1155
- ...addressData,
1156
- contact_id: contactId,
1157
- is_primary: isPrimaryAddress,
1158
- },
1159
- });
1160
- } else {
1161
- await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
1162
- values: {
1163
- ...addressData,
1164
- contact_id: contactId,
1165
- is_primary: isPrimaryAddress,
1166
- 'location_type_id:name': addressLocationName,
1167
- },
1168
- });
1169
- }
1170
- }
1171
- }
1172
- }
1173
-
1174
- /* FINAL GET */
1175
- const res = await civicrmApiRequest.call(
1176
- this,
1177
- 'POST',
1178
- `/civicrm/ajax/api4/${entity}/get`,
1179
- {
1180
- where: [['id', '=', contactId]],
1181
- select: [
1182
- 'id',
1183
- 'display_name',
1184
- 'first_name',
1185
- 'last_name',
1186
- 'organization_name',
1187
- 'legal_name',
1188
- 'contact_type',
1189
- 'gender_id',
1190
- 'gender_id:name',
1191
- 'birth_date',
1192
- ],
1193
- ...(resource === 'contact'
1194
- ? {
1195
- chain: {
1196
- emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
1197
- phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
1198
- addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
1199
- },
1200
- }
1201
- : {}),
1202
- },
1203
- );
1204
-
1205
- out.push({
1206
- json: res?.values?.[0] ?? {},
1207
- pairedItem: { item: i },
1208
- });
1209
- }
1210
-
1211
- return [out];
1212
- }
1213
- }
1214
-
1215
- /* ============================================================================
1216
- UTILS
1217
- ============================================================================ */
1218
-
1219
- function convertValue(val: string): unknown {
1220
- const t = String(val ?? '').trim();
1221
- if (t === '') return '';
1222
- if (t === 'true') return true;
1223
- if (t === 'false') return false;
1224
- if (/^-?\d+(\.\d+)?$/.test(t)) return Number(t);
1225
- try {
1226
- const j = JSON.parse(t);
1227
- if (typeof j === 'object') return j;
1228
- } catch (error) {
1229
- // Not valid JSON, return original value
1230
- }
1231
- return val;
1232
- }
1233
-
1234
- function normalizeBirthDate(input: string): string {
1235
- if (!input) return input;
1236
- let v = input.trim();
1237
-
1238
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
1239
- // Already in correct format
1240
- } else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
1241
- const [d, m, y] = v.split('/');
1242
- v = `${y}-${m}-${d}`;
1243
- } else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
1244
- const [d, m, y] = v.split('-');
1245
- v = `${y}-${m}-${d}`;
1246
- } else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
1247
- v = v.replace(/\//g, '-');
1248
- } else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
1249
- v = v.replace(/\./g, '-');
1250
- }
1251
-
1252
- const dt = new Date(v);
1253
- if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
1254
- return v;
1255
- }
1256
-
1257
- export default CiviCrm;