@ixiam/n8n-nodes-civicrm 0.3.8 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,4 +9,5 @@ Credentials:
9
9
  - Use the **Save** button to verify connectivity.
10
10
 
11
11
  Entities: Contact, Event, Case, Contribution, Membership → `get|getMany|create|update|delete`
12
- Form Processor: `execute` with `recordsJson`
12
+
13
+ Node developed by Ixiam Global Solution: `https://www.ixiam.com'
@@ -35,6 +35,65 @@ class CiviCrm {
35
35
  properties: [
36
36
  resources_1.resourceProp,
37
37
  resources_1.operationProp,
38
+ {
39
+ displayName: 'Contact Type',
40
+ name: 'contactType',
41
+ type: 'options',
42
+ default: 'Individual',
43
+ options: [
44
+ { name: 'Individual', value: 'Individual' },
45
+ { name: 'Organization', value: 'Organization' },
46
+ { name: 'Household', value: 'Household' },
47
+ ],
48
+ displayOptions: { show: { resource: ['contact'] } },
49
+ description: 'Filter or assign contact type',
50
+ },
51
+ {
52
+ displayName: 'Email Location Type',
53
+ name: 'emailLocation',
54
+ type: 'options',
55
+ default: 'Work',
56
+ options: [
57
+ { name: 'Home', value: 'Home' },
58
+ { name: 'Work', value: 'Work' },
59
+ { name: 'Other', value: 'Other' },
60
+ ],
61
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
62
+ },
63
+ {
64
+ displayName: 'Phone Location Type',
65
+ name: 'phoneLocation',
66
+ type: 'options',
67
+ default: 'Work',
68
+ options: [
69
+ { name: 'Home', value: 'Home' },
70
+ { name: 'Work', value: 'Work' },
71
+ { name: 'Mobile', value: 'Mobile' },
72
+ { name: 'Other', value: 'Other' },
73
+ ],
74
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
75
+ },
76
+ {
77
+ displayName: 'Address Location Type',
78
+ name: 'addressLocation',
79
+ type: 'options',
80
+ default: 'Home',
81
+ options: [
82
+ { name: 'Home', value: 'Home' },
83
+ { name: 'Work', value: 'Work' },
84
+ { name: 'Billing', value: 'Billing' },
85
+ { name: 'Other', value: 'Other' },
86
+ ],
87
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
88
+ },
89
+ {
90
+ displayName: 'Mark as Primary',
91
+ name: 'isPrimary',
92
+ type: 'boolean',
93
+ default: true,
94
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
95
+ description: 'If enabled, replaces previous primary email/phone/address with this one',
96
+ },
38
97
  {
39
98
  displayName: 'ID',
40
99
  name: 'id',
@@ -59,9 +118,7 @@ class CiviCrm {
59
118
  'X-Civi-Auth': `Bearer ${apiToken}`,
60
119
  'Content-Type': 'application/x-www-form-urlencoded',
61
120
  },
62
- body: {
63
- params: JSON.stringify({ limit: 5 }),
64
- },
121
+ body: { params: JSON.stringify({ limit: 5 }) },
65
122
  json: true,
66
123
  });
67
124
  const values = (res?.values || []);
@@ -80,61 +137,69 @@ class CiviCrm {
80
137
  const operation = this.getNodeParameter('operation', 0);
81
138
  const entity = ENTITY_MAP[resource];
82
139
  for (let i = 0; i < items.length; i++) {
140
+ const emailLocation = this.getNodeParameter('emailLocation', i, 'Work');
141
+ const phoneLocation = this.getNodeParameter('phoneLocation', i, 'Work');
142
+ const addressLocation = this.getNodeParameter('addressLocation', i, 'Home');
143
+ const isPrimary = this.getNodeParameter('isPrimary', i, true);
83
144
  // === GET ===
84
145
  if (operation === 'get') {
85
146
  const id = this.getNodeParameter('id', i);
86
- const params = {
87
- where: [['id', '=', id]],
88
- limit: 1,
89
- select: [
90
- 'id',
91
- 'display_name',
92
- 'first_name',
93
- 'last_name',
94
- 'contact_type',
95
- 'email.email',
96
- 'phone.phone',
97
- 'address.city',
98
- 'address.country_id:label',
99
- 'address.postal_code',
100
- 'address.street_address',
101
- ],
102
- join: [
103
- ['Email AS email', 'LEFT'],
104
- ['Phone AS phone', 'LEFT'],
105
- ['Address AS address', 'LEFT'],
106
- ],
107
- };
108
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
109
- out.push({ json: (res?.values?.[0] ?? {}) });
147
+ const where = [['id', '=', id]];
148
+ // Solo para Contact → usar chain
149
+ if (resource === 'contact') {
150
+ const params = {
151
+ where,
152
+ limit: 1,
153
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
154
+ chain: {
155
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
156
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
157
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
158
+ },
159
+ };
160
+ const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Contact/get`, params);
161
+ out.push({ json: (res?.values?.[0] ?? {}) });
162
+ }
163
+ else {
164
+ // Otras entidades → select estándar
165
+ const params = {
166
+ where,
167
+ limit: 1,
168
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
169
+ };
170
+ const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
171
+ out.push({ json: (res?.values?.[0] ?? {}) });
172
+ }
110
173
  }
