@ixiam/n8n-nodes-civicrm 0.4.4 → 1.0.0
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 +105 -8
- package/credentials/CiviCrmApi.credentials.ts +50 -53
- package/dist/credentials/CiviCrmApi.credentials.d.ts +1 -5
- package/dist/credentials/CiviCrmApi.credentials.js +7 -10
- package/dist/nodes/CiviCrm/CiviCrm.node.d.ts +5 -5
- package/dist/nodes/CiviCrm/CiviCrm.node.js +302 -336
- package/dist/nodes/CiviCrm/descriptions/resources.js +11 -5
- package/jest.config.cjs +9 -0
- package/nodes/CiviCrm/CiviCrm.node.ts +385 -449
- package/nodes/CiviCrm/descriptions/resources.ts +11 -5
- package/package.json +21 -7
- package/test/helpers/mockServer.ts +27 -0
- package/test/nodes/CiviCrm.test.ts +67 -0
- package/test/tsconfig.json +10 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import type {
|
|
3
2
|
IExecuteFunctions,
|
|
4
3
|
ILoadOptionsFunctions,
|
|
@@ -13,42 +12,31 @@ import { civicrmApiRequest } from '../transport/GenericFunctions';
|
|
|
13
12
|
import { resourceProp, operationProp } from './descriptions/resources';
|
|
14
13
|
import { genericFields, upsertFields } from './descriptions/generic';
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
15
|
+
/* ============================================================================
|
|
16
|
+
RESOURCES
|
|
17
|
+
============================================================================ */
|
|
18
|
+
|
|
19
19
|
type Resource =
|
|
20
20
|
| 'contact'
|
|
21
|
-
| 'event'
|
|
22
|
-
| 'case'
|
|
23
|
-
| 'contribution'
|
|
24
21
|
| 'membership'
|
|
25
|
-
| 'participant'
|
|
26
22
|
| 'group'
|
|
27
23
|
| 'relationship'
|
|
28
|
-
| 'email'
|
|
29
24
|
| 'activity';
|
|
30
25
|
|
|
26
|
+
type Operation = 'get' | 'getMany' | 'create' | 'update' | 'delete';
|
|
27
|
+
|
|
31
28
|
const ENTITY_MAP: Record<Resource, string> = {
|
|
32
29
|
contact: 'Contact',
|
|
33
|
-
event: 'Event',
|
|
34
|
-
case: 'Case',
|
|
35
|
-
contribution: 'Contribution',
|
|
36
30
|
membership: 'Membership',
|
|
37
|
-
participant: 'Participant',
|
|
38
31
|
group: 'Group',
|
|
39
32
|
relationship: 'Relationship',
|
|
40
|
-
email: 'Email',
|
|
41
33
|
activity: 'Activity',
|
|
42
34
|
};
|
|
43
35
|
|
|
44
|
-
|
|
36
|
+
/* ============================================================================
|
|
37
|
+
LOCATION TYPE CACHE
|
|
38
|
+
============================================================================ */
|
|
45
39
|
|
|
46
|
-
/**
|
|
47
|
-
* Caché global de location types
|
|
48
|
-
* Se rellena la primera vez que se usa y dura hasta que se reinicia n8n.
|
|
49
|
-
* key: normalizado (lowercase, sin espacios/ símbolos)
|
|
50
|
-
* value: option_value.name (para usar en location_type_id:name)
|
|
51
|
-
*/
|
|
52
40
|
let locationTypeCache: Record<string, string> | null = null;
|
|
53
41
|
|
|
54
42
|
function normalizeLocationKey(s: string): string {
|
|
@@ -66,27 +54,26 @@ async function getLocationTypeMap(this: IExecuteFunctions): Promise<Record<strin
|
|
|
66
54
|
limit: 0,
|
|
67
55
|
});
|
|
68
56
|
|
|
69
|
-
const values = (res?.values || []) as Array<{ name?: string; label?: string }>;
|
|
70
57
|
const map: Record<string, string> = {};
|
|
58
|
+
const values = (res?.values || []) as Array<{ name?: string; label?: string }>;
|
|
71
59
|
|
|
72
60
|
for (const v of values) {
|
|
73
61
|
const name = v.name || '';
|
|
74
62
|
const label = v.label || '';
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (normName) map[normName] = name;
|
|
80
|
-
if (normLabel) map[normLabel] = name;
|
|
63
|
+
const k1 = normalizeLocationKey(name);
|
|
64
|
+
const k2 = normalizeLocationKey(label);
|
|
65
|
+
if (k1) map[k1] = name;
|
|
66
|
+
if (k2) map[k2] = name;
|
|
81
67
|
}
|
|
82
68
|
|
|
83
69
|
locationTypeCache = map;
|
|
84
70
|
return map;
|
|
85
71
|
}
|
|
86
72
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*/
|
|
73
|
+
/* ============================================================================
|
|
74
|
+
NODE DEFINITION
|
|
75
|
+
============================================================================ */
|
|
76
|
+
|
|
90
77
|
export class CiviCrm implements INodeType {
|
|
91
78
|
description: INodeTypeDescription = {
|
|
92
79
|
displayName: 'CiviCRM',
|
|
@@ -95,43 +82,25 @@ export class CiviCrm implements INodeType {
|
|
|
95
82
|
group: ['transform'],
|
|
96
83
|
version: 1,
|
|
97
84
|
description:
|
|
98
|
-
'Interact with CiviCRM API v4 (Civi-Go compatible)
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
' Example prefixes (from Location Types): Home, Work, Billing, Mobile, Oficina, etc.\\n' +
|
|
103
|
-
'- Prefix format: <location-prefix>.<entity>[.<subfield>]\\n' +
|
|
104
|
-
' Examples:\\n' +
|
|
105
|
-
' email = test@ixiam.com\\n' +
|
|
106
|
-
' phone = 600123123\\n' +
|
|
107
|
-
' address.city = Barcelona\\n' +
|
|
108
|
-
' work.email = test@company.com (Location Type = Work)\\n' +
|
|
109
|
-
' billing.address.postal_code = 28010 (Location Type = Billing)\\n' +
|
|
110
|
-
' home.phone.phone_type_id = 2 (Location Type = Home)\\n\\n' +
|
|
111
|
-
'Subfields examples:\\n' +
|
|
112
|
-
'- email.is_primary, email.signature_html, email.signature_text\\n' +
|
|
113
|
-
'- phone.phone_type_id, phone.phone_ext\\n' +
|
|
114
|
-
'- address.country_id, address.state_province_id, address.postal_code, address.street_address\\n\\n' +
|
|
115
|
-
'Prefixes and Location Types:\\n' +
|
|
116
|
-
'- The prefix (e.g. work, home, billing, oficina) is matched against CiviCRM Location Types (name/label).\\n' +
|
|
117
|
-
'- If the prefix matches, the node sets location_type_id:name automatically.\\n' +
|
|
118
|
-
'- If no prefix is used (e.g. email, phone, address.city), the node uses the Location Type selectors.\\n\\n' +
|
|
119
|
-
'birth_date:\\n' +
|
|
120
|
-
'- Accepted formats: YYYY-MM-DD, DD/MM/YYYY, DD-MM-YYYY, YYYY/MM/DD, YYYY.MM.DD.\\n' +
|
|
121
|
-
'- The node normalizes and validates the date before sending it to CiviCRM.\\n\\n' +
|
|
122
|
-
'Filters in GET MANY:\\n' +
|
|
123
|
-
'- Use API4 "where" JSON array format, e.g.:\\n' +
|
|
124
|
-
' [ ["contact_type","=","Individual"] ]\\n' +
|
|
125
|
-
' [ ["first_name","LIKE","Jul%"] ]\\n' +
|
|
126
|
-
' [ ["birth_date",">","1990-01-01"] ]\\n' +
|
|
127
|
-
' [ ["gender_id","IN",[1,2]] ]',
|
|
85
|
+
'Interact with CiviCRM API v4 (Civi-Go compatible).\n\n' +
|
|
86
|
+
'Supports Contact, Membership, Group, Relationship and Activity entities.\n' +
|
|
87
|
+
'Includes dynamic mapping of email, phone, address and location types.\n' +
|
|
88
|
+
'Includes birth_date validation and JSON filters for GET MANY.\n',
|
|
128
89
|
defaults: { name: 'CiviCRM' },
|
|
129
90
|
inputs: ['main'],
|
|
130
91
|
outputs: ['main'],
|
|
131
92
|
credentials: [{ name: 'civiCrmApi', required: true }],
|
|
93
|
+
|
|
132
94
|
properties: [
|
|
95
|
+
//
|
|
96
|
+
// RESOURCE + OPERATION
|
|
97
|
+
//
|
|
133
98
|
resourceProp,
|
|
134
99
|
operationProp,
|
|
100
|
+
|
|
101
|
+
//
|
|
102
|
+
// CONTACT TYPE
|
|
103
|
+
//
|
|
135
104
|
{
|
|
136
105
|
displayName: 'Contact Type',
|
|
137
106
|
name: 'contactType',
|
|
@@ -143,10 +112,11 @@ export class CiviCrm implements INodeType {
|
|
|
143
112
|
{ name: 'Household', value: 'Household' },
|
|
144
113
|
],
|
|
145
114
|
displayOptions: { show: { resource: ['contact'] } },
|
|
146
|
-
description:
|
|
147
|
-
'Contact type to filter (GET MANY) or assign (CREATE/UPDATE). ' +
|
|
148
|
-
'For GET MANY, if set, a condition [ "contact_type", "=", selectedType ] is added to the where.',
|
|
149
115
|
},
|
|
116
|
+
|
|
117
|
+
//
|
|
118
|
+
// LOCATION TYPES
|
|
119
|
+
//
|
|
150
120
|
{
|
|
151
121
|
displayName: 'Email Location Type',
|
|
152
122
|
name: 'emailLocation',
|
|
@@ -158,11 +128,6 @@ export class CiviCrm implements INodeType {
|
|
|
158
128
|
{ name: 'Other', value: 'Other' },
|
|
159
129
|
],
|
|
160
130
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
161
|
-
description:
|
|
162
|
-
'Default Location Type used for email when no prefix is provided.\\n' +
|
|
163
|
-
'- If you use "email" or "email.xxx" as field name, this Location Type will be used.\\n' +
|
|
164
|
-
'- If you use a prefixed field like "work.email", "home.email", etc., ' +
|
|
165
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
166
131
|
},
|
|
167
132
|
{
|
|
168
133
|
displayName: 'Phone Location Type',
|
|
@@ -176,11 +141,6 @@ export class CiviCrm implements INodeType {
|
|
|
176
141
|
{ name: 'Other', value: 'Other' },
|
|
177
142
|
],
|
|
178
143
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
179
|
-
description:
|
|
180
|
-
'Default Location Type used for phone when no prefix is provided.\\n' +
|
|
181
|
-
'- If you use "phone" or "phone.xxx" as field name, this Location Type will be used.\\n' +
|
|
182
|
-
'- If you use a prefixed field like "mobile.phone", "work.phone", etc., ' +
|
|
183
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
184
144
|
},
|
|
185
145
|
{
|
|
186
146
|
displayName: 'Address Location Type',
|
|
@@ -194,22 +154,22 @@ export class CiviCrm implements INodeType {
|
|
|
194
154
|
{ name: 'Other', value: 'Other' },
|
|
195
155
|
],
|
|
196
156
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
197
|
-
description:
|
|
198
|
-
'Default Location Type used for address when no prefix is provided.\\n' +
|
|
199
|
-
'- If you use "address.city", "address.country_id", etc., this Location Type will be used.\\n' +
|
|
200
|
-
'- If you use a prefixed field like "billing.address.postal_code", "home.address.city", etc., ' +
|
|
201
|
-
'the prefix overrides this selector and maps to the matching CiviCRM Location Type.',
|
|
202
157
|
},
|
|
158
|
+
|
|
159
|
+
//
|
|
160
|
+
// PRIMARY FLAG
|
|
161
|
+
//
|
|
203
162
|
{
|
|
204
163
|
displayName: 'Mark as Primary',
|
|
205
164
|
name: 'isPrimary',
|
|
206
165
|
type: 'boolean',
|
|
207
166
|
default: true,
|
|
208
167
|
displayOptions: { show: { resource: ['contact'], operation: ['create', 'update'] } },
|
|
209
|
-
description:
|
|
210
|
-
'If enabled, previous primary email/phone/address for this contact are deleted before creating the new ones. ' +
|
|
211
|
-
'Useful when you want to replace existing primary contact details.',
|
|
212
168
|
},
|
|
169
|
+
|
|
170
|
+
//
|
|
171
|
+
// ID for GET / UPDATE / DELETE
|
|
172
|
+
//
|
|
213
173
|
{
|
|
214
174
|
displayName: 'ID',
|
|
215
175
|
name: 'id',
|
|
@@ -217,35 +177,42 @@ export class CiviCrm implements INodeType {
|
|
|
217
177
|
default: 0,
|
|
218
178
|
required: true,
|
|
219
179
|
displayOptions: { show: { operation: ['get', 'update', 'delete'] } },
|
|
220
|
-
description: 'Record ID of the selected entity (Contact, Event, Case, etc.).',
|
|
221
180
|
},
|
|
181
|
+
|
|
182
|
+
//
|
|
183
|
+
// DYNAMIC FIELDS
|
|
184
|
+
//
|
|
222
185
|
...genericFields,
|
|
223
186
|
...upsertFields,
|
|
224
187
|
],
|
|
225
188
|
};
|
|
226
189
|
|
|
190
|
+
/* ============================================================================
|
|
191
|
+
LOAD OPTIONS (required by n8n verification)
|
|
192
|
+
============================================================================ */
|
|
193
|
+
|
|
227
194
|
methods = {
|
|
228
195
|
loadOptions: {
|
|
229
|
-
async
|
|
196
|
+
async loadOptionValues(this: ILoadOptionsFunctions) {
|
|
230
197
|
const { baseUrl, apiToken } = (await this.getCredentials('civiCrmApi')) as {
|
|
231
198
|
baseUrl: string;
|
|
232
199
|
apiToken: string;
|
|
233
200
|
};
|
|
234
201
|
|
|
235
|
-
const url = `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`;
|
|
236
202
|
const res = await this.helpers.httpRequest({
|
|
237
203
|
method: 'POST',
|
|
238
|
-
url
|
|
204
|
+
url: `${baseUrl.replace(/\/$/, '')}/civicrm/ajax/api4/OptionValue/get`,
|
|
239
205
|
headers: {
|
|
240
206
|
'X-Civi-Auth': `Bearer ${apiToken}`,
|
|
241
207
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
242
208
|
},
|
|
243
|
-
body: { params: JSON.stringify({ limit:
|
|
209
|
+
body: { params: JSON.stringify({ limit: 50, select: ['id', 'label'] }) },
|
|
244
210
|
json: true,
|
|
245
211
|
});
|
|
246
212
|
|
|
247
213
|
const values = (res?.values || []) as Array<{ id: number; label: string }>;
|
|
248
|
-
|
|
214
|
+
|
|
215
|
+
return values.map((v) => ({
|
|
249
216
|
name: v.label,
|
|
250
217
|
value: v.id,
|
|
251
218
|
}));
|
|
@@ -253,6 +220,10 @@ export class CiviCrm implements INodeType {
|
|
|
253
220
|
},
|
|
254
221
|
};
|
|
255
222
|
|
|
223
|
+
/* ============================================================================
|
|
224
|
+
EXECUTE
|
|
225
|
+
============================================================================ */
|
|
226
|
+
|
|
256
227
|
async execute(this: IExecuteFunctions) {
|
|
257
228
|
const items = this.getInputData();
|
|
258
229
|
const out: INodeExecutionData[] = [];
|
|
@@ -267,451 +238,416 @@ export class CiviCrm implements INodeType {
|
|
|
267
238
|
const addressLocationParam = this.getNodeParameter('addressLocation', i, 'Home') as string;
|
|
268
239
|
const isPrimary = this.getNodeParameter('isPrimary', i, true) as boolean;
|
|
269
240
|
|
|
270
|
-
// Valores de location type efectivos (pueden ser sobreescritos por prefijo)
|
|
271
241
|
let emailLocationName = emailLocationParam;
|
|
272
242
|
let phoneLocationName = phoneLocationParam;
|
|
273
243
|
let addressLocationName = addressLocationParam;
|
|
274
244
|
|
|
275
|
-
|
|
245
|
+
/* ======================================================
|
|
246
|
+
GET
|
|
247
|
+
====================================================== */
|
|
276
248
|
if (operation === 'get') {
|
|
277
249
|
const id = this.getNodeParameter('id', i) as number;
|
|
278
|
-
const where = [['id', '=', id]];
|
|
279
250
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
`/civicrm/ajax/api4/${entity}/get`,
|
|
317
|
-
params,
|
|
318
|
-
);
|
|
319
|
-
out.push({ json: (res?.values?.[0] ?? {}) as IDataObject });
|
|
320
|
-
}
|
|
251
|
+
const params =
|
|
252
|
+
resource === 'contact'
|
|
253
|
+
? {
|
|
254
|
+
where: [['id', '=', id]],
|
|
255
|
+
limit: 1,
|
|
256
|
+
select: [
|
|
257
|
+
'id',
|
|
258
|
+
'display_name',
|
|
259
|
+
'first_name',
|
|
260
|
+
'last_name',
|
|
261
|
+
'contact_type',
|
|
262
|
+
'gender_id',
|
|
263
|
+
'gender_id:name',
|
|
264
|
+
'birth_date',
|
|
265
|
+
],
|
|
266
|
+
chain: {
|
|
267
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
268
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
269
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
: {
|
|
273
|
+
where: [['id', '=', id]],
|
|
274
|
+
limit: 1,
|
|
275
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const res = await civicrmApiRequest.call(
|
|
279
|
+
this,
|
|
280
|
+
'POST',
|
|
281
|
+
`/civicrm/ajax/api4/${entity}/get`,
|
|
282
|
+
params,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
out.push({ json: res?.values?.[0] ?? {} });
|
|
286
|
+
continue;
|
|
321
287
|
}
|
|
322
288
|
|
|
323
|
-
|
|
289
|
+
/* ======================================================
|
|
290
|
+
GET MANY
|
|
291
|
+
====================================================== */
|
|
324
292
|
if (operation === 'getMany') {
|
|
325
|
-
const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
|
|
326
293
|
const limit = this.getNodeParameter('limit', i, 100) as number;
|
|
294
|
+
const returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
|
|
327
295
|
const whereJson = this.getNodeParameter('whereJson', i, '') as string;
|
|
328
296
|
|
|
329
|
-
let where =
|
|
297
|
+
let where: any[] = [];
|
|
298
|
+
if (whereJson) {
|
|
299
|
+
try {
|
|
300
|
+
where = JSON.parse(whereJson);
|
|
301
|
+
} catch {
|
|
302
|
+
throw new Error('Invalid JSON in whereJson');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
330
305
|
|
|
331
306
|
if (resource === 'contact') {
|
|
332
307
|
const contactType = this.getNodeParameter('contactType', i, '') as string;
|
|
333
308
|
if (contactType) where.push(['contact_type', '=', contactType]);
|
|
334
309
|
}
|
|
335
310
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
361
|
-
};
|
|
362
|
-
}
|
|
311
|
+
const params =
|
|
312
|
+
resource === 'contact'
|
|
313
|
+
? {
|
|
314
|
+
where,
|
|
315
|
+
select: [
|
|
316
|
+
'id',
|
|
317
|
+
'display_name',
|
|
318
|
+
'first_name',
|
|
319
|
+
'last_name',
|
|
320
|
+
'contact_type',
|
|
321
|
+
'gender_id',
|
|
322
|
+
'gender_id:name',
|
|
323
|
+
'birth_date',
|
|
324
|
+
],
|
|
325
|
+
chain: {
|
|
326
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
327
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
328
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
: {
|
|
332
|
+
where,
|
|
333
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
334
|
+
};
|
|
363
335
|
|
|
364
336
|
if (returnAll) {
|
|
365
337
|
let offset = 0;
|
|
366
338
|
const page = 500;
|
|
367
339
|
|
|
368
340
|
while (true) {
|
|
369
|
-
const
|
|
341
|
+
const r = await civicrmApiRequest.call(
|
|
370
342
|
this,
|
|
371
343
|
'POST',
|
|
372
344
|
`/civicrm/ajax/api4/${entity}/get`,
|
|
373
|
-
{ ...
|
|
345
|
+
{ ...params, limit: page, offset },
|
|
374
346
|
);
|
|
375
347
|
|
|
376
|
-
const vals =
|
|
348
|
+
const vals = r?.values ?? [];
|
|
377
349
|
for (const v of vals) out.push({ json: v });
|
|
378
350
|
|
|
379
351
|
if (vals.length < page) break;
|
|
380
352
|
offset += page;
|
|
381
353
|
}
|
|
382
354
|
} else {
|
|
383
|
-
const
|
|
355
|
+
const r = await civicrmApiRequest.call(
|
|
384
356
|
this,
|
|
385
357
|
'POST',
|
|
386
358
|
`/civicrm/ajax/api4/${entity}/get`,
|
|
387
|
-
{ ...
|
|
359
|
+
{ ...params, limit },
|
|
388
360
|
);
|
|
389
|
-
|
|
361
|
+
|
|
362
|
+
const vals = r?.values ?? [];
|
|
390
363
|
for (const v of vals) out.push({ json: v });
|
|
391
364
|
}
|
|
365
|
+
|
|
366
|
+
continue;
|
|
392
367
|
}
|
|
393
368
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
369
|
+
/* ======================================================
|
|
370
|
+
DELETE
|
|
371
|
+
====================================================== */
|
|
372
|
+
if (operation === 'delete') {
|
|
373
|
+
const id = this.getNodeParameter('id', i) as number;
|
|
398
374
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
375
|
+
const r = await civicrmApiRequest.call(
|
|
376
|
+
this,
|
|
377
|
+
'POST',
|
|
378
|
+
`/civicrm/ajax/api4/${entity}/delete`,
|
|
379
|
+
{
|
|
380
|
+
where: [['id', '=', id]],
|
|
381
|
+
},
|
|
382
|
+
);
|
|
403
383
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
384
|
+
out.push({
|
|
385
|
+
json: {
|
|
386
|
+
success: true,
|
|
387
|
+
message: `${entity} ${id} deleted`,
|
|
388
|
+
deleted_id: id,
|
|
389
|
+
api_response: r,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
408
392
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (!input) return input;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
412
395
|
|
|
413
|
-
|
|
396
|
+
/* ======================================================
|
|
397
|
+
CREATE / UPDATE
|
|
398
|
+
====================================================== */
|
|
414
399
|
|
|
415
|
-
|
|
416
|
-
|
|
400
|
+
const isCreate = operation === 'create';
|
|
401
|
+
const id = !isCreate ? (this.getNodeParameter('id', i) as number) : undefined;
|
|
402
|
+
|
|
403
|
+
const pairs = this.getNodeParameter('fields.field', i, []) as Array<{
|
|
404
|
+
fieldName: string;
|
|
405
|
+
fieldValue: string;
|
|
406
|
+
}>;
|
|
407
|
+
|
|
408
|
+
const values: Record<string, unknown> = {};
|
|
409
|
+
const emailData: Record<string, unknown> = {};
|
|
410
|
+
const phoneData: Record<string, unknown> = {};
|
|
411
|
+
const addressData: Record<string, unknown> = {};
|
|
412
|
+
|
|
413
|
+
function normalizeBirthDate(input: string): string {
|
|
414
|
+
if (!input) return input;
|
|
415
|
+
let v = input.trim();
|
|
416
|
+
|
|
417
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
418
|
+
} else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
|
|
419
|
+
const [d, m, y] = v.split('/');
|
|
420
|
+
v = `${y}-${m}-${d}`;
|
|
421
|
+
} else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
|
|
422
|
+
const [d, m, y] = v.split('-');
|
|
423
|
+
v = `${y}-${m}-${d}`;
|
|
424
|
+
} else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
|
|
425
|
+
v = v.replace(/\//g, '-');
|
|
426
|
+
} else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
|
|
427
|
+
v = v.replace(/\./g, '-');
|
|
428
|
+
}
|
|
417
429
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
// DD-MM-YYYY
|
|
424
|
-
else if (/^\d{2}-\d{2}-\d{4}$/.test(val)) {
|
|
425
|
-
const [d, m, y] = val.split('-');
|
|
426
|
-
val = `${y}-${m}-${d}`;
|
|
427
|
-
}
|
|
428
|
-
// YYYY/MM/DD
|
|
429
|
-
else if (/^\d{4}\/\d{2}\/\d{2}$/.test(val)) {
|
|
430
|
-
val = val.replace(/\//g, '-');
|
|
431
|
-
}
|
|
432
|
-
// YYYY.MM.DD
|
|
433
|
-
else if (/^\d{4}\.\d{2}\.\d{2}$/.test(val)) {
|
|
434
|
-
val = val.replace(/\./g, '-');
|
|
435
|
-
}
|
|
430
|
+
const dt = new Date(v);
|
|
431
|
+
if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
|
|
432
|
+
return v;
|
|
433
|
+
}
|
|
436
434
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
435
|
+
let locationTypeMap: Record<string, string> = {};
|
|
436
|
+
if (resource === 'contact') {
|
|
437
|
+
locationTypeMap = await getLocationTypeMap.call(this);
|
|
438
|
+
}
|
|
441
439
|
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
for (const p of pairs) {
|
|
441
|
+
if (!p.fieldName) continue;
|
|
444
442
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
443
|
+
const key = p.fieldName.trim();
|
|
444
|
+
const rawVal = convertValue(p.fieldValue);
|
|
445
|
+
const val = rawVal;
|
|
446
|
+
|
|
447
|
+
/* Email simple */
|
|
448
|
+
if (key === 'email') {
|
|
449
|
+
emailData.email = val;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (key.startsWith('email.')) {
|
|
453
|
+
emailData[key.replace(/^email\./, '')] = val;
|
|
454
|
+
continue;
|
|
449
455
|
}
|
|
450
456
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const key = p.fieldName.trim();
|
|
462
|
-
const rawVal = convertValue(p.fieldValue);
|
|
463
|
-
const val = rawVal;
|
|
464
|
-
|
|
465
|
-
// 1) Casos simples sin prefijo: email / email.xxx / phone / phone.xxx / address.xxx
|
|
466
|
-
|
|
467
|
-
// Email simple
|
|
468
|
-
if (key === 'email') {
|
|
469
|
-
emailData.email = val;
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
if (key.startsWith('email.')) {
|
|
473
|
-
emailData[key.replace(/^email\./, '')] = val;
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
457
|
+
/* Phone simple */
|
|
458
|
+
if (key === 'phone') {
|
|
459
|
+
phoneData.phone = val;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (key.startsWith('phone.')) {
|
|
463
|
+
phoneData[key.replace(/^phone\./, '')] = val;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
476
466
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (key.startsWith('phone.')) {
|
|
483
|
-
phoneData[key.replace(/^phone\./, '')] = val;
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
467
|
+
/* Address */
|
|
468
|
+
if (key.startsWith('address.')) {
|
|
469
|
+
addressData[key.replace(/^address\./, '')] = val;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
486
472
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
473
|
+
/* Prefijo dinámico */
|
|
474
|
+
const segments = key.split('.');
|
|
475
|
+
if (segments.length >= 2 && resource === 'contact') {
|
|
476
|
+
const prefixRaw = segments[0];
|
|
477
|
+
const root = segments[1];
|
|
478
|
+
const subfield = segments.slice(2).join('.') || '';
|
|
479
|
+
|
|
480
|
+
const normalizedPrefix = normalizeLocationKey(prefixRaw);
|
|
481
|
+
const mappedLocationName = locationTypeMap[normalizedPrefix];
|
|
482
|
+
|
|
483
|
+
if (root === 'email' || root === 'phone' || root === 'address') {
|
|
484
|
+
if (mappedLocationName) {
|
|
485
|
+
if (root === 'email') emailLocationName = mappedLocationName;
|
|
486
|
+
if (root === 'phone') phoneLocationName = mappedLocationName;
|
|
487
|
+
if (root === 'address') addressLocationName = mappedLocationName;
|
|
488
|
+
}
|
|
492
489
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const root = segments[1]; // email / phone / address
|
|
498
|
-
const subfield = segments.slice(2).join('.') || '';
|
|
499
|
-
|
|
500
|
-
const normalizedPrefix = normalizeLocationKey(prefixRaw);
|
|
501
|
-
const mappedLocationName = locationTypeMap[normalizedPrefix];
|
|
502
|
-
|
|
503
|
-
if (root === 'email' || root === 'phone' || root === 'address') {
|
|
504
|
-
// Si el prefijo coincide con un location type, sobreescribir el locationName correspondiente
|
|
505
|
-
if (mappedLocationName) {
|
|
506
|
-
if (root === 'email') emailLocationName = mappedLocationName;
|
|
507
|
-
if (root === 'phone') phoneLocationName = mappedLocationName;
|
|
508
|
-
if (root === 'address') addressLocationName = mappedLocationName;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Asignar campo
|
|
512
|
-
if (root === 'email') {
|
|
513
|
-
if (!subfield) {
|
|
514
|
-
emailData.email = val;
|
|
515
|
-
} else {
|
|
516
|
-
emailData[subfield] = val;
|
|
517
|
-
}
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (root === 'phone') {
|
|
522
|
-
if (!subfield) {
|
|
523
|
-
phoneData.phone = val;
|
|
524
|
-
} else {
|
|
525
|
-
phoneData[subfield] = val;
|
|
526
|
-
}
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (root === 'address') {
|
|
531
|
-
if (!subfield) {
|
|
532
|
-
// No existe campo address "plano", así que ignoramos si no hay subfield
|
|
533
|
-
} else {
|
|
534
|
-
addressData[subfield] = val;
|
|
535
|
-
}
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
490
|
+
if (root === 'email') {
|
|
491
|
+
if (!subfield) emailData.email = val;
|
|
492
|
+
else emailData[subfield] = val;
|
|
493
|
+
continue;
|
|
538
494
|
}
|
|
539
|
-
}
|
|
540
495
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
496
|
+
if (root === 'phone') {
|
|
497
|
+
if (!subfield) phoneData.phone = val;
|
|
498
|
+
else phoneData[subfield] = val;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
546
501
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
502
|
+
if (root === 'address') {
|
|
503
|
+
if (subfield) addressData[subfield] = val;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
551
506
|
}
|
|
552
|
-
|
|
553
|
-
// 5) default → campo de Contact o de la entidad
|
|
554
|
-
values[key] = val;
|
|
555
507
|
}
|
|
556
508
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
509
|
+
/* gender */
|
|
510
|
+
if (key === 'gender' || key === 'gender_id') {
|
|
511
|
+
values.gender_id = val;
|
|
512
|
+
continue;
|
|
561
513
|
}
|
|
562
514
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this,
|
|
568
|
-
'POST',
|
|
569
|
-
'/civicrm/ajax/api4/Contact/create',
|
|
570
|
-
{ values },
|
|
571
|
-
);
|
|
572
|
-
|
|
573
|
-
contactId = contactRes?.values?.[0]?.id;
|
|
574
|
-
if (!contactId) throw new Error('Failed to create contact.');
|
|
575
|
-
} else {
|
|
576
|
-
await civicrmApiRequest.call(
|
|
577
|
-
this,
|
|
578
|
-
'POST',
|
|
579
|
-
`/civicrm/ajax/api4/${entity}/update`,
|
|
580
|
-
{
|
|
581
|
-
values,
|
|
582
|
-
where: [['id', '=', contactId]],
|
|
583
|
-
},
|
|
584
|
-
);
|
|
515
|
+
/* birth_date */
|
|
516
|
+
if (key === 'birth_date' || key === 'birth') {
|
|
517
|
+
values.birth_date = normalizeBirthDate(String(val));
|
|
518
|
+
continue;
|
|
585
519
|
}
|
|
586
520
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (isPrimary) {
|
|
591
|
-
await civicrmApiRequest.call(
|
|
592
|
-
this,
|
|
593
|
-
'POST',
|
|
594
|
-
'/civicrm/ajax/api4/Email/delete',
|
|
595
|
-
{
|
|
596
|
-
where: [
|
|
597
|
-
['contact_id', '=', contactId],
|
|
598
|
-
['is_primary', '=', true],
|
|
599
|
-
],
|
|
600
|
-
},
|
|
601
|
-
);
|
|
602
|
-
await civicrmApiRequest.call(
|
|
603
|
-
this,
|
|
604
|
-
'POST',
|
|
605
|
-
'/civicrm/ajax/api4/Phone/delete',
|
|
606
|
-
{
|
|
607
|
-
where: [
|
|
608
|
-
['contact_id', '=', contactId],
|
|
609
|
-
['is_primary', '=', true],
|
|
610
|
-
],
|
|
611
|
-
},
|
|
612
|
-
);
|
|
613
|
-
await civicrmApiRequest.call(
|
|
614
|
-
this,
|
|
615
|
-
'POST',
|
|
616
|
-
'/civicrm/ajax/api4/Address/delete',
|
|
617
|
-
{
|
|
618
|
-
where: [
|
|
619
|
-
['contact_id', '=', contactId],
|
|
620
|
-
['is_primary', '=', true],
|
|
621
|
-
],
|
|
622
|
-
},
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Email
|
|
627
|
-
if (Object.keys(emailData).length) {
|
|
628
|
-
await civicrmApiRequest.call(
|
|
629
|
-
this,
|
|
630
|
-
'POST',
|
|
631
|
-
'/civicrm/ajax/api4/Email/create',
|
|
632
|
-
{
|
|
633
|
-
values: {
|
|
634
|
-
...emailData,
|
|
635
|
-
contact_id: contactId,
|
|
636
|
-
is_primary: isPrimary,
|
|
637
|
-
'location_type_id:name': emailLocationName,
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Phone
|
|
644
|
-
if (Object.keys(phoneData).length) {
|
|
645
|
-
await civicrmApiRequest.call(
|
|
646
|
-
this,
|
|
647
|
-
'POST',
|
|
648
|
-
'/civicrm/ajax/api4/Phone/create',
|
|
649
|
-
{
|
|
650
|
-
values: {
|
|
651
|
-
...phoneData,
|
|
652
|
-
contact_id: contactId,
|
|
653
|
-
is_primary: isPrimary,
|
|
654
|
-
'location_type_id:name': phoneLocationName,
|
|
655
|
-
},
|
|
656
|
-
},
|
|
657
|
-
);
|
|
658
|
-
}
|
|
521
|
+
/* default */
|
|
522
|
+
values[key] = val;
|
|
523
|
+
}
|
|
659
524
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
'/civicrm/ajax/api4/Address/create',
|
|
666
|
-
{
|
|
667
|
-
values: {
|
|
668
|
-
...addressData,
|
|
669
|
-
contact_id: contactId,
|
|
670
|
-
is_primary: isPrimary,
|
|
671
|
-
'location_type_id:name': addressLocationName,
|
|
672
|
-
},
|
|
673
|
-
},
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
525
|
+
/* Contact type */
|
|
526
|
+
const contactType = this.getNodeParameter('contactType', i, '') as string;
|
|
527
|
+
if (resource === 'contact' && contactType) {
|
|
528
|
+
values.contact_type = contactType;
|
|
529
|
+
}
|
|
677
530
|
|
|
678
|
-
|
|
679
|
-
|
|
531
|
+
/* CREATE/UPDATE contact */
|
|
532
|
+
let contactId = id;
|
|
533
|
+
if (isCreate) {
|
|
534
|
+
const r = await civicrmApiRequest.call(
|
|
680
535
|
this,
|
|
681
536
|
'POST',
|
|
682
|
-
|
|
537
|
+
'/civicrm/ajax/api4/Contact/create',
|
|
538
|
+
{ values },
|
|
539
|
+
);
|
|
540
|
+
contactId = r?.values?.[0]?.id;
|
|
541
|
+
if (!contactId) throw new Error('Failed to create contact.');
|
|
542
|
+
} else {
|
|
543
|
+
await civicrmApiRequest.call(
|
|
544
|
+
this,
|
|
545
|
+
'POST',
|
|
546
|
+
`/civicrm/ajax/api4/${entity}/update`,
|
|
683
547
|
{
|
|
548
|
+
values,
|
|
684
549
|
where: [['id', '=', contactId]],
|
|
685
|
-
select: [
|
|
686
|
-
'id',
|
|
687
|
-
'display_name',
|
|
688
|
-
'first_name',
|
|
689
|
-
'last_name',
|
|
690
|
-
'contact_type',
|
|
691
|
-
'gender_id',
|
|
692
|
-
'gender_id:name',
|
|
693
|
-
'birth_date',
|
|
694
|
-
],
|
|
695
|
-
...(resource === 'contact'
|
|
696
|
-
? {
|
|
697
|
-
chain: {
|
|
698
|
-
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
699
|
-
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
700
|
-
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
701
|
-
},
|
|
702
|
-
}
|
|
703
|
-
: {}),
|
|
704
550
|
},
|
|
705
551
|
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* SUBENTITIES */
|
|
555
|
+
if (resource === 'contact') {
|
|
556
|
+
if (isPrimary) {
|
|
557
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/delete', {
|
|
558
|
+
where: [
|
|
559
|
+
['contact_id', '=', contactId],
|
|
560
|
+
['is_primary', '=', true],
|
|
561
|
+
],
|
|
562
|
+
});
|
|
563
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/delete', {
|
|
564
|
+
where: [
|
|
565
|
+
['contact_id', '=', contactId],
|
|
566
|
+
['is_primary', '=', true],
|
|
567
|
+
],
|
|
568
|
+
});
|
|
569
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/delete', {
|
|
570
|
+
where: [
|
|
571
|
+
['contact_id', '=', contactId],
|
|
572
|
+
['is_primary', '=', true],
|
|
573
|
+
],
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (Object.keys(emailData).length) {
|
|
578
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Email/create', {
|
|
579
|
+
values: {
|
|
580
|
+
...emailData,
|
|
581
|
+
contact_id: contactId,
|
|
582
|
+
is_primary: isPrimary,
|
|
583
|
+
'location_type_id:name': emailLocationName,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
}
|
|
706
587
|
|
|
707
|
-
|
|
588
|
+
if (Object.keys(phoneData).length) {
|
|
589
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Phone/create', {
|
|
590
|
+
values: {
|
|
591
|
+
...phoneData,
|
|
592
|
+
contact_id: contactId,
|
|
593
|
+
is_primary: isPrimary,
|
|
594
|
+
'location_type_id:name': phoneLocationName,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (Object.keys(addressData).length) {
|
|
600
|
+
await civicrmApiRequest.call(this, 'POST', '/civicrm/ajax/api4/Address/create', {
|
|
601
|
+
values: {
|
|
602
|
+
...addressData,
|
|
603
|
+
contact_id: contactId,
|
|
604
|
+
is_primary: isPrimary,
|
|
605
|
+
'location_type_id:name': addressLocationName,
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
}
|
|
708
609
|
}
|
|
610
|
+
|
|
611
|
+
/* FINAL GET */
|
|
612
|
+
const res = await civicrmApiRequest.call(
|
|
613
|
+
this,
|
|
614
|
+
'POST',
|
|
615
|
+
`/civicrm/ajax/api4/${entity}/get`,
|
|
616
|
+
{
|
|
617
|
+
where: [['id', '=', contactId]],
|
|
618
|
+
select: [
|
|
619
|
+
'id',
|
|
620
|
+
'display_name',
|
|
621
|
+
'first_name',
|
|
622
|
+
'last_name',
|
|
623
|
+
'contact_type',
|
|
624
|
+
'gender_id',
|
|
625
|
+
'gender_id:name',
|
|
626
|
+
'birth_date',
|
|
627
|
+
],
|
|
628
|
+
...(resource === 'contact'
|
|
629
|
+
? {
|
|
630
|
+
chain: {
|
|
631
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
632
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
633
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
634
|
+
},
|
|
635
|
+
}
|
|
636
|
+
: {}),
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
out.push({ json: res?.values?.[0] ?? {} });
|
|
709
641
|
}
|
|
710
642
|
|
|
711
643
|
return [out];
|
|
712
644
|
}
|
|
713
645
|
}
|
|
714
646
|
|
|
647
|
+
/* ============================================================================
|
|
648
|
+
UTILS
|
|
649
|
+
============================================================================ */
|
|
650
|
+
|
|
715
651
|
function convertValue(val: string): unknown {
|
|
716
652
|
const t = String(val ?? '').trim();
|
|
717
653
|
if (t === '') return '';
|