Skip to content

Commit 029885e

Browse files
authored
Merge pull request #5 from traien/dev/add-other-entities
Add new operations and fields for EspoCRM integration
2 parents 5bb7a4f + 1bb939c commit 029885e

17 files changed

Lines changed: 1448 additions & 11 deletions

.vscode/tasks.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build:tsc",
6+
"type": "shell",
7+
"command": "npm run build",
8+
"problemMatcher": [
9+
"$tsc"
10+
],
11+
"group": "build"
12+
},
13+
{
14+
"label": "build:tsc",
15+
"type": "shell",
16+
"command": "npm run build",
17+
"problemMatcher": [
18+
"$tsc"
19+
],
20+
"group": "build"
21+
},
22+
{
23+
"label": "build:tsc",
24+
"type": "shell",
25+
"command": "npm run build",
26+
"isBackground": false,
27+
"problemMatcher": [
28+
"$tsc"
29+
],
30+
"group": "build"
31+
},
32+
{
33+
"label": "build:tsc",
34+
"type": "shell",
35+
"command": "npm run build",
36+
"isBackground": false,
37+
"problemMatcher": [
38+
"$tsc"
39+
],
40+
"group": "build"
41+
}
42+
]
43+
}

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The EspoCRM node provides comprehensive access to EspoCRM's API functionality:
1515
- Update records (full or partial updates)
1616
- Delete records
1717
- List/search entities with advanced filtering
18+
- Supported core entities: Account, Contact, Lead, Meeting, Task, Call, Opportunity, Case
1819

1920
- **Dynamic Entity Support**
2021
- Work with any entity type in your EspoCRM instance
@@ -95,6 +96,17 @@ Create a new contact record with customized field values:
9596
5. Add any additional fields as needed
9697
6. Connect to other nodes in your workflow
9798

99+
### Creating a Meeting
100+
101+
1. Add an "EspoCRM" node to your workflow
102+
2. Select `Meeting` as the Resource
103+
3. Choose `Create` as the Operation
104+
4. Fill in the required fields:
105+
- `Name`
106+
- `Start Date`
107+
5. Optionally set `End Date`, `Status`, `Assigned User ID`, or parent (`parentType` + `parentId`)
108+
6. Execute to create the meeting in EspoCRM
109+
98110
### Working with Dynamic Entities
99111

100112
The Dynamic resource allows you to work with any entity type in your EspoCRM system:

nodes/EspoCRM/EspoCRM.node.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { accountOperations, accountFields } from './operations/account/account.o
1414
import { contactOperations, contactFields } from './operations/contact/contact.operations';
1515
import { leadOperations, leadFields } from './operations/lead/lead.operations';
1616
import { dynamicOperations, dynamicFields } from './operations/dynamic/dynamic.operations';
17+
import { meetingOperations, meetingFields } from './operations/meeting/meeting.operations';
18+
import { taskOperations, taskFields } from './operations/task/task.operations';
19+
import { callOperations, callFields } from './operations/call/call.operations';
20+
import { opportunityOperations, opportunityFields } from './operations/opportunity/opportunity.operations';
21+
import { caseOperations, caseFields } from './operations/case/case.operations';
1722