111
174
  // === GET MANY ===
112
175
  if (operation === 'getMany') {
113
176
  const returnAll = this.getNodeParameter('returnAll', i, false);
114
177
  const limit = this.getNodeParameter('limit', i, 100);
115
178
  const whereJson = this.getNodeParameter('whereJson', i, '');
116
- const where = whereJson ? JSON.parse(whereJson) : undefined;
117
- const baseParams = {
118
- where,
119
- select: [
120
- 'id',
121
- 'display_name',
122
- 'first_name',
123
- 'last_name',
124
- 'contact_type',
125
- 'email.email',
126
- 'phone.phone',
127
- 'address.city',
128
- 'address.country_id:label',
129
- 'address.postal_code',
130
- 'address.street_address',
131
- ],
132
- join: [
133
- ['Email AS email', 'LEFT'],
134
- ['Phone AS phone', 'LEFT'],
135
- ['Address AS address', 'LEFT'],
136
- ],
137
- };
179
+ let where = whereJson ? JSON.parse(whereJson) : [];
180
+ if (resource === 'contact') {
181
+ const contactType = this.getNodeParameter('contactType', i, '');
182
+ if (contactType)
183
+ where.push(['contact_type', '=', contactType]);
184
+ }
185
+ let baseParams;
186
+ if (resource === 'contact') {
187
+ baseParams = {
188
+ where,
189
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
190
+ chain: {
191
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
192
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
193
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
194
+ },
195
+ };
196
+ }
197
+ else {
198
+ baseParams = {
199
+ where,
200
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
201
+ };
202
+ }
138
203
  if (returnAll) {
139
204
  let offset = 0;
140
205
  const page = 500;
@@ -155,47 +220,90 @@ class CiviCrm {
155
220
  out.push({ json: v });
156
221
  }
157
222
  }
158
- // === CREATE ===
159
- if (operation === 'create') {
223
+ // === CREATE y UPDATE ===
224
+ const isCreate = operation === 'create';
225
+ if (isCreate || operation === 'update') {
226
+ const id = !isCreate ? this.getNodeParameter('id', i) : undefined;
160
227
  const pairs = this.getNodeParameter('fields.field', i, []);
161
- const values = Object.fromEntries(pairs
162
- .filter((p) => p.fieldName)
163
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]));
164
- const params = { values };
165
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/create`, params);
166
- out.push({ json: res });
167
- }
168
- // === UPDATE ===
169
- if (operation === 'update') {
170
- const id = this.getNodeParameter('id', i);
171
- const pairs = this.getNodeParameter('fields.field', i, []);
172
- const values = Object.fromEntries(pairs
173
- .filter((p) => p.fieldName)
174
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]));
175
- const params = {
176
- values,
177
- where: [['id', '=', id]],
178
- };
179
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, params);
180
- out.push({ json: res });
181
- }
182
- // === DELETE ===
183
- if (operation === 'delete') {
184
- const id = this.getNodeParameter('id', i);
185
- const params = {
186
- where: [['id', '=', id]],
187
- };
188
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/delete`, params);
189
- out.push({ json: res });
228
+ const values = {};
229
+ const emailData = {};
230
+ const phoneData = {};
231
+ const addressData = {};
232
+ for (const p of pairs) {
233
+ if (!p.fieldName)
234
+ continue;
235
+ const key = p.fieldName.trim();
236
+ const val = convertValue(p.fieldValue);
237
+ if (key.startsWith('email.'))
238
+ emailData[key.replace(/^email\./, '')] = val;
239
+ else if (key.startsWith('phone.'))
240
+ phoneData[key.replace(/^phone\./, '')] = val;
241
+ else if (key.startsWith('address.'))
242
+ addressData[key.replace(/^address\./, '')] = val;
243
+ else
244
+ values[key] = val;
245
+ }
246
+ const contactType = this.getNodeParameter('contactType', i, '');
247
+ if (resource === 'contact' && contactType)
248
+ values.contact_type = contactType;
249
+ let contactId = id;
250
+ if (isCreate) {
251
+ const contactRes = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Contact/create`, { values });
252
+ contactId = contactRes?.values?.[0]?.id;
253
+ if (!contactId)
254
+ throw new Error('Failed to create contact.');
255
+ }
256
+ else {
257
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, {
258
+ values,
259
+ where: [['id', '=', contactId]],
260
+ });
261
+ }
262
+ if (resource === 'contact') {
263
+ if (isPrimary) {
264
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Email/delete`, {
265
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
266
+ });
267
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Phone/delete`, {
268
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
269
+ });
270
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Address/delete`, {
271
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
272
+ });
273
+ }
274
+ if (Object.keys(emailData).length)
275
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Email/create`, {
276
+ values: { ...emailData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': emailLocation },
277
+ });
278
+ if (Object.keys(phoneData).length)
279
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Phone/create`, {
280
+ values: { ...phoneData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': phoneLocation },
281
+ });
282
+ if (Object.keys(addressData).length)
283
+ await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Address/create`, {
284
+ values: { ...addressData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': addressLocation },
285
+ });
286
+ }
287
+ const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, {
288
+ where: [['id', '=', contactId]],
289
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
290
+ ...(resource === 'contact'
291
+ ? {
292
+ chain: {
293
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
294
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
295
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
296
+ },
297
+ }
298
+ : {}),
299
+ });
300
+ out.push({ json: (res?.values?.[0] ?? {}) });
190
301
  }
191
302
  }
192
303
  return [out];
193
304
  }
194
305
  }
195
306
  exports.CiviCrm = CiviCrm;
196
- /**
197
- * Convierte string a número, boolean, objeto o deja string
198
- */
199
307
  function convertValue(val) {
200
308
  const t = String(val ?? '').trim();
201
309
  if (t === '')
@@ -214,5 +322,4 @@ function convertValue(val) {
214
322
  catch { }
215
323
  return val;
216
324
  }
217
- // Requerido por n8n >=1.110
218
325
  exports.default = CiviCrm;
@@ -60,6 +60,65 @@ export class CiviCrm implements INodeType {
60
60
  properties: [
61
61
  resourceProp,
62
62
  operationProp,
63
+ {
64
+ displayName: 'Contact Type',
65
+ name: 'contactType',
66
+ type: 'options',
67
+ default: 'Individual',
68
+ options: [
69
+ { name: 'Individual', value: 'Individual' },
70
+ { name: 'Organization', value: 'Organization' },
71
+ { name: 'Household', value: 'Household' },
72
+ ],
73
+ displayOptions: { show: { resource: ['contact'] } },
74
+ description: 'Filter or assign contact type',
75
+ },
76
+ {
77
+ displayName: 'Email Location Type',
78
+ name: 'emailLocation',
79
+ type: 'options',
80
+ default: 'Work',
81
+ options: [
82
+ { name: 'Home', value: 'Home' },
83
+ { name: 'Work', value: 'Work' },
84
+ { name: 'Other', value: 'Other' },
85
+ ],
86
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
87
+ },
88
+ {
89
+ displayName: 'Phone Location Type',
90
+ name: 'phoneLocation',
91
+ type: 'options',
92
+ default: 'Work',
93
+ options: [
94
+ { name: 'Home', value: 'Home' },
95
+ { name: 'Work', value: 'Work' },
96
+ { name: 'Mobile', value: 'Mobile' },
97
+ { name: 'Other', value: 'Other' },
98
+ ],
99
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
100
+ },
101
+ {
102
+ displayName: 'Address Location Type',
103
+ name: 'addressLocation',
104
+ type: 'options',
105
+ default: 'Home',
106
+ options: [
107
+ { name: 'Home', value: 'Home' },
108
+ { name: 'Work', value: 'Work' },
109
+ { name: 'Billing', value: 'Billing' },
110
+ { name: 'Other', value: 'Other' },
111
+ ],
112
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
113
+ },
114
+ {
115
+ displayName: 'Mark as Primary',
116
+ name: 'isPrimary',
117
+ type: 'boolean',
118
+ default: true,
119
+ displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
120
+ description: 'If enabled, replaces previous primary email/phone/address with this one',
121
+ },
63
122
  {
64
123
  displayName: 'ID',
65
124
  name: 'id',
@@ -82,7 +141,6 @@ export class CiviCrm implements INodeType {
82
141
  };
83
142
 
84
143
  const url = `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`;
85
-
86
144
  const res = await this.helpers.httpRequest({
87
145
  method: 'POST',
88
146
  url,
@@ -90,9 +148,7 @@ export class CiviCrm implements INodeType {
90
148
  'X-Civi-Auth': `Bearer ${apiToken}`,
91
149
  'Content-Type': 'application/x-www-form-urlencoded',
92
150
  },
93
- body: {
94
- params: JSON.stringify({ limit: 5 }),
95
- },
151
+ body: { params: JSON.stringify({ limit: 5 }) },
96
152
  json: true,
97
153
  });
98
154
 
@@ -105,186 +161,195 @@ export class CiviCrm implements INodeType {
105
161
  },
106
162
  };
107
163
 
108
- async execute(this: IExecuteFunctions) {
109
- const items = this.getInputData();
110
- const out: INodeExecutionData[] = [];
111
-
112
- const resource = this.getNodeParameter('resource', 0) as Resource;
113
- const operation = this.getNodeParameter('operation', 0) as Operation;
114
- const entity = ENTITY_MAP[resource];
115
-
116
- for (let i = 0; i < items.length; i++) {
117
- // === GET ===
118
- if (operation === 'get') {
119
- const id = this.getNodeParameter('id', i) as number;
164
+ async execute(this: IExecuteFunctions) {
165
+ const items = this.getInputData();
166
+ const out: INodeExecutionData[] = [];
120
167
 
121
- const params = {
122
- where: [['id', '=', id]],
123
- limit: 1,
124
- select: [
125
- 'id',
126
- 'display_name',
127
- 'first_name',
128
- 'last_name',
129
- 'contact_type',
130
- 'email.email',
131
- 'phone.phone',
132
- 'address.city',
133
- 'address.country_id:label',
134
- 'address.postal_code',
135
- 'address.street_address',
136
- ],
137
- join: [
138
- ['Email AS email', 'LEFT'],
139
- ['Phone AS phone', 'LEFT'],
140
- ['Address AS address', 'LEFT'],
141
- ],
142
- };
168
+ const resource = this.getNodeParameter('resource', 0) as Resource;
169
+ const operation = this.getNodeParameter('operation', 0) as Operation;
170
+ const entity = ENTITY_MAP[resource];
143
171
 
144
- const res = await civicrmApiRequest.call(
145
- this,
146
- 'POST',
147
- `/civicrm/ajax/api4/${entity}/get`,
148
- params,
149
- );
172
+ for (let i = 0; i < items.length; i++) {
173
+ const emailLocation = this.getNodeParameter('emailLocation', i, 'Work') as string;
174
+ const phoneLocation = this.getNodeParameter('phoneLocation', i, 'Work') as string;
175
+ const addressLocation = this.getNodeParameter('addressLocation', i, 'Home') as string;
176
+ const isPrimary = this.getNodeParameter('isPrimary', i, true) as boolean;
150
177
 
151
- out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
152
- }
153
-
154
- // === GET MANY ===
155
- if (operation === 'getMany') {
156
- const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
157
- const limit = this.getNodeParameter('limit', i, 100) as number;
158
- const whereJson = this.getNodeParameter('whereJson', i, '') as string;
159
- const where = whereJson ? JSON.parse(whereJson) : undefined;
178
+ // === GET ===
179
+ if (operation === 'get') {
180
+ const id = this.getNodeParameter('id', i) as number;
181
+ const where = [['id', '=', id]];
160
182
 
161
- const baseParams = {
162
- where,
163
- select: [
164
- 'id',
165
- 'display_name',
166
- 'first_name',
167
- 'last_name',
168
- 'contact_type',
169
- 'email.email',
170
- 'phone.phone',
171
- 'address.city',
172
- 'address.country_id:label',
173
- 'address.postal_code',
174
- 'address.street_address',
175
- ],
176
- join: [
177
- ['Email AS email', 'LEFT'],
178
- ['Phone AS phone', 'LEFT'],
179
- ['Address AS address', 'LEFT'],
180
- ],
181
- };
183
+ // Solo para Contact → usar chain
184
+ if (resource === 'contact') {
185
+ const params = {
186
+ where,
187
+ limit: 1,
188
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
189
+ chain: {
190
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
191
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
192
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
193
+ },
194
+ };
195
+ const res = await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Contact/get`, params);
196
+ out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
197
+ } else {
198
+ // Otras entidades → select estándar
199
+ const params = {
200
+ where,
201
+ limit: 1,
202
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
203
+ };
204
+ const res = await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
205
+ out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
206
+ }
207
+ }
182
208
 
