@ixiam/n8n-nodes-civicrm 0.3.9 → 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.
@@ -45,11 +45,55 @@ class CiviCrm {
45
45
  { name: 'Organization', value: 'Organization' },
46
46
  { name: 'Household', value: 'Household' },
47
47
  ],
48
- displayOptions: {
49
- show: { resource: ['contact'] },
50
- },
48
+ displayOptions: { show: { resource: ['contact'] } },
51
49
  description: 'Filter or assign contact type',
52
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
+ },
53
97
  {
54
98
  displayName: 'ID',
55
99
  name: 'id',
@@ -74,9 +118,7 @@ class CiviCrm {
74
118
  'X-Civi-Auth': `Bearer ${apiToken}`,
75
119
  'Content-Type': 'application/x-www-form-urlencoded',
76
120
  },
77
- body: {
78
- params: JSON.stringify({ limit: 5 }),
79
- },
121
+ body: { params: JSON.stringify({ limit: 5 }) },
80
122
  json: true,
81
123
  });
82
124
  const values = (res?.values || []);
@@ -95,86 +137,68 @@ class CiviCrm {
95
137
  const operation = this.getNodeParameter('operation', 0);
96
138
  const entity = ENTITY_MAP[resource];
97
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);
98
144
  // === GET ===
99
145
  if (operation === 'get') {
100
146
  const id = this.getNodeParameter('id', i);
101
- const contactType = this.getNodeParameter('contactType', i, '');
102
147
  const where = [['id', '=', id]];
103
- if (resource === 'contact' && contactType) {
104
- where.push(['contact_type', '=', contactType]);
105
- }
106
- let params;
148
+ // Solo para Contact usar chain
107
149
  if (resource === 'contact') {
108
- params = {
150
+ const params = {
109
151
  where,
110
152
  limit: 1,
111
- select: [
112
- 'id',
113
- 'display_name',
114
- 'first_name',
115
- 'last_name',
116
- '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'],
128
- ],
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
+ },
129
159
  };
160
+ const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/Contact/get`, params);
161
+ out.push({ json: (res?.values?.[0] ?? {}) });
130
162
  }
131
163
  else {
132
- // otras entidades: select básico
133
- params = {
164
+ // Otras entidades select estándar
165
+ const params = {
134
166
  where,
135
167
  limit: 1,
136
- select: ['id', 'title', 'subject', 'display_name', 'name'],
168
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
137
169
  };
170
+ const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
171
+ out.push({ json: (res?.values?.[0] ?? {}) });
138
172
  }
139
- const res = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, params);
140
- out.push({ json: (res?.values?.[0] ?? {}) });
141
173
  }
142
174
  // === GET MANY ===
143
175
  if (operation === 'getMany') {
144
176
  const returnAll = this.getNodeParameter('returnAll', i, false);
145
177
  const limit = this.getNodeParameter('limit', i, 100);
146
178
  const whereJson = this.getNodeParameter('whereJson', i, '');
147
- const contactType = this.getNodeParameter('contactType', i, '');
148
179
  let where = whereJson ? JSON.parse(whereJson) : [];
149
- if (resource === 'contact' && contactType) {
150
- where.push(['contact_type', '=', contactType]);
180
+ if (resource === 'contact') {
181
+ const contactType = this.getNodeParameter('contactType', i, '');
182
+ if (contactType)
183
+ where.push(['contact_type', '=', contactType]);
151
184
  }
152
185
  let baseParams;
153
186
  if (resource === 'contact') {
154
187
  baseParams = {
155
188
  where,
156
- select: [
157
- 'id',
158
- 'display_name',
159
- 'first_name',
160
- 'last_name',
161
- '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'],
173
- ],
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
+ },
174
195
  };
175
196
  }
176
197
  else {
177
- baseParams = { where, select: ['id', 'title', 'subject', 'display_name', 'name'] };
198
+ baseParams = {
199
+ where,
200
+ select: ['id', 'name', 'title', 'subject', 'display_name'],
201
+ };
178
202
  }
179
203
  if (returnAll) {
180
204
  let offset = 0;
@@ -196,55 +220,90 @@ class CiviCrm {
196
220
  out.push({ json: v });
197
221
  }
198
222
  }
199
- // === CREATE ===
200
- 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;
201
227
  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;
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;
208
245
  }
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)]));
220
246
  const contactType = this.getNodeParameter('contactType', i, '');
221
- if (resource === 'contact' && contactType) {
247
+ if (resource === 'contact' && contactType)
222
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.');
223
255
  }
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 });
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] ?? {}) });
239
301
  }
240
302
  }
241
303
  return [out];
242
304
  }
243
305
  }
244
306
  exports.CiviCrm = CiviCrm;
245
- /**
246
- * Convierte string a número, boolean, objeto o deja string
247
- */
248
307
  function convertValue(val) {
249
308
  const t = String(val ?? '').trim();
250
309
  if (t === '')
@@ -263,5 +322,4 @@ function convertValue(val) {
263
322
  catch { }
264
323
  return val;
265
324
  }
266
- // Requerido por n8n >=1.110
267
325
  exports.default = CiviCrm;
@@ -70,11 +70,55 @@ export class CiviCrm implements INodeType {
70
70
  { name: 'Organization', value: 'Organization' },
71
71
  { name: 'Household', value: 'Household' },
72
72
  ],
73
- displayOptions: {
74
- show: { resource: ['contact'] },
75
- },
73
+ displayOptions: { show: { resource: ['contact'] } },
76
74
  description: 'Filter or assign contact type',
77
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
+ },
78
122
  {
79
123
  displayName: 'ID',
80
124
  name: 'id',
@@ -97,7 +141,6 @@ export class CiviCrm implements INodeType {
97
141
  };
98
142
 
99
143
  const url = `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`;
100
-
101
144
  const res = await this.helpers.httpRequest({
102
145
  method: 'POST',
103
146
  url,
@@ -105,9 +148,7 @@ export class CiviCrm implements INodeType {
105
148
  'X-Civi-Auth': `Bearer ${apiToken}`,
106
149
  'Content-Type': 'application/x-www-form-urlencoded',
107
150
  },
108
- body: {
109
- params: JSON.stringify({ limit: 5 }),
110
- },
151
+ body: { params: JSON.stringify({ limit: 5 }) },
111
152
  json: true,
112
153
  });
113
154
 
@@ -120,224 +161,195 @@ export class CiviCrm implements INodeType {
120
161
  },
121
162
  };
122
163
 
123
- async execute(this: IExecuteFunctions) {
124
- const items = this.getInputData();
125
- const out: INodeExecutionData[] = [];
164
+ async execute(this: IExecuteFunctions) {
165
+ const items = this.getInputData();
166
+ const out: INodeExecutionData[] = [];
126
167
 
127
- const resource = this.getNodeParameter('resource', 0) as Resource;
128
- const operation = this.getNodeParameter('operation', 0) as Operation;
129
- const entity = ENTITY_MAP[resource];
168
+ const resource = this.getNodeParameter('resource', 0) as Resource;
169
+ const operation = this.getNodeParameter('operation', 0) as Operation;
170
+ const entity = ENTITY_MAP[resource];
130
171
 
131
- for (let i = 0; i < items.length; i++) {
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;
132
177
 
133
- // === GET ===
134
- if (operation === 'get') {
135
- const id = this.getNodeParameter('id', i) as number;
136
- const contactType = this.getNodeParameter('contactType', i, '') as string;
178
+ // === GET ===
179
+ if (operation === 'get') {
180
+ const id = this.getNodeParameter('id', i) as number;
181
+ const where = [['id', '=', id]];
137
182
 
138
- const where = [['id', '=', id]];
139
- if (resource === 'contact' && contactType) {
140
- where.push(['contact_type', '=', contactType]);
141
- }
142
-
143
- let params: Record<string, unknown>;
144
-
145
- if (resource === 'contact') {
146
- params = {
147
- where,
148
- limit: 1,
149
- select: [
150
- 'id',
151
- 'display_name',
152
- 'first_name',
153
- 'last_name',
154
- 'contact_type',
155
- 'email.email',
156
- 'phone.phone',
157
- 'address.city',
158
- 'address.country_id:label',
159
- 'address.postal_code',
160
- 'address.street_address',
161
- ],
162
- join: [
163
- ['Email AS email', 'LEFT'],
164
- ['Phone AS phone', 'LEFT'],
165
- ['Address AS address', 'LEFT'],
166
- ],
167
- };
168
- } else {
169
- // otras entidades: select básico
170
- params = {
171
- where,
172
- limit: 1,
173
- select: ['id', 'title', 'subject', 'display_name', 'name'],
174
- };
175
- }
176
-
177
- const res = await civicrmApiRequest.call(
178
- this,
179
- 'POST',
180
- `/civicrm/ajax/api4/${entity}/get`,
181
- params,
182
- );
183
-
184
- out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
185
- }
186
-
187
- // === GET MANY ===
188
- if (operation === 'getMany') {
189
- const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
190
- const limit = this.getNodeParameter('limit', i, 100) as number;
191
- const whereJson = this.getNodeParameter('whereJson', i, '') as string;
192
- const contactType = this.getNodeParameter('contactType', i, '') as string;
193
-
194
- let where = whereJson ? JSON.parse(whereJson) : [];
195
- if (resource === 'contact' && contactType) {
196
- where.push(['contact_type', '=', contactType]);
197
- }
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
+ }
198
208
 
199
- let baseParams: Record<string, unknown>;
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;
200
214
 
201
- if (resource === 'contact') {
202
- baseParams = {
203
- where,
204
- select: [
205
- 'id',
206
- 'display_name',
207
- 'first_name',
208
- 'last_name',
209
- 'contact_type',
210
- 'email.email',
211
- 'phone.phone',
212
- 'address.city',
213
- 'address.country_id:label',
214
- 'address.postal_code',
215
- 'address.street_address',
216
- ],
217
- join: [
218
- ['Email AS email', 'LEFT'],
219
- ['Phone AS phone', 'LEFT'],
220
- ['Address AS address', 'LEFT'],
221
- ],
222
- };
223
- } else {
224
- baseParams = { where, select: ['id', 'title', 'subject', 'display_name', 'name'] };
225
- }
215
+ let where = whereJson ? JSON.parse(whereJson) : [];
226
216
 
227
- if (returnAll) {
228
- let offset = 0;
229
- const page = 500;
230
- while (true) {
231
- const res = await civicrmApiRequest.call(
232
- this,
233
- 'POST',
234
- `/civicrm/ajax/api4/${entity}/get`,
235
- { ...baseParams, limit: page, offset },
236
- );
237
- const vals = (res?.values ?? []) as IDataObject[];
238
- for (const v of vals) out.push({ json: v });
239
- if (vals.length < page) break;
240
- offset += page;
241
- }
242
- } else {
243
- const res = await civicrmApiRequest.call(
244
- this,
245
- 'POST',
246
- `/civicrm/ajax/api4/${entity}/get`,
247
- { ...baseParams, limit },
248
- );
249
- const vals = (res?.values ?? []) as IDataObject[];
250
- for (const v of vals) out.push({ json: v });
251
- }
252
- }
217
+ if (resource === 'contact') {
218
+ const contactType = this.getNodeParameter('contactType', i, '') as string;
219
+ if (contactType) where.push(['contact_type', '=', contactType]);
220
+ }
253
221
 
254
- // === CREATE ===
255
- if (operation === 'create') {
256
- const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
257
- fieldName: string;
258
- fieldValue: string;
259
- }>;
222
+ let baseParams: Record<string, unknown>;
260
223
 
261
- const values = Object.fromEntries(
262
- pairs
263
- .filter((p) => p.fieldName)
264
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]),
265
- );
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
+ }
266
240
 
267
- const contactType = this.getNodeParameter('contactType', i, '') as string;
268
- if (resource === 'contact' && contactType) {
269
- values.contact_type = contactType;
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;
270
255
  }
271
-
272
- const params = { values };
273
-
256
+ } else {
274
257
  const res = await civicrmApiRequest.call(
275
258
  this,
276
259
  'POST',
277
- `/civicrm/ajax/api4/${entity}/create`,
278
- params,
260
+ `/civicrm/ajax/api4/${entity}/get`,
261
+ { ...baseParams, limit },
279
262
  );
280
- out.push({ json: res as IDataObject });
263
+ const vals = (res?.values ?? []) as IDataObject[];
264
+ for (const v of vals) out.push({ json: v });
281
265
  }
266
+ }
282
267
 
283
- // === UPDATE ===
284
- if (operation === 'update') {
285
- const id = this.getNodeParameter('id', i) as number;
286
- const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
287
- fieldName: string;
288
- fieldValue: string;
289
- }>;
290
-
291
- const values = Object.fromEntries(
292
- pairs
293
- .filter((p) => p.fieldName)
294
- .map((p) => [p.fieldName, convertValue(p.fieldValue)]),
295
- );
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
+ }
296
287
 
297
- const contactType = this.getNodeParameter('contactType', i, '') as string;
298
- if (resource === 'contact' && contactType) {
299
- values.contact_type = contactType;
300
- }
288
+ const contactType = this.getNodeParameter('contactType', i, '') as string;
289
+ if (resource === 'contact' && contactType) values.contact_type = contactType;
301
290
 
302
- 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`, {
303
298
  values,
304
- where: [['id', '=', id]],
305
- };
306
-
307
- const res = await civicrmApiRequest.call(
308
- this,
309
- 'POST',
310
- `/civicrm/ajax/api4/${entity}/update`,
311
- params,
312
- );
313
- out.push({ json: res as IDataObject });
299
+ where: [['id', '=', contactId]],
300
+ });
314
301
  }
315
302
 
316
- // === DELETE ===
317
- if (operation === 'delete') {
318
- const id = this.getNodeParameter('id', i) as number;
319
-
320
- const params = {
321
- where: [['id', '=', id]],
322
- };
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
+ }
323
315
 
324
- const res = await civicrmApiRequest.call(
325
- this,
326
- 'POST',
327
- `/civicrm/ajax/api4/${entity}/delete`,
328
- params,
329
- );
330
- out.push({ json: res as IDataObject });
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
+ });
331
328
  }
332
- }
333
329
 
334
- return [out];
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
+ }
335
345
  }
346
+
347
+ return [out];
348
+ }
349
+
350
+
336
351
  }
337
352
 
338
- /**
339
- * Convierte string a número, boolean, objeto o deja string
340
- */
341
353
  function convertValue(val: string): unknown {
342
354
  const t = String(val ?? '').trim();
343
355
  if (t === '') return '';
@@ -351,5 +363,4 @@ function convertValue(val: string): unknown {
351
363
  return val;
352
364
  }
353
365
 
354
- // Requerido por n8n >=1.110
355
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.9",
3
+ "version": "0.4.1",
4
4
  "description": "CiviCRM API v4",
5
5
  "license": "MIT",
6
6
  "keywords": [