@ixiam/n8n-nodes-civicrm 1.1.18 → 1.1.23
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/LICENSE +21 -0
- package/README.md +65 -48
- package/dist/credentials/CiviCrmApi.credentials.js +1 -7
- package/dist/nodes/CiviCrm/CiviCrm.node.js +38 -31
- package/package.json +2 -2
- package/reload-in-docker.md +68 -0
- package/src/credentials/CiviCrmApi.credentials.ts +3 -9
- package/src/nodes/CiviCrm/CiviCrm.node.ts +82 -75
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 iXiam Global Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -7,20 +7,25 @@
|
|
|
7
7
|
Community Node for **CiviCRM API v4** (Civi-Go compatible)
|
|
8
8
|
Developed and maintained by **Ixiam Global Solutions**.
|
|
9
9
|
|
|
10
|
-
This
|
|
11
|
-
Custom fields are **not** supported.
|
|
10
|
+
This node enables full integration between **n8n** and **CiviCRM API v4**, supporting create/update/delete operations, smart field mapping, dynamic location types, and advanced filtering on GET operations.
|
|
12
11
|
|
|
13
12
|
---
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
## About CiviCRM
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="https://civicrm.org/sites/civicrm.org/files/CiviCRM-logo-2019-F2-200px.png" alt="CiviCRM Logo" width="200"/>
|
|
18
|
+
</p>
|
|
15
19
|
|
|
16
20
|
CiviCRM is an open-source Constituent Relationship Management platform designed for nonprofits, NGOs, and advocacy organizations. It supports contact management, memberships, contributions, event registration, email marketing, case management, and reporting. CiviCRM integrates with WordPress, Drupal, and Joomla.
|
|
17
21
|
|
|
18
22
|
Download: https://civicrm.org/download
|
|
19
23
|
|
|
24
|
+
---
|
|
20
25
|
|
|
21
|
-
## Installation
|
|
26
|
+
## 🚀 Installation
|
|
22
27
|
|
|
23
|
-
1. In your n8n instance,
|
|
28
|
+
1. In your n8n instance, go to:
|
|
24
29
|
**Settings → Community Nodes → Install**
|
|
25
30
|
2. Enter the package name:
|
|
26
31
|
|
|
@@ -28,61 +33,82 @@ Download: https://civicrm.org/download
|
|
|
28
33
|
@ixiam/n8n-nodes-civicrm
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
3.
|
|
36
|
+
3. Approve installation and enable Community Nodes.
|
|
32
37
|
4. If running n8n via Docker, restart/rebuild for the node to load.
|
|
33
38
|
|
|
34
39
|
---
|
|
35
40
|
|
|
36
|
-
## Credentials
|
|
41
|
+
## 🔐 Credentials
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
The node uses **Bearer Token Authentication**.
|
|
39
44
|
|
|
40
45
|
| Field | Description |
|
|
41
46
|
|-------|-------------|
|
|
42
|
-
| **Base URL** |
|
|
47
|
+
| **Base URL** | The root URL of your CiviCRM instance (without trailing slash). Example: `https://crm.example.org` |
|
|
43
48
|
| **API Token** | Sent as header `X-Civi-Auth: Bearer <token>` |
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
After entering credentials, click **Save** to validate the connection.
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
---
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
## 📦 Supported Entities
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
- Membership
|
|
53
|
-
- Group
|
|
54
|
-
- Relationship
|
|
55
|
-
- Activity
|
|
56
|
-
- Custom API Call (raw API4 request)
|
|
56
|
+
The node includes full API v4 support for the following entities:
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
| Entity | Operations |
|
|
59
|
+
|--------|------------|
|
|
60
|
+
| **Contact** | get, getMany, create, update, delete |
|
|
61
|
+
| **Membership** | get, getMany, create, update, delete |
|
|
62
|
+
| **Group** | get, getMany, create, update, delete |
|
|
63
|
+
| **Relationship** | get, getMany, create, update, delete |
|
|
64
|
+
| **Activity** | get, getMany, create, update, delete |
|
|
65
|
+
| **Custom API Call** | full custom API4 request |
|
|
64
66
|
|
|
65
67
|
---
|
|
66
68
|
|
|
67
|
-
## Features
|
|
69
|
+
## 🧩 Key Features
|
|
70
|
+
|
|
71
|
+
### **1. Dynamic Field Mapping**
|
|
72
|
+
Supports any standard or custom field:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
first_name = John
|
|
76
|
+
last_name = Doe
|
|
77
|
+
custom_45 = Blue
|
|
78
|
+
```
|
|
68
79
|
|
|
69
|
-
### Email,
|
|
70
|
-
Two
|
|
80
|
+
### **2. Smart Email, Phone & Address Mapping**
|
|
81
|
+
Two ways to set location-aware fields:
|
|
71
82
|
|
|
72
|
-
Simple
|
|
83
|
+
**(A) Simple fields**
|
|
73
84
|
```
|
|
74
85
|
email = test@example.org
|
|
86
|
+
phone.mobile = 600123456
|
|
75
87
|
address.city = Barcelona
|
|
76
88
|
```
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
**(B) Dynamic prefixes matched to CiviCRM Location Types**
|
|
79
91
|
```
|
|
80
92
|
work.email = user@company.org
|
|
81
|
-
billing.address.postal_code =
|
|
93
|
+
billing.address.postal_code = 80331
|
|
82
94
|
home.phone.phone_type_id = 2
|
|
83
95
|
```
|
|
84
96
|
|
|
85
|
-
###
|
|
97
|
+
### **3. Default Location Type selectors**
|
|
98
|
+
If no prefix is used, default types are applied.
|
|
99
|
+
|
|
100
|
+
### **4. Birth Date Normalization**
|
|
101
|
+
Accepted input formats:
|
|
102
|
+
|
|
103
|
+
- YYYY-MM-DD
|
|
104
|
+
- DD/MM/YYYY
|
|
105
|
+
- DD-MM-YYYY
|
|
106
|
+
- YYYY/MM/DD
|
|
107
|
+
- YYYY.MM.DD
|
|
108
|
+
|
|
109
|
+
Auto-normalized to `YYYY-MM-DD`.
|
|
110
|
+
|
|
111
|
+
### **5. GET MANY with JSON Filters**
|
|
86
112
|
Example:
|
|
87
113
|
```json
|
|
88
114
|
[
|
|
@@ -92,17 +118,7 @@ Example:
|
|
|
92
118
|
]
|
|
93
119
|
```
|
|
94
120
|
|
|
95
|
-
###
|
|
96
|
-
Accepted input formats:
|
|
97
|
-
- YYYY-MM-DD
|
|
98
|
-
- DD/MM/YYYY
|
|
99
|
-
- DD-MM-YYYY
|
|
100
|
-
- YYYY/MM/DD
|
|
101
|
-
- YYYY.MM.DD
|
|
102
|
-
|
|
103
|
-
Normalized to `YYYY-MM-DD`.
|
|
104
|
-
|
|
105
|
-
### Custom API Call
|
|
121
|
+
### **6. Custom API Call Mode**
|
|
106
122
|
Example:
|
|
107
123
|
```json
|
|
108
124
|
{
|
|
@@ -126,14 +142,14 @@ Example:
|
|
|
126
142
|
|
|
127
143
|
Clone the repository and run:
|
|
128
144
|
|
|
129
|
-
```
|
|
145
|
+
```bash
|
|
130
146
|
npm install
|
|
131
147
|
npm run dev
|
|
132
148
|
```
|
|
133
149
|
|
|
134
150
|
Build:
|
|
135
151
|
|
|
136
|
-
```
|
|
152
|
+
```bash
|
|
137
153
|
npm run build
|
|
138
154
|
```
|
|
139
155
|
|
|
@@ -145,12 +161,13 @@ Pull requests and issues are welcome in the GitHub repository.
|
|
|
145
161
|
|
|
146
162
|
---
|
|
147
163
|
|
|
148
|
-
## About Ixiam Global Solutions
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
## 🧑💻 About Ixiam Global Solutions
|
|
165
|
+
|
|
166
|
+
Website: **https://www.ixiam.com**
|
|
167
|
+
Contact: **info@ixiam.com**
|
|
151
168
|
|
|
152
169
|
---
|
|
153
170
|
|
|
154
|
-
## License
|
|
171
|
+
## 📄 License
|
|
155
172
|
|
|
156
173
|
MIT License
|
|
@@ -23,7 +23,6 @@ class CiviCrmApi {
|
|
|
23
23
|
required: true,
|
|
24
24
|
},
|
|
25
25
|
];
|
|
26
|
-
// ← ESTA ES LA PARTE CRÍTICA
|
|
27
26
|
this.authenticate = {
|
|
28
27
|
type: 'generic',
|
|
29
28
|
properties: {
|
|
@@ -34,13 +33,8 @@ class CiviCrmApi {
|
|
|
34
33
|
};
|
|
35
34
|
this.test = {
|
|
36
35
|
request: {
|
|
37
|
-
|
|
38
|
-
url: '/civicrm/ajax/api4/Contact/get',
|
|
36
|
+
url: '={{ $credentials.baseUrl }}/civicrm/ajax/api4/Contact/get',
|
|
39
37
|
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'X-Civi-Auth': '={{ "Bearer " + $credentials.apiToken }}',
|
|
42
|
-
'Content-Type': 'application/json',
|
|
43
|
-
},
|
|
44
38
|
body: {
|
|
45
39
|
limit: 1,
|
|
46
40
|
select: ['id']
|
|
@@ -242,7 +242,7 @@ class CiviCrm {
|
|
|
242
242
|
try {
|
|
243
243
|
where = JSON.parse(whereJson);
|
|
244
244
|
}
|
|
245
|
-
catch {
|
|
245
|
+
catch (error) {
|
|
246
246
|
throw new Error('Invalid JSON in whereJson');
|
|
247
247
|
}
|
|
248
248
|
}
|
|
@@ -277,14 +277,18 @@ class CiviCrm {
|
|
|
277
277
|
if (returnAll) {
|
|
278
278
|
let offset = 0;
|
|
279
279
|
const page = 500;
|
|
280
|
-
|
|
280
|
+
let hasMore = true;
|
|
281
|
+
while (hasMore) {
|
|
281
282
|
const r = await GenericFunctions_1.civicrmApiRequest.call(this, 'POST', `/civicrm/ajax/api4/${entity}/get`, { ...params, limit: page, offset });
|
|
282
283
|
const vals = (_c = r === null || r === void 0 ? void 0 : r.values) !== null && _c !== void 0 ? _c : [];
|
|
283
284
|
for (const v of vals)
|
|
284
285
|
out.push({ json: v });
|
|
285
|
-
if (vals.length < page)
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
if (vals.length < page) {
|
|
287
|
+
hasMore = false;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
offset += page;
|
|
291
|
+
}
|
|
288
292
|
}
|
|
289
293
|
}
|
|
290
294
|
else {
|
|
@@ -323,31 +327,6 @@ class CiviCrm {
|
|
|
323
327
|
const emailData = {};
|
|
324
328
|
const phoneData = {};
|
|
325
329
|
const addressData = {};
|
|
326
|
-
function normalizeBirthDate(input) {
|
|
327
|
-
if (!input)
|
|
328
|
-
return input;
|
|
329
|
-
let v = input.trim();
|
|
330
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
331
|
-
}
|
|
332
|
-
else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
|
|
333
|
-
const [d, m, y] = v.split('/');
|
|
334
|
-
v = `${y}-${m}-${d}`;
|
|
335
|
-
}
|
|
336
|
-
else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
|
|
337
|
-
const [d, m, y] = v.split('-');
|
|
338
|
-
v = `${y}-${m}-${d}`;
|
|
339
|
-
}
|
|
340
|
-
else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
|
|
341
|
-
v = v.replace(/\//g, '-');
|
|
342
|
-
}
|
|
343
|
-
else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
|
|
344
|
-
v = v.replace(/\./g, '-');
|
|
345
|
-
}
|
|
346
|
-
const dt = new Date(v);
|
|
347
|
-
if (isNaN(dt.getTime()))
|
|
348
|
-
throw new Error(`Invalid birth_date: ${input}`);
|
|
349
|
-
return v;
|
|
350
|
-
}
|
|
351
330
|
let locationTypeMap = {};
|
|
352
331
|
if (resource === 'contact') {
|
|
353
332
|
locationTypeMap = await getLocationTypeMap.call(this);
|
|
@@ -551,7 +530,35 @@ function convertValue(val) {
|
|
|
551
530
|
if (typeof j === 'object')
|
|
552
531
|
return j;
|
|
553
532
|
}
|
|
554
|
-
catch {
|
|
533
|
+
catch (error) {
|
|
534
|
+
// Not valid JSON, return original value
|
|
535
|
+
}
|
|
555
536
|
return val;
|
|
556
537
|
}
|
|
538
|
+
function normalizeBirthDate(input) {
|
|
539
|
+
if (!input)
|
|
540
|
+
return input;
|
|
541
|
+
let v = input.trim();
|
|
542
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
543
|
+
// Already in correct format
|
|
544
|
+
}
|
|
545
|
+
else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
|
|
546
|
+
const [d, m, y] = v.split('/');
|
|
547
|
+
v = `${y}-${m}-${d}`;
|
|
548
|
+
}
|
|
549
|
+
else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
|
|
550
|
+
const [d, m, y] = v.split('-');
|
|
551
|
+
v = `${y}-${m}-${d}`;
|
|
552
|
+
}
|
|
553
|
+
else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
|
|
554
|
+
v = v.replace(/\//g, '-');
|
|
555
|
+
}
|
|
556
|
+
else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
|
|
557
|
+
v = v.replace(/\./g, '-');
|
|
558
|
+
}
|
|
559
|
+
const dt = new Date(v);
|
|
560
|
+
if (isNaN(dt.getTime()))
|
|
561
|
+
throw new Error(`Invalid birth_date: ${input}`);
|
|
562
|
+
return v;
|
|
563
|
+
}
|
|
557
564
|
exports.default = CiviCrm;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixiam/n8n-nodes-civicrm",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"description": "Full-featured CiviCRM API v4 integration for n8n with support for Contacts, Memberships, Groups, Relationships, Activities and structured sub-entities like Email, Phone, and Address.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -60,4 +60,4 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"packageManager": "pnpm@10.18.2+sha512.9fb969fa749b3ade6035e0f109f0b8a60b5d08a1a87fdf72e337da90dcc93336e2280ca4e44f2358a649b83c17959e9993e777c2080879f3801e6f0d999ad3dd"
|
|
63
|
-
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Reload Node in Docker n8n
|
|
2
|
+
|
|
3
|
+
If you're loading this node from a local folder in Docker and seeing cached/old versions, try these steps:
|
|
4
|
+
|
|
5
|
+
## Option 1: Restart Docker Container (Quick)
|
|
6
|
+
```bash
|
|
7
|
+
# Restart your n8n container
|
|
8
|
+
docker restart <container-name>
|
|
9
|
+
# or if using docker-compose
|
|
10
|
+
docker-compose restart
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Option 2: Clear n8n Cache (Better)
|
|
14
|
+
```bash
|
|
15
|
+
# Stop the container
|
|
16
|
+
docker stop <container-name>
|
|
17
|
+
|
|
18
|
+
# Remove the container (keeps volumes)
|
|
19
|
+
docker rm <container-name>
|
|
20
|
+
|
|
21
|
+
# Start fresh
|
|
22
|
+
docker-compose up -d
|
|
23
|
+
# or your docker run command
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Option 3: Verify Volume Mount
|
|
27
|
+
If you're using a volume mount like `-v /path/to/n8n-nodes-civicrm:/data/custom-nodes/n8n-nodes-civicrm`:
|
|
28
|
+
|
|
29
|
+
1. Make sure the path points to THIS directory
|
|
30
|
+
2. Verify the dist folder is being mounted:
|
|
31
|
+
```bash
|
|
32
|
+
docker exec <container-name> ls -la /data/custom-nodes/n8n-nodes-civicrm/dist/credentials/
|
|
33
|
+
docker exec <container-name> cat /data/custom-nodes/n8n-nodes-civicrm/dist/credentials/CiviCrmApi.credentials.js | grep baseURL
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Option 4: Check n8n Custom Nodes Path
|
|
37
|
+
If using `N8N_CUSTOM_EXTENSION_ENV` environment variable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Check what path n8n is using
|
|
41
|
+
docker exec <container-name> env | grep N8N_CUSTOM
|
|
42
|
+
|
|
43
|
+
# Verify the file exists in that path
|
|
44
|
+
docker exec <container-name> ls -la <custom-path>/dist/credentials/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Option 5: Force Rebuild and Reload
|
|
48
|
+
```bash
|
|
49
|
+
# In this directory, clean and rebuild
|
|
50
|
+
npm run build
|
|
51
|
+
|
|
52
|
+
# If using npm link
|
|
53
|
+
npm link
|
|
54
|
+
|
|
55
|
+
# Then restart n8n container
|
|
56
|
+
docker restart <container-name>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Verify the Fix
|
|
60
|
+
After restarting, check if the error is gone. The built file should have:
|
|
61
|
+
```javascript
|
|
62
|
+
baseURL: '={{ $credentials.baseUrl }}',
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
NOT:
|
|
66
|
+
```javascript
|
|
67
|
+
baseURL: '={{ $credentials.baseUrl.replace(/\\/$/, "") }}',
|
|
68
|
+
```
|
|
@@ -28,7 +28,6 @@ export class CiviCrmApi implements ICredentialType {
|
|
|
28
28
|
},
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
-
// ← ESTA ES LA PARTE CRÍTICA
|
|
32
31
|
authenticate = {
|
|
33
32
|
type: 'generic' as const,
|
|
34
33
|
properties: {
|
|
@@ -40,17 +39,12 @@ export class CiviCrmApi implements ICredentialType {
|
|
|
40
39
|
|
|
41
40
|
test: ICredentialTestRequest = {
|
|
42
41
|
request: {
|
|
43
|
-
|
|
44
|
-
url: '/civicrm/ajax/api4/Contact/get',
|
|
42
|
+
url: '={{ $credentials.baseUrl }}/civicrm/ajax/api4/Contact/get',
|
|
45
43
|
method: 'POST',
|
|
46
|
-
|
|
47
|
-
'X-Civi-Auth': '={{ "Bearer " + $credentials.apiToken }}',
|
|
48
|
-
'Content-Type': 'application/json',
|
|
49
|
-
},
|
|
50
|
-
body: {
|
|
44
|
+
body: {
|
|
51
45
|
limit: 1,
|
|
52
46
|
select: ['id']
|
|
53
47
|
},
|
|
54
48
|
},
|
|
55
49
|
};
|
|
56
|
-
}
|
|
50
|
+
}
|
|
@@ -251,29 +251,29 @@ export class CiviCrm implements INodeType {
|
|
|
251
251
|
const params =
|
|
252
252
|
resource === 'contact'
|
|
253
253
|
? {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
272
|
: {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
where: [['id', '=', id]],
|
|
274
|
+
limit: 1,
|
|
275
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
276
|
+
};
|
|
277
277
|
|
|
278
278
|
const res = await civicrmApiRequest.call(
|
|
279
279
|
this,
|
|
@@ -298,7 +298,7 @@ export class CiviCrm implements INodeType {
|
|
|
298
298
|
if (whereJson) {
|
|
299
299
|
try {
|
|
300
300
|
where = JSON.parse(whereJson);
|
|
301
|
-
} catch {
|
|
301
|
+
} catch (error) {
|
|
302
302
|
throw new Error('Invalid JSON in whereJson');
|
|
303
303
|
}
|
|
304
304
|
}
|
|
@@ -311,33 +311,34 @@ export class CiviCrm implements INodeType {
|
|
|
311
311
|
const params =
|
|
312
312
|
resource === 'contact'
|
|
313
313
|
? {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
331
|
: {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
332
|
+
where,
|
|
333
|
+
select: ['id', 'name', 'title', 'subject', 'display_name'],
|
|
334
|
+
};
|
|
335
335
|
|
|
336
336
|
if (returnAll) {
|
|
337
337
|
let offset = 0;
|
|
338
338
|
const page = 500;
|
|
339
|
+
let hasMore = true;
|
|
339
340
|
|
|
340
|
-
while (
|
|
341
|
+
while (hasMore) {
|
|
341
342
|
const r = await civicrmApiRequest.call(
|
|
342
343
|
this,
|
|
343
344
|
'POST',
|
|
@@ -348,8 +349,11 @@ export class CiviCrm implements INodeType {
|
|
|
348
349
|
const vals = r?.values ?? [];
|
|
349
350
|
for (const v of vals) out.push({ json: v });
|
|
350
351
|
|
|
351
|
-
if (vals.length < page)
|
|
352
|
-
|
|
352
|
+
if (vals.length < page) {
|
|
353
|
+
hasMore = false;
|
|
354
|
+
} else {
|
|
355
|
+
offset += page;
|
|
356
|
+
}
|
|
353
357
|
}
|
|
354
358
|
} else {
|
|
355
359
|
const r = await civicrmApiRequest.call(
|
|
@@ -410,28 +414,6 @@ export class CiviCrm implements INodeType {
|
|
|
410
414
|
const phoneData: Record<string, unknown> = {};
|
|
411
415
|
const addressData: Record<string, unknown> = {};
|
|
412
416
|
|
|
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
|
-
}
|
|
429
|
-
|
|
430
|
-
const dt = new Date(v);
|
|
431
|
-
if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
|
|
432
|
-
return v;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
417
|
let locationTypeMap: Record<string, string> = {};
|
|
436
418
|
if (resource === 'contact') {
|
|
437
419
|
locationTypeMap = await getLocationTypeMap.call(this);
|
|
@@ -627,12 +609,12 @@ export class CiviCrm implements INodeType {
|
|
|
627
609
|
],
|
|
628
610
|
...(resource === 'contact'
|
|
629
611
|
? {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
612
|
+
chain: {
|
|
613
|
+
emails: ['Email', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
614
|
+
phones: ['Phone', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
615
|
+
addresses: ['Address', 'get', { where: [['contact_id', '=', '$id']] }],
|
|
616
|
+
},
|
|
617
|
+
}
|
|
636
618
|
: {}),
|
|
637
619
|
},
|
|
638
620
|
);
|
|
@@ -657,8 +639,33 @@ function convertValue(val: string): unknown {
|
|
|
657
639
|
try {
|
|
658
640
|
const j = JSON.parse(t);
|
|
659
641
|
if (typeof j === 'object') return j;
|
|
660
|
-
} catch {
|
|
642
|
+
} catch (error) {
|
|
643
|
+
// Not valid JSON, return original value
|
|
644
|
+
}
|
|
661
645
|
return val;
|
|
662
646
|
}
|
|
663
647
|
|
|
648
|
+
function normalizeBirthDate(input: string): string {
|
|
649
|
+
if (!input) return input;
|
|
650
|
+
let v = input.trim();
|
|
651
|
+
|
|
652
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
|
653
|
+
// Already in correct format
|
|
654
|
+
} else if (/^\d{2}\/\d{2}\/\d{4}$/.test(v)) {
|
|
655
|
+
const [d, m, y] = v.split('/');
|
|
656
|
+
v = `${y}-${m}-${d}`;
|
|
657
|
+
} else if (/^\d{2}-\d{2}-\d{4}$/.test(v)) {
|
|
658
|
+
const [d, m, y] = v.split('-');
|
|
659
|
+
v = `${y}-${m}-${d}`;
|
|
660
|
+
} else if (/^\d{4}\/\d{2}\/\d{2}$/.test(v)) {
|
|
661
|
+
v = v.replace(/\//g, '-');
|
|
662
|
+
} else if (/^\d{4}\.\d{2}\.\d{2}$/.test(v)) {
|
|
663
|
+
v = v.replace(/\./g, '-');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const dt = new Date(v);
|
|
667
|
+
if (isNaN(dt.getTime())) throw new Error(`Invalid birth_date: ${input}`);
|
|
668
|
+
return v;
|
|
669
|
+
}
|
|
670
|
+
|
|
664
671
|
export default CiviCrm;
|