183
- if (returnAll) {
184
- let offset = 0;
185
- const page = 500;
186
- while (true) {
187
- const res = await civicrmApiRequest.call(
188
- this,
189
- 'POST',
190
- `/civicrm/ajax/api4/${entity}/get`,
191
- { ...baseParams, limit: page, offset },
192
- );
193
- const vals = (res?.values ?? []) as IDataObject[];
194
- for (const v of vals) out.push({ json: v });
195
- if (vals.length < page) break;
196
- offset += page;
197
- }
198
- } else {
199
- const res = await civicrmApiRequest.call(
200
- this,
201
- 'POST',
202
- `/civicrm/ajax/api4/${entity}/get`,
203
- { ...baseParams, limit },
204
- );
205
- const vals = (res?.values ?? []) as IDataObject[];
206
- for (const v of vals) out.push({ json: v });
207
- }
208
- }
209
+ // === GET MANY ===
210
+ if (operation === 'getMany') {
211
+ const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
212
+ const limit = this.getNodeParameter('limit', i, 100) as number;
213
+ const whereJson = this.getNodeParameter('whereJson', i, '') as string;
209
214
 
215
+ let where = whereJson ? JSON.parse(whereJson) : [];
210
216
 
211
- // === CREATE ===
212
- if (operation === 'create') {
213
- const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
214
- fieldName: string;
215
- fieldValue: string;
216
- }>;
217
+ if (resource === 'contact') {
218
+ const contactType = this.getNodeParameter('contactType', i, '') as string;
219
+ if (contactType) where.push(['contact_type', '=', contactType]);
220
+ }
217
221
 
218
- const values = Object.fromEntries(
219
- pairs
220
- .filter((p) => p.fieldName)
221
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]),
222
- );
222
+ let baseParams: Record<string, unknown>;
223
223
 