1823
// Import handler factory
1924
import { HandlerFactory } from './handlers/HandlerFactory';
@@ -73,6 +78,26 @@ export class EspoCRM implements INodeType {
7378
name: 'Lead',
7479
value: 'lead',
7580
},
81+
{
82+
name: 'Meeting',
83+
value: 'meeting',
84+
},
85+
{
86+
name: 'Task',
87+
value: 'task',
88+
},
89+
{
90+
name: 'Call',
91+
value: 'call',
92+
},
93+
{
94+
name: 'Opportunity',
95+
value: 'opportunity',
96+
},
97+
{
98+
name: 'Case',
99+
value: 'case',
100+
},
76101
],
77102
default: 'contact',
78103
},
@@ -101,6 +126,16 @@ export class EspoCRM implements INodeType {
101126
...contactFields,
102127
...leadOperations,
103128
...leadFields,
129+
...meetingOperations,
130+
...meetingFields,
131+
...taskOperations,
132+
...taskFields,
133+
...callOperations,
134+
...callFields,
135+
...opportunityOperations,
136+
...opportunityFields,
137+
...caseOperations,
138+
...caseFields,
104139
...dynamicOperations,
105140
...dynamicFields,
106141

nodes/EspoCRM/GenericFunctions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export async function espoApiRequest(
3838
},
3939
method,
4040
body: Object.keys(body).length === 0 ? undefined : body,
41-
url: endpoint,
41+
// Allow overriding full URL via `uri` if provided
42+
url: uri ?? endpoint,
4243
qs,
4344
};
4445

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { IExecuteFunctions, IDataObject, NodeOperationError } from 'n8n-workflow';
2+
import { EntityHandler } from './EntityHandler';
3+
import { espoApiRequest, espoApiRequestAllItems } from '../GenericFunctions';
4+
import { toEspoDate, toEspoDateTime } from './Utils';
5+
6+
/**
7+
* Class for handling Call entity operations
8+
*/
9+
export class CallHandler implements EntityHandler {
10+
async create(this: IExecuteFunctions, index: number): Promise<IDataObject> {
11+
const entityData: IDataObject = {};
12+
entityData.name = this.getNodeParameter('name', index) as string;
13+
entityData.dateStart = this.getNodeParameter('dateStart', index) as string;
14+
entityData.dateEnd = this.getNodeParameter('dateEnd', index) as string;
15+
16+
const additionalFields = this.getNodeParameter('additionalFields', index, {}) as IDataObject;
17+
Object.assign(entityData, additionalFields);
18+
19+
// Normalize date fields
20+
if (entityData.dateStart) {
21+
const ds = toEspoDateTime(entityData.dateStart as string);
22+
entityData.dateStart = ds;
23+
entityData.dateStartDate = toEspoDate(ds);
24+
}
25+
if (entityData.dateEnd) {
26+
const de = toEspoDateTime(entityData.dateEnd as string);
27+
entityData.dateEnd = de;
28+
entityData.dateEndDate = toEspoDate(de);
29+
}
30+
31+
const endpoint = '/call';
32+
const responseData = await espoApiRequest.call(this, 'POST', endpoint, entityData);
33+
return responseData as IDataObject;
34+
}
35+
36+
async get(this: IExecuteFunctions, index: number): Promise<IDataObject> {
37+
const id = this.getNodeParameter('callId', index) as string;
38+
const endpoint = `/call/${id}`;
39+
const responseData = await espoApiRequest.call(this, 'GET', endpoint);
40+
return responseData as IDataObject;
41+
}
42+
43+
async update(this: IExecuteFunctions, index: number): Promise<IDataObject> {
44+
const id = this.getNodeParameter('callId', index) as string;
45+
const updateFields = this.getNodeParameter('updateFields', index, {}) as IDataObject;
46+
if (updateFields.dateStart) {
47+
const ds = toEspoDateTime(updateFields.dateStart as string);
48+
updateFields.dateStart = ds;
49+
updateFields.dateStartDate = toEspoDate(ds);
50+
}
51+
if (updateFields.dateEnd) {
52+
const de = toEspoDateTime(updateFields.dateEnd as string);
53+
updateFields.dateEnd = de;
54+
updateFields.dateEndDate = toEspoDate(de);
55+
}
56+
const endpoint = `/call/${id}`;
57+
const responseData = await espoApiRequest.call(this, 'PATCH', endpoint, updateFields);
58+
return responseData as IDataObject;
59+
}
60+
61+
async delete(this: IExecuteFunctions, index: number): Promise<IDataObject> {
62+
const id = this.getNodeParameter('callId', index) as string;
63+
const endpoint = `/call/${id}`;
64+
await espoApiRequest.call(this, 'DELETE', endpoint);
65+
return { success: true, entityType: 'call', id, message: 'Call deleted successfully' };
66+
}
67+
68+
async getAll(this: IExecuteFunctions, index: number): Promise<IDataObject[]> {
69+
const returnAll = this.getNodeParameter('returnAll', index) as boolean;
70+
const endpoint = '/call';
71+
const qs: IDataObject = {};
72+
73+
const filterOptions = this.getNodeParameter('filterOptions', index, {}) as IDataObject;
74+
75+
if (filterOptions.where) {
76+
if (typeof filterOptions.where === 'string') {
77+
try {
78+
qs.where = JSON.parse(filterOptions.where);
79+
} catch (e: any) {
80+
throw new NodeOperationError(this.getNode(), `Invalid JSON in 'where' parameter: ${e.message}`);
81+
}
82+
} else {
83+
qs.where = filterOptions.where;
84+
}
85+
}
86+
87+
if (filterOptions.orderBy) qs.orderBy = filterOptions.orderBy;
88+
if (filterOptions.order) qs.order = filterOptions.order;
89+
if (filterOptions.select) qs.select = filterOptions.select;
90+
if (filterOptions.offset) qs.offset = filterOptions.offset;
91+
if (filterOptions.boolFilterList) qs.boolFilterList = filterOptions.boolFilterList;
92+
if (filterOptions.primaryFilter) qs.primaryFilter = filterOptions.primaryFilter;
93+
94+
const headers: IDataObject = {};
95+
if (filterOptions.skipTotalCount === true) headers['X-No-Total'] = 'true';
96+
97+
if (returnAll === true) {
98+
return await espoApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
99+
} else {
100+
const limit = this.getNodeParameter('limit', index) as number;
101+
qs.maxSize = limit;
102+
const response = await espoApiRequest.call(this, 'GET', endpoint, {}, qs, undefined, headers);
103+
return response.list as IDataObject[];
104+
}
105+
}
106+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { IExecuteFunctions, IDataObject, NodeOperationError } from 'n8n-workflow';
2+
import { EntityHandler } from './EntityHandler';
3+
import { espoApiRequest, espoApiRequestAllItems } from '../GenericFunctions';
4+
5+
/**
6+
* Class for handling Case entity operations
7+
*/
8+
export class CaseHandler implements EntityHandler {
9+
async create(this: IExecuteFunctions, index: number): Promise<IDataObject> {
10+
const entityData: IDataObject = {};
11+
entityData.name = this.getNodeParameter('name', index) as string;
12+
13+
const additionalFields = this.getNodeParameter('additionalFields', index, {}) as IDataObject;
14+
Object.assign(entityData, additionalFields);
15+
16+
const endpoint = '/case';
17+
const responseData = await espoApiRequest.call(this, 'POST', endpoint, entityData);
18+
return responseData as IDataObject;
19+
}
20+
21+
async get(this: IExecuteFunctions, index: number): Promise<IDataObject> {
22+
const id = this.getNodeParameter('caseId', index) as string;
23+
const endpoint = `/case/${id}`;
24+
const responseData = await espoApiRequest.call(this, 'GET', endpoint);
25+
return responseData as IDataObject;
26+
}
27+
28+
async update(this: IExecuteFunctions, index: number): Promise<IDataObject> {
29+
const id = this.getNodeParameter('caseId', index) as string;
30+
const updateFields = this.getNodeParameter('updateFields', index, {}) as IDataObject;
31+
const endpoint = `/case/${id}`;
32+
const responseData = await espoApiRequest.call(this, 'PATCH', endpoint, updateFields);
33+
return responseData as IDataObject;
34+
}
35+
36+
async delete(this: IExecuteFunctions, index: number): Promise<IDataObject> {
37+
const id = this.getNodeParameter('caseId', index) as string;
38+
const endpoint = `/case/${id}`;
39+
await espoApiRequest.call(this, 'DELETE', endpoint);
40+
return { success: true, entityType: 'case', id, message: 'Case deleted successfully' };
41+
}
42+
43+
async getAll(this: IExecuteFunctions, index: number): Promise<IDataObject[]> {
44+
const returnAll = this.getNodeParameter('returnAll', index) as boolean;
45+
const endpoint = '/case';
46+
const qs: IDataObject = {};
47+
48+
const filterOptions = this.getNodeParameter('filterOptions', index, {}) as IDataObject;
49+
50+
if (filterOptions.where) {
51+
if (typeof filterOptions.where === 'string') {
52+
try {
53+
qs.where = JSON.parse(filterOptions.where);
54+
} catch (e: any) {
55+
throw new NodeOperationError(this.getNode(), `Invalid JSON in 'where' parameter: ${e.message}`);
56+
}
57+
} else {
58+
qs.where = filterOptions.where;
59+
}
60+
}
61+
62+
if (filterOptions.orderBy) qs.orderBy = filterOptions.orderBy;
63+
if (filterOptions.order) qs.order = filterOptions.order;
64+
if (filterOptions.select) qs.select = filterOptions.select;
65+
if (filterOptions.offset) qs.offset = filterOptions.offset;
66+
if (filterOptions.boolFilterList) qs.boolFilterList = filterOptions.boolFilterList;
67+
if (filterOptions.primaryFilter) qs.primaryFilter = filterOptions.primaryFilter;
68+
69+
const headers: IDataObject = {};
70+
if (filterOptions.skipTotalCount === true) headers['X-No-Total'] = 'true';
71+
72+
if (returnAll === true) {
73+
return await espoApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
74+
} else {
75+
const limit = this.getNodeParameter('limit', index) as number;
76+
qs.maxSize = limit;
77+
const response = await espoApiRequest.call(this, 'GET', endpoint, {}, qs, undefined, headers);
78+
return response.list as IDataObject[];
79+
}
80+
}
81+
}

nodes/EspoCRM/handlers/HandlerFactory.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { AccountHandler } from './AccountHandler';
22
import { ContactHandler } from './ContactHandler';
33
import { LeadHandler } from './LeadHandler';
44
import { CustomEntityHandler } from './CustomEntityHandler';
5+
import { MeetingHandler } from './MeetingHandler';
6+
import { TaskHandler } from './TaskHandler';
7+
import { CallHandler } from './CallHandler';
8+
import { OpportunityHandler } from './OpportunityHandler';
9+
import { CaseHandler } from './CaseHandler';
510
import { EntityHandler } from './EntityHandler';
611

712
/**
@@ -10,7 +15,7 @@ import { EntityHandler } from './EntityHandler';
1015
export class HandlerFactory {
1116
/**
1217
* Get the appropriate handler for a given resource type
13-
*
18+
*
1419
* @param resource The resource type from the node parameters
1520
* @returns The appropriate entity handler
1621
*/
@@ -22,10 +27,20 @@ export class HandlerFactory {
2227
return new ContactHandler();
2328
case 'lead':
2429
return new LeadHandler();
30+
case 'meeting':
31+
return new MeetingHandler();
32+
case 'task':
33+
return new TaskHandler();
34+
case 'call':
35+
return new CallHandler();
36+
case 'opportunity':
37+
return new OpportunityHandler();
38+
case 'case':
39+
return new CaseHandler();
2540
case 'customEntity':
2641
return new CustomEntityHandler();
2742
default:
2843
throw new Error(`Unsupported resource type: ${resource}`);
2944
}
3045
}
31-
}
46+
}

0 commit comments

Comments
 (0)