224
- const params = { values };
224
+ if (resource === 'contact') {
225
+ baseParams = {
226
+ where,
227
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
228
+ chain: {
229
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
230
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
231
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
232
+ },
233
+ };
234
+ } else {
235
+ baseParams = {
236
+ where,
237
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
238
+ };
239
+ }
225
240
 
241
+ if (returnAll) {
242
+ let offset = 0;
243
+ const page = 500;
244
+ while (true) {
245
+ const res = await civicrmApiRequest.call(
246
+ this,
247
+ 'POST',
248
+ `/civicrm/ajax/api4/${entity}/get`,
249
+ { ...baseParams, limit: page, offset },
250
+ );
251
+ const vals = (res?.values ?? []) as IDataObject[];
252
+ for (const v of vals) out.push({ json: v });
253
+ if (vals.length < page) break;
254
+ offset += page;
255
+ }
256
+ } else {
226
257
  const res = await civicrmApiRequest.call(
227
258
  this,
228
259
  'POST',
229
- `/civicrm/ajax/api4/${entity}/create`,
230
- params,
260
+ `/civicrm/ajax/api4/${entity}/get`,
261
+ { ...baseParams, limit },
231
262
  );
232
- out.push({ json: res as IDataObject });
263
+ const vals = (res?.values ?? []) as IDataObject[];
264
+ for (const v of vals) out.push({ json: v });
233
265
  }
266
+ }
234
267
 
235
- // === UPDATE ===
236
- if (operation === 'update') {
237
- const id = this.getNodeParameter('id', i) as number;
238
- const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
239
- fieldName: string;
240
- fieldValue: string;
241
- }>;
268
+ // === CREATE y UPDATE ===
269
+ const isCreate = operation === 'create';
270
+ if (isCreate || operation === 'update') {
271
+ const id = !isCreate ? (this.getNodeParameter('id', i) as number) : undefined;
272
+ const pairs = this.getNodeParameter('fields.field', i, []) as Array<{ fieldName: string; fieldValue: string }>;
273
+ const values: Record<string, unknown> = {};
274
+ const emailData: Record<string, unknown> = {};
275
+ const phoneData: Record<string, unknown> = {};
276
+ const addressData: Record<string, unknown> = {};
277
+
278
+ for (const p of pairs) {
279
+ if (!p.fieldName) continue;
280
+ const key = p.fieldName.trim();
281
+ const val = convertValue(p.fieldValue);
282
+ if (key.startsWith('email.')) emailData[key.replace(/^email\./, '')] = val;
283
+ else if (key.startsWith('phone.')) phoneData[key.replace(/^phone\./, '')] = val;
284
+ else if (key.startsWith('address.')) addressData[key.replace(/^address\./, '')] = val;
285
+ else values[key] = val;
286
+ }
242
287
 
243
- const values = Object.fromEntries(
244
- pairs
245
- .filter((p) => p.fieldName)
246
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]),
247
- );
288
+ const contactType = this.getNodeParameter('contactType', i, '') as string;
289
+ if (resource === 'contact' && contactType) values.contact_type = contactType;
248
290
 
249
- const params = {
291
+ let contactId = id;
292
+ if (isCreate) {
293
+ const contactRes = await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Contact/create`, { values });
294
+ contactId = contactRes?.values?.[0]?.id;
295
+ if (!contactId) throw new Error('Failed to create contact.');
296
+ } else {
297
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/update`, {
250
298
  values,
251
- where: [['id', '=', id]],
252
- };
299
+ where: [['id', '=', contactId]],
300
+ });
301
+ }
253
302
 
254
- const res = await civicrmApiRequest.call(
255
- this,
256
- 'POST',
257
- `/civicrm/ajax/api4/${entity}/update`,
258
- params,
259
- );
260
- out.push({ json: res as IDataObject });
303
+ if (resource === 'contact') {
304
+ if (isPrimary) {
305
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Email/delete`, {
306
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
307
+ });
308
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Phone/delete`, {
309
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
310
+ });
311
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Address/delete`, {
312
+ where: [['contact_id', '=', contactId], ['is_primary', '=', true]],
313
+ });
314
+ }
315
+
316
+ if (Object.keys(emailData).length)
317
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Email/create`, {
318
+ values: { ...emailData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': emailLocation },
319
+ });
320
+ if (Object.keys(phoneData).length)
321
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Phone/create`, {
322
+ values: { ...phoneData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': phoneLocation },
323
+ });
324
+ if (Object.keys(addressData).length)
325
+ await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Address/create`, {
326
+ values: { ...addressData, contact_id: contactId, is_primary: isPrimary, 'location_type_id:name': addressLocation },
327
+ });
261
328
  }
262
329
 
263
- // === DELETE ===
264
- if (operation === 'delete') {
265
- const id = this.getNodeParameter('id', i) as number;
330
+ const res = await civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, {
331
+ where: [['id', '=', contactId]],
332
+ select: ['id', 'display_name', 'first_name', 'last_name', 'contact_type'],
333
+ ...(resource === 'contact'
334
+ ? {
335
+ chain: {
336
+ emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
337
+ phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
338
+ addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
339
+ },
340
+ }
341
+ : {}),
342
+ });
343
+ out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
344
+ }
345
+ }
266
346
 
267
- const params = {
268
- where: [['id', '=', id]],
269
- };
347
+ return [out];
348
+ }
270
349
 
271
- const res = await civicrmApiRequest.call(
272
- this,
273
- 'POST',
274
- `/civicrm/ajax/api4/${entity}/delete`,
275
- params,
276
- );
277
- out.push({ json: res as IDataObject });
278
- }
279
- }
280
350
 
281
- return [out];
282
- }
283
351
  }
284
352
 
285
- /**
286
- * Convierte string a número, boolean, objeto o deja string
287
- */
288
353
  function convertValue(val: string): unknown {
289
354
  const t = String(val ?? '').trim();
290
355
  if (t === '') return '';
@@ -298,5 +363,4 @@ function convertValue(val: string): unknown {
298
363
  return val;
299
364
  }
300
365
 
301
- // Requerido por n8n >=1.110
302
366
  export default CiviCrm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixiam/n8n-nodes-civicrm",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "description": "CiviCRM API v4",
5
5
  "license": "MIT",
6
6
  "keywords": [