first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.highLevel",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Marketing", "Sales"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/highlevel/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.highlevel/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { HighLevelV1 } from './v1/HighLevelV1.node';
|
||||
import { HighLevelV2 } from './v2/HighLevelV2.node';
|
||||
|
||||
export class HighLevel extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'HighLevel',
|
||||
name: 'highLevel',
|
||||
icon: 'file:highLevel.svg',
|
||||
group: ['transform'],
|
||||
defaultVersion: 2,
|
||||
description: 'Consume HighLevel API',
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new HighLevelV1(baseDescription),
|
||||
2: new HighLevelV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address1": {
|
||||
"type": "string"
|
||||
},
|
||||
"assignedTo": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"customField": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"fingerprint": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"fullNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"postalCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"appoinmentStatus": {
|
||||
"type": "string"
|
||||
},
|
||||
"assignedUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"calendarId": {
|
||||
"type": "string"
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isRecurring": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"additionalPhones": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"phoneLabel": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fullNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"additionalPhones": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"phoneLabel": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributionSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionSource": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"fullNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attributions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isFirst": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"utmSessionSource": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactName": {
|
||||
"type": "string"
|
||||
},
|
||||
"customFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"dnd": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"additionalPhones": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"phone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceId": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dateAdded": {
|
||||
"type": "string"
|
||||
},
|
||||
"dateUpdated": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"firstNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fullNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastNameLowerCase": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"opportunity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"internalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isAttribute": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastActionDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStageChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStatusChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"monetaryValue": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineStageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"opportunity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"indexVersion": {
|
||||
"type": "integer"
|
||||
},
|
||||
"internalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isAttribute": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastActionDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStageChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStatusChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineStageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attributions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isFirst": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"medium": {
|
||||
"type": "string"
|
||||
},
|
||||
"utmSessionSource": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"customFields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fieldValueString": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStageChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStatusChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineStageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineStageUId": {
|
||||
"type": "string"
|
||||
},
|
||||
"relations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"associationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recordId": {
|
||||
"type": "string"
|
||||
},
|
||||
"relationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"opportunity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"followers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"indexVersion": {
|
||||
"type": "integer"
|
||||
},
|
||||
"internalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isAttribute": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastActionDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStageChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastStatusChangeAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"monetaryValue": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineId": {
|
||||
"type": "string"
|
||||
},
|
||||
"pipelineStageId": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"businessId": {
|
||||
"type": "string"
|
||||
},
|
||||
"completed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"contactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"traceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5.77734" y="6.8042" width="5.29216" height="32.887" fill="#FDC400"/>
|
||||
<rect x="28.8359" y="6.8042" width="5.29216" height="32.887" fill="#4ACF27"/>
|
||||
<rect x="16.9287" y="18.9951" width="6.04819" height="20.6961" fill="#2896FB"/>
|
||||
<path d="M8.47064 0L16.9413 8.93053H0L8.47064 0Z" fill="#FDC400"/>
|
||||
<path d="M31.5292 0L39.9999 8.93053H23.0586L31.5292 0Z" fill="#4ACF27"/>
|
||||
<path d="M19.9999 12.6633L28.4706 21.5939H11.5293L19.9999 12.6633Z" fill="#2896FB"/>
|
||||
<path d="M5.77734 8.93042H11.0695V14.2226L5.77734 8.93042Z" fill="#DAA900"/>
|
||||
<path d="M28.8359 8.93042H34.1281V14.2226L28.8359 8.93042Z" fill="#51B346"/>
|
||||
<path d="M16.9287 21.594H22.9769V27.6422L16.9287 21.594Z" fill="#1F81DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 793 B |
@@ -0,0 +1,276 @@
|
||||
import type { ToISOTimeOptions } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import type {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
IPollFunctions,
|
||||
IRequestOptions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
const VALID_EMAIL_REGEX =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const VALID_PHONE_REGEX =
|
||||
/((?:\+|00)[17](?: |\-)?|(?:\+|00)[1-9]\d{0,2}(?: |\-)?|(?:\+|00)1\-\d{3}(?: |\-)?)?(0\d|\([0-9]{3}\)|[1-9]{0,3})(?:((?: |\-)[0-9]{2}){4}|((?:[0-9]{2}){4})|((?: |\-)[0-9]{3}(?: |\-)[0-9]{4})|([0-9]{7}))/;
|
||||
|
||||
export function isEmailValid(email: string): boolean {
|
||||
return VALID_EMAIL_REGEX.test(String(email).toLowerCase());
|
||||
}
|
||||
|
||||
export function isPhoneValid(phone: string): boolean {
|
||||
return VALID_PHONE_REGEX.test(String(phone));
|
||||
}
|
||||
|
||||
function dateToIsoSupressMillis(dateTime: string) {
|
||||
const options: ToISOTimeOptions = { suppressMilliseconds: true };
|
||||
return DateTime.fromISO(dateTime).toISO(options);
|
||||
}
|
||||
|
||||
export async function taskPostReceiceAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
_response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const contactId = this.getNodeParameter('contactId');
|
||||
items.forEach((item) => (item.json.contactId = contactId));
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function dueDatePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
let dueDateParam = this.getNodeParameter('dueDate', null) as string;
|
||||
if (!dueDateParam) {
|
||||
const fields = this.getNodeParameter('updateFields') as { dueDate: string };
|
||||
dueDateParam = fields.dueDate;
|
||||
}
|
||||
if (!dueDateParam) {
|
||||
throw new NodeApiError(
|
||||
this.getNode(),
|
||||
{},
|
||||
{ message: 'dueDate is required', description: 'dueDate is required' },
|
||||
);
|
||||
}
|
||||
const dueDate = dateToIsoSupressMillis(dueDateParam);
|
||||
requestOptions.body = (requestOptions.body || {}) as object;
|
||||
Object.assign(requestOptions.body, { dueDate });
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function contactIdentifierPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
requestOptions.body = (requestOptions.body || {}) as object;
|
||||
let identifier = this.getNodeParameter('contactIdentifier', null) as string;
|
||||
if (!identifier) {
|
||||
const fields = this.getNodeParameter('updateFields') as { contactIdentifier: string };
|
||||
identifier = fields.contactIdentifier;
|
||||
}
|
||||
if (isEmailValid(identifier)) {
|
||||
Object.assign(requestOptions.body, { email: identifier });
|
||||
} else if (isPhoneValid(identifier)) {
|
||||
Object.assign(requestOptions.body, { phone: identifier });
|
||||
} else {
|
||||
Object.assign(requestOptions.body, { contactId: identifier });
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function validEmailAndPhonePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body || {}) as { email?: string; phone?: string };
|
||||
|
||||
if (body.email && !isEmailValid(body.email)) {
|
||||
const message = `email "${body.email}" has invalid format`;
|
||||
throw new NodeApiError(this.getNode(), {}, { message, description: message });
|
||||
}
|
||||
|
||||
if (body.phone && !isPhoneValid(body.phone)) {
|
||||
const message = `phone "${body.phone}" has invalid format`;
|
||||
throw new NodeApiError(this.getNode(), {}, { message, description: message });
|
||||
}
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function dateTimeToEpochPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const qs = (requestOptions.qs || {}) as {
|
||||
startDate?: string | number;
|
||||
endDate?: string | number;
|
||||
};
|
||||
const toEpoch = (dt: string) => new Date(dt).getTime();
|
||||
if (qs.startDate) qs.startDate = toEpoch(qs.startDate as string);
|
||||
if (qs.endDate) qs.endDate = toEpoch(qs.endDate as string);
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function highLevelApiRequest(
|
||||
this:
|
||||
| IExecuteFunctions
|
||||
| IExecuteSingleFunctions
|
||||
| IWebhookFunctions
|
||||
| IPollFunctions
|
||||
| IHookFunctions
|
||||
| ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
) {
|
||||
let options: IRequestOptions = {
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://rest.gohighlevel.com/v1${resource}`,
|
||||
json: true,
|
||||
};
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
if (!Object.keys(qs).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
return await this.helpers.requestWithAuthentication.call(this, 'highLevelApi', options);
|
||||
}
|
||||
|
||||
export async function opportunityUpdatePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body || {}) as { title?: string; status?: string };
|
||||
if (!body.status || !body.title) {
|
||||
const pipelineId = this.getNodeParameter('pipelineId');
|
||||
const opportunityId = this.getNodeParameter('opportunityId');
|
||||
const resource = `/pipelines/${pipelineId}/opportunities/${opportunityId}`;
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', resource);
|
||||
body.status = body.status || responseData.status;
|
||||
body.title = body.title || responseData.name;
|
||||
requestOptions.body = body;
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function taskUpdatePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body || {}) as { title?: string; dueDate?: string };
|
||||
if (!body.title || !body.dueDate) {
|
||||
const contactId = this.getNodeParameter('contactId');
|
||||
const taskId = this.getNodeParameter('taskId');
|
||||
const resource = `/contacts/${contactId}/tasks/${taskId}`;
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', resource);
|
||||
body.title = body.title || responseData.title;
|
||||
// the api response dueDate has to be formatted or it will error on update
|
||||
body.dueDate = body.dueDate || dateToIsoSupressMillis(responseData.dueDate as string);
|
||||
requestOptions.body = body;
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function splitTagsPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body || {}) as IDataObject;
|
||||
if (body.tags) {
|
||||
if (Array.isArray(body.tags)) return requestOptions;
|
||||
body.tags = (body.tags as string).split(',').map((tag) => tag.trim());
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function highLevelApiPagination(
|
||||
this: IExecutePaginationFunctions,
|
||||
requestData: DeclarativeRestApiSettings.ResultOptions,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const responseData: INodeExecutionData[] = [];
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', false) as boolean;
|
||||
|
||||
const resourceMapping: { [key: string]: string } = {
|
||||
contact: 'contacts',
|
||||
opportunity: 'opportunities',
|
||||
};
|
||||
const rootProperty = resourceMapping[resource];
|
||||
|
||||
requestData.options.qs = requestData.options.qs || {};
|
||||
if (returnAll) requestData.options.qs.limit = 100;
|
||||
|
||||
let responseTotal = 0;
|
||||
|
||||
do {
|
||||
const pageResponseData: INodeExecutionData[] = await this.makeRoutingRequest(requestData);
|
||||
const items = pageResponseData[0].json[rootProperty] as [];
|
||||
items.forEach((item) => responseData.push({ json: item }));
|
||||
|
||||
const meta = pageResponseData[0].json.meta as IDataObject;
|
||||
const startAfterId = meta.startAfterId as string;
|
||||
const startAfter = meta.startAfter as number;
|
||||
requestData.options.qs = { startAfterId, startAfter };
|
||||
responseTotal = (meta.total as number) || 0;
|
||||
} while (returnAll && responseTotal > responseData.length);
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
||||
export async function getPipelineStages(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const pipelineId = this.getCurrentNodeParameter('pipelineId') as string;
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', '/pipelines');
|
||||
const pipelines = responseData.pipelines as [
|
||||
{ id: string; stages: [{ id: string; name: string }] },
|
||||
];
|
||||
const pipeline = pipelines.find((p) => p.id === pipelineId);
|
||||
if (pipeline) {
|
||||
const options: INodePropertyOptions[] = pipeline.stages.map((stage) => {
|
||||
const name = stage.name;
|
||||
const value = stage.id;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', '/users');
|
||||
const users = responseData.users as [{ id: string; name: string; email: string }];
|
||||
const options: INodePropertyOptions[] = users.map((user) => {
|
||||
const name = user.name;
|
||||
const value = user.id;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function getTimezones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', '/timezones');
|
||||
const timezones = responseData.timezones as string[];
|
||||
return timezones.map((zone) => ({
|
||||
name: zone,
|
||||
value: zone,
|
||||
})) as INodePropertyOptions[];
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type {
|
||||
INodeProperties,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { contactFields, contactNotes, contactOperations } from './description/ContactDescription';
|
||||
import { opportunityFields, opportunityOperations } from './description/OpportunityDescription';
|
||||
import { taskFields, taskOperations } from './description/TaskDescription';
|
||||
import {
|
||||
getPipelineStages,
|
||||
getTimezones,
|
||||
getUsers,
|
||||
highLevelApiPagination,
|
||||
} from './GenericFunctions';
|
||||
|
||||
const resources: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Contact',
|
||||
value: 'contact',
|
||||
},
|
||||
{
|
||||
name: 'Opportunity',
|
||||
value: 'opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Task',
|
||||
value: 'task',
|
||||
},
|
||||
],
|
||||
default: 'contact',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'HighLevel',
|
||||
name: 'highLevel',
|
||||
icon: 'file:highLevel.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Consume HighLevel API v1',
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
defaults: {
|
||||
name: 'HighLevel',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'highLevelApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
baseURL: 'https://rest.gohighlevel.com/v1',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
requestOperations: {
|
||||
pagination: highLevelApiPagination,
|
||||
},
|
||||
properties: [
|
||||
...resources,
|
||||
...contactOperations,
|
||||
...contactNotes,
|
||||
...contactFields,
|
||||
...opportunityOperations,
|
||||
...opportunityFields,
|
||||
...taskOperations,
|
||||
...taskFields,
|
||||
],
|
||||
};
|
||||
|
||||
export class HighLevelV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getPipelineStages,
|
||||
getUsers,
|
||||
getTimezones,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,857 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { splitTagsPreSendAction, validEmailAndPhonePreSendAction } from '../GenericFunctions';
|
||||
|
||||
export const contactOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create or Update',
|
||||
value: 'create',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/contacts',
|
||||
},
|
||||
send: {
|
||||
preSend: [validEmailAndPhonePreSendAction, splitTagsPreSendAction],
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Create or update a contact',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/contacts/{{$parameter.contactId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts',
|
||||
},
|
||||
send: {
|
||||
paginate: true,
|
||||
},
|
||||
},
|
||||
action: 'Get many contacts',
|
||||
},
|
||||
{
|
||||
name: 'Lookup',
|
||||
value: 'lookup',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/lookup?email={{$parameter.email}}&phone={{$parameter.phone}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contacts',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Lookup a contact',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/contacts/{{$parameter.contactId}}',
|
||||
},
|
||||
send: {
|
||||
preSend: [validEmailAndPhonePreSendAction, splitTagsPreSendAction],
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Update a contact',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export const contactNotes: INodeProperties[] = [
|
||||
{
|
||||
displayName:
|
||||
'Create a new contact or update an existing one if email or phone matches (upsert)',
|
||||
name: 'contactCreateNotice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const customFields: INodeProperties = {
|
||||
displayName: 'Custom Fields',
|
||||
name: 'customFields',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'values',
|
||||
displayName: 'Value',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name or ID',
|
||||
name: 'fieldId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptions: {
|
||||
routing: {
|
||||
request: {
|
||||
url: '/custom-fields',
|
||||
method: 'GET',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'customFields',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'setKeyValue',
|
||||
properties: {
|
||||
name: '={{$responseItem.name}}',
|
||||
value: '={{$responseItem.id}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
properties: {
|
||||
key: 'name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
value: '={{$value}}',
|
||||
property: '=customField.{{$parent.fieldId}}',
|
||||
type: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
description: 'Email or Phone are required to create contact',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
description:
|
||||
'Phone or Email are required to create contact. Phone number has to start with a valid <a href="https://en.wikipedia.org/wiki/List_of_country_calling_codes">country code</a> leading with + sign.',
|
||||
placeholder: '+491234567890',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'phone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'address1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'city',
|
||||
},
|
||||
},
|
||||
},
|
||||
customFields,
|
||||
{
|
||||
displayName: 'Do Not Disturb',
|
||||
name: 'dnd',
|
||||
description:
|
||||
'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'firstName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'lastName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'e.g. John Deo',
|
||||
description:
|
||||
"The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set",
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'postalCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source',
|
||||
name: 'source',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. Public API',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'source',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTimezones',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'timezone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'website',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'address1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'city',
|
||||
},
|
||||
},
|
||||
},
|
||||
customFields,
|
||||
{
|
||||
displayName: 'Do Not Disturb',
|
||||
name: 'dnd',
|
||||
description:
|
||||
'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'firstName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'lastName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description:
|
||||
"The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set",
|
||||
default: 'e.g. John Deo',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Phone number has to start with a valid <a href="https://en.wikipedia.org/wiki/List_of_country_calling_codes">country code</a> leading with + sign',
|
||||
placeholder: '+491234567890',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'phone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'postalCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTimezones',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'timezone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'website',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
output: {
|
||||
maxResults: '={{$value}}', // Set maxResults to the value of current parameter
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query will search on these fields: Name, Phone, Email, Tags, and Company Name',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'query',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Order',
|
||||
name: 'order',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Descending',
|
||||
value: 'desc',
|
||||
},
|
||||
{
|
||||
name: 'Ascending',
|
||||
value: 'asc',
|
||||
},
|
||||
],
|
||||
default: 'desc',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'order',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sort By',
|
||||
name: 'sortBy',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Date Added',
|
||||
value: 'date_added',
|
||||
},
|
||||
{
|
||||
name: 'Date Updated',
|
||||
value: 'date_updated',
|
||||
},
|
||||
],
|
||||
default: 'date_added',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'sortBy',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const lookupProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
description:
|
||||
'Lookup Contact by Email. If Email is not found it will try to find a contact by phone.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['lookup'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
description:
|
||||
'Lookup Contact by Phone. It will first try to find a contact by Email and than by Phone.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['lookup'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const contactFields: INodeProperties[] = [
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
...lookupProperties,
|
||||
];
|
||||
@@ -0,0 +1,740 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
contactIdentifierPreSendAction,
|
||||
dateTimeToEpochPreSendAction,
|
||||
opportunityUpdatePreSendAction,
|
||||
splitTagsPreSendAction,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export const opportunityOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [splitTagsPreSendAction],
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/pipelines/{{$parameter.pipelineId}}/opportunities',
|
||||
},
|
||||
},
|
||||
action: 'Create an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
},
|
||||
action: 'Get an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/pipelines/{{$parameter.pipelineId}}/opportunities',
|
||||
},
|
||||
send: {
|
||||
paginate: true,
|
||||
},
|
||||
},
|
||||
action: 'Get many opportunities',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
send: {
|
||||
preSend: [opportunityUpdatePreSendAction, splitTagsPreSendAction],
|
||||
},
|
||||
},
|
||||
action: 'Update an opportunity',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
const pipelineId: INodeProperties = {
|
||||
displayName: 'Pipeline Name or ID',
|
||||
name: 'pipelineId',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create', 'delete', 'get', 'getAll', 'update'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptions: {
|
||||
routing: {
|
||||
request: {
|
||||
url: '/pipelines',
|
||||
method: 'GET',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'pipelines',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'setKeyValue',
|
||||
properties: {
|
||||
name: '={{$responseItem.name}}',
|
||||
value: '={{$responseItem.id}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
properties: {
|
||||
key: 'name',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
};
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
required: true,
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'stageId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Identifier',
|
||||
name: 'contactIdentifier',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Either Email, Phone or Contact ID',
|
||||
hint: 'There can only be one opportunity for each contact.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [contactIdentifierPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose staff member from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Company Name',
|
||||
name: 'companyName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'companyName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Monetary Value',
|
||||
name: 'monetaryValue',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description: 'Monetary value of lead opportunity',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'monetaryValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. John Deo',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose staff member from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Campaign ID',
|
||||
name: 'campaignId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'campaignId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'End Date',
|
||||
name: 'endDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'endDate',
|
||||
preSend: [dateTimeToEpochPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
// api should filter by monetary value but doesn't
|
||||
// {
|
||||
// displayName: 'Monetary Value',
|
||||
// name: 'monetaryValue',
|
||||
// type: 'number',
|
||||
// default: '',
|
||||
// routing: {
|
||||
// send: {
|
||||
// type: 'query',
|
||||
// property: 'monetaryValue',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'stageId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Start Date',
|
||||
name: 'startDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'startDate',
|
||||
preSend: [dateTimeToEpochPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query will search on these fields: Name, Phone, Email, Tags, and Company Name',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'query',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
hint: "You cannot update an opportunity's pipeline ID.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||
description:
|
||||
'Choose staff member from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Company Name',
|
||||
name: 'companyName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'companyName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Identifier',
|
||||
name: 'contactIdentifier',
|
||||
type: 'string',
|
||||
description: 'Either Email, Phone or Contact ID',
|
||||
hint: 'There can only be one opportunity for each contact.',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [contactIdentifierPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Monetary Value',
|
||||
name: 'monetaryValue',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description: 'Monetary value of lead opportunity',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'monetaryValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. John Deo',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'stageId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const opportunityFields: INodeProperties[] = [
|
||||
pipelineId,
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
];
|
||||
@@ -0,0 +1,479 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
dueDatePreSendAction,
|
||||
taskPostReceiceAction,
|
||||
taskUpdatePreSendAction,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export const taskOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks',
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Create a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Get a task',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'tasks',
|
||||
},
|
||||
},
|
||||
taskPostReceiceAction,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get many tasks',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}',
|
||||
},
|
||||
send: {
|
||||
preSend: [taskUpdatePreSendAction],
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Update a task',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Contact the task belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dueDate',
|
||||
preSend: [dueDatePreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'description',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Incompleted',
|
||||
value: 'incompleted',
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
value: 'completed',
|
||||
},
|
||||
],
|
||||
default: 'incompleted',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Contact the task belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Contact the task belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Contact the task belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Contact the task belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'description',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dueDate',
|
||||
preSend: [dueDatePreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Incompleted',
|
||||
value: 'incompleted',
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
value: 'completed',
|
||||
},
|
||||
],
|
||||
default: 'incompleted',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const taskFields: INodeProperties[] = [
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
];
|
||||
@@ -0,0 +1,416 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import type { ToISOTimeOptions } from 'luxon';
|
||||
import type {
|
||||
DeclarativeRestApiSettings,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecutePaginationFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
IHttpRequestMethods,
|
||||
IHttpRequestOptions,
|
||||
ILoadOptionsFunctions,
|
||||
IN8nHttpFullResponse,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
IPollFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { ApplicationError, NodeApiError } from 'n8n-workflow';
|
||||
|
||||
const VALID_EMAIL_REGEX =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const VALID_PHONE_REGEX =
|
||||
/((?:\+|00)[17](?: |\-)?|(?:\+|00)[1-9]\d{0,2}(?: |\-)?|(?:\+|00)1\-\d{3}(?: |\-)?)?(0\d|\([0-9]{3}\)|[1-9]{0,3})(?:((?: |\-)[0-9]{2}){4}|((?:[0-9]{2}){4})|((?: |\-)[0-9]{3}(?: |\-)[0-9]{4})|([0-9]{7}))/;
|
||||
|
||||
export function isEmailValid(email: string): boolean {
|
||||
return VALID_EMAIL_REGEX.test(String(email).toLowerCase());
|
||||
}
|
||||
|
||||
export function isPhoneValid(phone: string): boolean {
|
||||
return VALID_PHONE_REGEX.test(String(phone));
|
||||
}
|
||||
|
||||
export function dateToIsoSupressMillis(dateTime: string) {
|
||||
const options: ToISOTimeOptions = { suppressMilliseconds: true };
|
||||
return DateTime.fromISO(dateTime).toISO(options);
|
||||
}
|
||||
|
||||
export async function taskPostReceiceAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
_response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const contactId = this.getNodeParameter('contactId');
|
||||
items.forEach((item) => (item.json.contactId = contactId));
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function dueDatePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
let dueDateParam = this.getNodeParameter('dueDate', null) as string;
|
||||
if (!dueDateParam) {
|
||||
const fields = this.getNodeParameter('updateFields') as { dueDate: string };
|
||||
dueDateParam = fields.dueDate;
|
||||
}
|
||||
if (!dueDateParam) {
|
||||
throw new NodeApiError(
|
||||
this.getNode(),
|
||||
{},
|
||||
{ message: 'dueDate is required', description: 'dueDate is required' },
|
||||
);
|
||||
}
|
||||
const dueDate = dateToIsoSupressMillis(dueDateParam);
|
||||
requestOptions.body = (requestOptions.body ?? {}) as object;
|
||||
Object.assign(requestOptions.body, { dueDate });
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function contactIdentifierPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
requestOptions.body = (requestOptions.body ?? {}) as object;
|
||||
let identifier = this.getNodeParameter('contactIdentifier', null) as string;
|
||||
if (!identifier) {
|
||||
const fields = this.getNodeParameter('updateFields') as { contactIdentifier: string };
|
||||
identifier = fields.contactIdentifier;
|
||||
}
|
||||
if (isEmailValid(identifier)) {
|
||||
Object.assign(requestOptions.body, { email: identifier });
|
||||
} else if (isPhoneValid(identifier)) {
|
||||
Object.assign(requestOptions.body, { phone: identifier });
|
||||
} else {
|
||||
Object.assign(requestOptions.body, { contactId: identifier });
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function validEmailAndPhonePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body ?? {}) as { email?: string; phone?: string };
|
||||
|
||||
if (body.email && !isEmailValid(body.email)) {
|
||||
const message = `email "${body.email}" has invalid format`;
|
||||
throw new NodeApiError(this.getNode(), {}, { message, description: message });
|
||||
}
|
||||
|
||||
if (body.phone && !isPhoneValid(body.phone)) {
|
||||
const message = `phone "${body.phone}" has invalid format`;
|
||||
throw new NodeApiError(this.getNode(), {}, { message, description: message });
|
||||
}
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function dateTimeToEpochPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const qs = (requestOptions.qs ?? {}) as {
|
||||
startDate?: string | number;
|
||||
endDate?: string | number;
|
||||
};
|
||||
const toEpoch = (dt: string) => new Date(dt).getTime();
|
||||
if (qs.startDate) qs.startDate = toEpoch(qs.startDate as string);
|
||||
if (qs.endDate) qs.endDate = toEpoch(qs.endDate as string);
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function addLocationIdPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
|
||||
if (resource === 'contact') {
|
||||
if (operation === 'getAll') {
|
||||
requestOptions.qs = requestOptions.qs ?? {};
|
||||
Object.assign(requestOptions.qs, { locationId });
|
||||
}
|
||||
if (operation === 'create') {
|
||||
requestOptions.body = requestOptions.body ?? {};
|
||||
Object.assign(requestOptions.body, { locationId });
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'opportunity') {
|
||||
if (operation === 'create') {
|
||||
requestOptions.body = requestOptions.body ?? {};
|
||||
Object.assign(requestOptions.body, { locationId });
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
requestOptions.qs = requestOptions.qs ?? {};
|
||||
Object.assign(requestOptions.qs, { location_id: locationId });
|
||||
}
|
||||
}
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function highLevelApiRequest(
|
||||
this:
|
||||
| IExecuteFunctions
|
||||
| IExecuteSingleFunctions
|
||||
| IWebhookFunctions
|
||||
| IPollFunctions
|
||||
| IHookFunctions
|
||||
| ILoadOptionsFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
url?: string,
|
||||
option: IDataObject = {},
|
||||
) {
|
||||
let options: IHttpRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
url: url ?? `https://services.leadconnectorhq.com${resource}`,
|
||||
json: true,
|
||||
};
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
if (!Object.keys(qs).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, 'highLevelOAuth2Api', options);
|
||||
}
|
||||
|
||||
export const addNotePostReceiveAction = async function (
|
||||
this: IExecuteSingleFunctions,
|
||||
items: INodeExecutionData[],
|
||||
response: IN8nHttpFullResponse,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const note = this.getNodeParameter('additionalFields.notes', 0) as string;
|
||||
|
||||
if (!note) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const contact: IDataObject = (response.body as IDataObject).contact as IDataObject;
|
||||
|
||||
// Ensure there is a valid response and extract contactId and userId
|
||||
if (!response || !response.body || !contact) {
|
||||
throw new ApplicationError('No response data available to extract contact ID and user ID.');
|
||||
}
|
||||
|
||||
const contactId = contact.id;
|
||||
const userId = contact.locationId;
|
||||
|
||||
const requestBody = {
|
||||
userId,
|
||||
body: note,
|
||||
};
|
||||
|
||||
await highLevelApiRequest.call(this, 'POST', `/contacts/${contactId}/notes`, requestBody, {});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export async function taskUpdatePreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body ?? {}) as { title?: string; dueDate?: string };
|
||||
if (!body.title || !body.dueDate) {
|
||||
const contactId = this.getNodeParameter('contactId');
|
||||
const taskId = this.getNodeParameter('taskId');
|
||||
const resource = `/contacts/${contactId}/tasks/${taskId}`;
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', resource);
|
||||
body.title = body.title || responseData.title;
|
||||
// the api response dueDate has to be formatted or it will error on update
|
||||
body.dueDate = body.dueDate || dateToIsoSupressMillis(responseData.dueDate as string);
|
||||
requestOptions.body = body;
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function splitTagsPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const body = (requestOptions.body ?? {}) as IDataObject;
|
||||
if (body.tags) {
|
||||
if (Array.isArray(body.tags)) return requestOptions;
|
||||
body.tags = (body.tags as string).split(',').map((tag) => tag.trim());
|
||||
}
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function highLevelApiPagination(
|
||||
this: IExecutePaginationFunctions,
|
||||
requestData: DeclarativeRestApiSettings.ResultOptions,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const responseData: INodeExecutionData[] = [];
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', false) as boolean;
|
||||
|
||||
const resourceMapping: { [key: string]: string } = {
|
||||
contact: 'contacts',
|
||||
opportunity: 'opportunities',
|
||||
};
|
||||
const rootProperty = resourceMapping[resource];
|
||||
|
||||
requestData.options.qs = requestData.options.qs ?? {};
|
||||
if (returnAll) requestData.options.qs.limit = 100;
|
||||
|
||||
let responseTotal = 0;
|
||||
|
||||
do {
|
||||
const pageResponseData: INodeExecutionData[] = await this.makeRoutingRequest(requestData);
|
||||
const items = pageResponseData[0].json[rootProperty] as [];
|
||||
items.forEach((item) => responseData.push({ json: item }));
|
||||
|
||||
const meta = pageResponseData[0].json.meta as IDataObject;
|
||||
const startAfterId = meta.startAfterId as string;
|
||||
const startAfter = meta.startAfter as number;
|
||||
requestData.options.qs = { startAfterId, startAfter };
|
||||
responseTotal = (meta.total as number) || 0;
|
||||
} while (returnAll && responseTotal > responseData.length);
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
||||
export async function getPipelineStages(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
|
||||
let pipelineId = '';
|
||||
if (operation === 'create') {
|
||||
pipelineId = this.getCurrentNodeParameter('pipelineId') as string;
|
||||
}
|
||||
if (operation === 'update') {
|
||||
pipelineId = this.getNodeParameter('updateFields.pipelineId') as string;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
pipelineId = this.getNodeParameter('filters.pipelineId') as string;
|
||||
}
|
||||
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
|
||||
const pipelines = (
|
||||
await highLevelApiRequest.call(this, 'GET', '/opportunities/pipelines', undefined, {
|
||||
locationId,
|
||||
})
|
||||
).pipelines as IDataObject[];
|
||||
|
||||
const pipeline = pipelines.find((p) => p.id === pipelineId);
|
||||
if (pipeline) {
|
||||
const options: INodePropertyOptions[] = (pipeline.stages as IDataObject[]).map((stage) => {
|
||||
const name = stage.name as string;
|
||||
const value = stage.id as string;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
export async function getPipelines(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
const responseData = await highLevelApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/opportunities/pipelines',
|
||||
undefined,
|
||||
{ locationId },
|
||||
);
|
||||
|
||||
const pipelines = responseData.pipelines as [{ id: string; name: string; email: string }];
|
||||
const options: INodePropertyOptions[] = pipelines.map((pipeline) => {
|
||||
const name = pipeline.name;
|
||||
const value = pipeline.id;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function getContacts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', '/contacts/', undefined, {
|
||||
locationId,
|
||||
});
|
||||
|
||||
const contacts = responseData.contacts as [{ id: string; name: string; email: string }];
|
||||
const options: INodePropertyOptions[] = contacts.map((contact) => {
|
||||
const name = contact.email;
|
||||
const value = contact.id;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
const responseData = await highLevelApiRequest.call(this, 'GET', '/users/', undefined, {
|
||||
locationId,
|
||||
});
|
||||
|
||||
const users = responseData.users as [{ id: string; name: string; email: string }];
|
||||
const options: INodePropertyOptions[] = users.map((user) => {
|
||||
const name = user.name;
|
||||
const value = user.id;
|
||||
return { name, value };
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function addCustomFieldsPreSendAction(
|
||||
this: IExecuteSingleFunctions,
|
||||
requestOptions: IHttpRequestOptions,
|
||||
): Promise<IHttpRequestOptions> {
|
||||
const requestBody = requestOptions.body as IDataObject;
|
||||
|
||||
if (requestBody && requestBody.customFields) {
|
||||
const rawCustomFields = requestBody.customFields as IDataObject;
|
||||
|
||||
// Define the structure of fieldId
|
||||
interface FieldIdType {
|
||||
value: unknown;
|
||||
cachedResultName?: string;
|
||||
}
|
||||
|
||||
// Ensure rawCustomFields.values is an array of objects with fieldId and fieldValue
|
||||
if (rawCustomFields && Array.isArray(rawCustomFields.values)) {
|
||||
const formattedCustomFields = rawCustomFields.values.map((field: unknown) => {
|
||||
// Assert that field is of the expected shape
|
||||
const typedField = field as { fieldId: FieldIdType; fieldValue: unknown };
|
||||
|
||||
const fieldId = typedField.fieldId;
|
||||
|
||||
if (typeof fieldId === 'object' && fieldId !== null && 'value' in fieldId) {
|
||||
return {
|
||||
id: fieldId.value,
|
||||
key: fieldId.cachedResultName ?? 'default_key',
|
||||
field_value: typedField.fieldValue,
|
||||
};
|
||||
} else {
|
||||
throw new ApplicationError('Error processing custom fields.');
|
||||
}
|
||||
});
|
||||
requestBody.customFields = formattedCustomFields;
|
||||
}
|
||||
}
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
INodeProperties,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { calendarFields, calendarOperations } from './description/CalendarDescription';
|
||||
import { contactFields, contactNotes, contactOperations } from './description/ContactDescription';
|
||||
import { opportunityFields, opportunityOperations } from './description/OpportunityDescription';
|
||||
import { taskFields, taskOperations } from './description/TaskDescription';
|
||||
import {
|
||||
getContacts,
|
||||
getPipelines,
|
||||
getPipelineStages,
|
||||
getUsers,
|
||||
highLevelApiPagination,
|
||||
} from './GenericFunctions';
|
||||
|
||||
const resources: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Contact',
|
||||
value: 'contact',
|
||||
},
|
||||
{
|
||||
name: 'Opportunity',
|
||||
value: 'opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Task',
|
||||
value: 'task',
|
||||
},
|
||||
{
|
||||
name: 'Calendar',
|
||||
value: 'calendar',
|
||||
},
|
||||
],
|
||||
default: 'contact',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'HighLevel',
|
||||
name: 'highLevel',
|
||||
icon: 'file:highLevel.svg',
|
||||
group: ['transform'],
|
||||
version: 2,
|
||||
description: 'Consume HighLevel API v2',
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
defaults: {
|
||||
name: 'HighLevel',
|
||||
},
|
||||
builderHint: {
|
||||
message: 'To add notes to contacts, set additionalFields.notes',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'highLevelOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
requestDefaults: {
|
||||
baseURL: 'https://services.leadconnectorhq.com',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
},
|
||||
requestOperations: {
|
||||
pagination: highLevelApiPagination,
|
||||
},
|
||||
properties: [
|
||||
...resources,
|
||||
...contactOperations,
|
||||
...contactNotes,
|
||||
...contactFields,
|
||||
...opportunityOperations,
|
||||
...opportunityFields,
|
||||
...taskOperations,
|
||||
...taskFields,
|
||||
...calendarOperations,
|
||||
...calendarFields,
|
||||
],
|
||||
};
|
||||
|
||||
export class HighLevelV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
getPipelines,
|
||||
getContacts,
|
||||
getPipelineStages,
|
||||
getUsers,
|
||||
},
|
||||
listSearch: {
|
||||
async searchCustomFields(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
|
||||
const responseData: IDataObject = (await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'highLevelOAuth2Api',
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
url: `https://services.leadconnectorhq.com/locations/${locationId}/customFields?model=contact`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
},
|
||||
)) as IDataObject;
|
||||
|
||||
const customFields = responseData.customFields as Array<{ name: string; id: string }>;
|
||||
|
||||
const results: INodeListSearchItems[] = customFields
|
||||
.map((a) => ({
|
||||
name: a.name,
|
||||
value: a.id,
|
||||
}))
|
||||
.filter((a) => !filter || a.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return { results };
|
||||
},
|
||||
async searchTimezones(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const { locationId } =
|
||||
((await this.getCredentials('highLevelOAuth2Api'))?.oauthTokenData as IDataObject) ?? {};
|
||||
|
||||
const responseData: IDataObject = (await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'highLevelOAuth2Api',
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
url: `https://services.leadconnectorhq.com/locations/${locationId}/timezones`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
},
|
||||
)) as IDataObject;
|
||||
|
||||
const timezones = responseData.timeZones as string[];
|
||||
|
||||
const results: INodeListSearchItems[] = timezones
|
||||
.map((zone) => ({
|
||||
name: zone.trim(),
|
||||
value: zone.trim(),
|
||||
}))
|
||||
.filter((zone) => !filter || zone.name.toLowerCase().includes(filter.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return { results };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const calendarOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Book Appointment',
|
||||
value: 'bookAppointment',
|
||||
action: 'Book appointment in a calendar',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/calendars/events/appointments',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Get Free Slots',
|
||||
value: 'getFreeSlots',
|
||||
action: 'Get free slots of a calendar',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/calendars/{{$parameter.calendarId}}/free-slots',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
default: 'bookAppointment',
|
||||
noDataExpression: true,
|
||||
},
|
||||
];
|
||||
|
||||
const bookAppointmentProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Calendar ID',
|
||||
name: 'calendarId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['bookAppointment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'calendarId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Location ID',
|
||||
name: 'locationId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['bookAppointment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'locationId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['bookAppointment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'contactId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Start Time',
|
||||
name: 'startTime',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Example: 2021-06-23T03:30:00+05:30',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['bookAppointment'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'startTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['bookAppointment'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'End Time',
|
||||
name: 'endTime',
|
||||
type: 'string',
|
||||
description: 'Example: 2021-06-23T04:30:00+05:30',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'endTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Appointment Status',
|
||||
name: 'appointmentStatus',
|
||||
type: 'options',
|
||||
default: 'new',
|
||||
description:
|
||||
'The status of the appointment. Allowed values: new, confirmed, cancelled, showed, noshow, invalid.',
|
||||
options: [
|
||||
{
|
||||
name: 'Cancelled',
|
||||
value: 'cancelled',
|
||||
},
|
||||
{
|
||||
name: 'Confirmed',
|
||||
value: 'confirmed',
|
||||
},
|
||||
{
|
||||
name: 'Invalid',
|
||||
value: 'invalid',
|
||||
},
|
||||
{
|
||||
name: 'New',
|
||||
value: 'new',
|
||||
},
|
||||
{
|
||||
name: 'No Show',
|
||||
value: 'noshow',
|
||||
},
|
||||
{
|
||||
name: 'Showed',
|
||||
value: 'showed',
|
||||
},
|
||||
],
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'appointmentStatus',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Assigned User ID',
|
||||
name: 'assignedUserId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedUserId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'address',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore Date Range',
|
||||
name: 'ignoreDateRange',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'ignoreDateRange',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Notify',
|
||||
name: 'toNotify',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'toNotify',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getFreeSlotsProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Calendar ID',
|
||||
name: 'calendarId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['getFreeSlots'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Start Date',
|
||||
name: 'startDate',
|
||||
type: 'number',
|
||||
//type: 'dateTime' TODO
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'The start date for fetching free calendar slots. Example: 1548898600000.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['getFreeSlots'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'startDate',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'End Date',
|
||||
name: 'endDate',
|
||||
type: 'number',
|
||||
//type: 'dateTime' TODO
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'The end date for fetching free calendar slots. Example: 1601490599999.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['getFreeSlots'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'endDate',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['calendar'],
|
||||
operation: ['getFreeSlots'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The timezone to use for the returned slots. Example: America/Chihuahua.',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'timezone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'userId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'User ID to filter the slots (optional)',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'userId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'User IDs',
|
||||
name: 'userIds',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'User IDs',
|
||||
name: 'userIds',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Comma-separated list of user IDs to filter the slots',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'userIds',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Apply Look Busy',
|
||||
name: 'enableLookBusy',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description: 'Apply Look Busy to the slots',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'enableLookBusy',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const calendarFields: INodeProperties[] = [
|
||||
...bookAppointmentProperties,
|
||||
...getFreeSlotsProperties,
|
||||
];
|
||||
@@ -0,0 +1,871 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
addCustomFieldsPreSendAction,
|
||||
addLocationIdPreSendAction,
|
||||
addNotePostReceiveAction,
|
||||
splitTagsPreSendAction,
|
||||
validEmailAndPhonePreSendAction,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export const contactOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create or Update',
|
||||
value: 'create',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/contacts/upsert/',
|
||||
},
|
||||
send: {
|
||||
preSend: [
|
||||
validEmailAndPhonePreSendAction,
|
||||
splitTagsPreSendAction,
|
||||
addLocationIdPreSendAction,
|
||||
addCustomFieldsPreSendAction,
|
||||
],
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
addNotePostReceiveAction,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Create or update a contact',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/contacts/{{$parameter.contactId}}/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/',
|
||||
},
|
||||
send: {
|
||||
preSend: [addLocationIdPreSendAction],
|
||||
paginate: true,
|
||||
},
|
||||
},
|
||||
action: 'Get many contacts',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/contacts/{{$parameter.contactId}}/',
|
||||
},
|
||||
send: {
|
||||
preSend: [validEmailAndPhonePreSendAction, splitTagsPreSendAction],
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'contact',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Update a contact',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export const contactNotes: INodeProperties[] = [
|
||||
{
|
||||
displayName:
|
||||
'Create a new contact or update an existing one if email or phone matches (upsert)',
|
||||
name: 'contactCreateNotice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const customFields: INodeProperties = {
|
||||
displayName: 'Custom Fields',
|
||||
name: 'customFields',
|
||||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'values',
|
||||
displayName: 'Value',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Name or ID',
|
||||
name: 'fieldId',
|
||||
required: true,
|
||||
type: 'resourceLocator',
|
||||
default: '',
|
||||
description: 'Choose from the list, or specify an ID using an expression',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchCustomFields',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'Enter Custom Field ID',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'fieldValue',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'customFields',
|
||||
value:
|
||||
'={{ $parent.values.map(field => ({ fieldId: { id: field.fieldId.id }, field_value: field.fieldValue })) }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'customFields',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
description: 'Email or Phone are required to create contact',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
description:
|
||||
'Phone or Email are required to create contact. Phone number has to start with a valid <a href="https://en.wikipedia.org/wiki/List_of_country_calling_codes">country code</a> leading with + sign.',
|
||||
placeholder: '+491234567890',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'phone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'address1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'city',
|
||||
},
|
||||
},
|
||||
},
|
||||
customFields,
|
||||
{
|
||||
displayName: 'Do Not Disturb',
|
||||
name: 'dnd',
|
||||
description:
|
||||
'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'firstName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'lastName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: 'e.g. John Deo',
|
||||
description:
|
||||
"The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set",
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'postalCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source',
|
||||
name: 'source',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. Public API',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'source',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Note',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
placeholder: 'Select Timezone',
|
||||
type: 'resourceLocator',
|
||||
default: '',
|
||||
description: 'Choose from the list, or specify a timezone using an expression',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchTimezones',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'Enter Timezone ID',
|
||||
},
|
||||
],
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'timezone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'website',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'address1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'city',
|
||||
},
|
||||
},
|
||||
},
|
||||
customFields,
|
||||
{
|
||||
displayName: 'Do Not Disturb',
|
||||
name: 'dnd',
|
||||
description:
|
||||
'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'email',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'firstName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'lastName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
description:
|
||||
"The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set",
|
||||
default: 'e.g. John Deo',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Phone number has to start with a valid <a href="https://en.wikipedia.org/wiki/List_of_country_calling_codes">country code</a> leading with + sign',
|
||||
placeholder: '+491234567890',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'phone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
name: 'postalCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'postalCode',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'state',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'timezone',
|
||||
placeholder: 'Select Timezone',
|
||||
type: 'resourceLocator',
|
||||
default: '',
|
||||
description: 'Choose from the list, or specify a timezone using an expression',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchTimezones',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'Enter Timezone ID',
|
||||
},
|
||||
],
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'timezone',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'website',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 50,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
output: {
|
||||
maxResults: '={{$value}}', // Set maxResults to the value of current parameter
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query will search on these fields: Name, Phone, Email, Tags, and Company Name',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'query',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Order',
|
||||
name: 'order',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Descending',
|
||||
value: 'desc',
|
||||
},
|
||||
{
|
||||
name: 'Ascending',
|
||||
value: 'asc',
|
||||
},
|
||||
],
|
||||
default: 'desc',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'order',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sort By',
|
||||
name: 'sortBy',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Date Added',
|
||||
value: 'date_added',
|
||||
},
|
||||
{
|
||||
name: 'Date Updated',
|
||||
value: 'date_updated',
|
||||
},
|
||||
],
|
||||
default: 'date_added',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'sortBy',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const lookupProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
placeholder: 'name@email.com',
|
||||
description:
|
||||
'Lookup Contact by Email. If Email is not found it will try to find a contact by phone.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['lookup'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
description:
|
||||
'Lookup Contact by Phone. It will first try to find a contact by Email and than by Phone.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['contact'],
|
||||
operation: ['lookup'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const contactFields: INodeProperties[] = [
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
...lookupProperties,
|
||||
];
|
||||
@@ -0,0 +1,669 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
addLocationIdPreSendAction,
|
||||
dateTimeToEpochPreSendAction,
|
||||
splitTagsPreSendAction,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export const opportunityOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
routing: {
|
||||
send: {
|
||||
preSend: [splitTagsPreSendAction, addLocationIdPreSendAction],
|
||||
},
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '/opportunities/',
|
||||
},
|
||||
},
|
||||
action: 'Create an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
},
|
||||
action: 'Get an opportunity',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/opportunities/search',
|
||||
},
|
||||
send: {
|
||||
preSend: [addLocationIdPreSendAction],
|
||||
paginate: true,
|
||||
},
|
||||
},
|
||||
action: 'Get many opportunities',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/opportunities/{{$parameter.opportunityId}}',
|
||||
},
|
||||
},
|
||||
action: 'Update an opportunity',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
const pipelineId: INodeProperties = {
|
||||
displayName: 'Pipeline Name or ID',
|
||||
name: 'pipelineId',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPipelines',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'pipelineId',
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
};
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
required: true,
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
hint: 'There can only be one opportunity for each contact',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'contactId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Company Name',
|
||||
name: 'companyName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'companyName',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Monetary Value',
|
||||
name: 'monetaryValue',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description: 'Monetary value of lead opportunity',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'monetaryValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'string',
|
||||
hint: 'Comma separated list of tags, array of strings can be set in expression',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'tags',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
|
||||
default: '',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['/pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'pipelineStageId',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'assigned_to',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Campaign ID',
|
||||
name: 'campaignId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'campaignId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'End Date',
|
||||
name: 'endDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'endDate',
|
||||
preSend: [dateTimeToEpochPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Pipeline Name or ID',
|
||||
name: 'pipelineId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPipelines',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'pipeline_id',
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
hint: "Select 'Pipeline Name or ID' first to see stages",
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'pipeline_stage_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Start Date',
|
||||
name: 'startDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'date',
|
||||
preSend: [dateTimeToEpochPreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'Query will search on these fields: Name, Phone, Email, Tags, and Company Name',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'q',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Opportunity ID',
|
||||
name: 'opportunityId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
hint: "You cannot update an opportunity's pipeline ID.",
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['opportunity'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Monetary Value',
|
||||
name: 'monetaryValue',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description: 'Monetary value of lead opportunity',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'monetaryValue',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. John Deo',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Pipeline Name or ID',
|
||||
name: 'pipelineId',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getPipelines',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'pipelineId',
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Stage Name or ID',
|
||||
name: 'stageId',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
hint: "Select 'Pipeline Name or ID' first to see stages",
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['pipelineId'],
|
||||
loadOptionsMethod: 'getPipelineStages',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'pipelineStageId',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
name: 'Won',
|
||||
value: 'won',
|
||||
},
|
||||
{
|
||||
name: 'Lost',
|
||||
value: 'lost',
|
||||
},
|
||||
{
|
||||
name: 'Abandoned',
|
||||
value: 'abandoned',
|
||||
},
|
||||
],
|
||||
default: 'open',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'status',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const opportunityFields: INodeProperties[] = [
|
||||
pipelineId,
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
];
|
||||
@@ -0,0 +1,491 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
dueDatePreSendAction,
|
||||
taskPostReceiceAction,
|
||||
taskUpdatePreSendAction,
|
||||
} from '../GenericFunctions';
|
||||
|
||||
export const taskOperations: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Create a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'DELETE',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'set',
|
||||
properties: {
|
||||
value: '={{ { "success": true } }}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Get a task',
|
||||
},
|
||||
{
|
||||
name: 'Get Many',
|
||||
value: 'getAll',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/',
|
||||
},
|
||||
output: {
|
||||
postReceive: [
|
||||
{
|
||||
type: 'rootProperty',
|
||||
properties: {
|
||||
property: 'tasks',
|
||||
},
|
||||
},
|
||||
taskPostReceiceAction,
|
||||
],
|
||||
},
|
||||
},
|
||||
action: 'Get many tasks',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
routing: {
|
||||
request: {
|
||||
method: 'PUT',
|
||||
url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}/',
|
||||
},
|
||||
send: {
|
||||
preSend: [taskUpdatePreSendAction],
|
||||
},
|
||||
output: {
|
||||
postReceive: [taskPostReceiceAction],
|
||||
},
|
||||
},
|
||||
action: 'Update a task',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
const createProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Contact the task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dueDate',
|
||||
preSend: [dueDatePreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Completed',
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'completed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Body',
|
||||
name: 'body',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const deleteProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Contact the task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Contact the task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['get'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getAllProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Contact the task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['getAll'],
|
||||
returnAll: [false],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 20,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'query',
|
||||
property: 'limit',
|
||||
},
|
||||
},
|
||||
description: 'Max number of results to return',
|
||||
},
|
||||
];
|
||||
|
||||
const updateProperties: INodeProperties[] = [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Contact Email or ID',
|
||||
name: 'contactId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContacts',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description:
|
||||
'Contact the task belongs to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Assigned To',
|
||||
name: 'assignedTo',
|
||||
type: 'options',
|
||||
default: '',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'assignedTo',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Completed',
|
||||
name: 'completed',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'completed',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Body',
|
||||
name: 'body',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'body',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'dueDate',
|
||||
preSend: [dueDatePreSendAction],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
routing: {
|
||||
send: {
|
||||
type: 'body',
|
||||
property: 'title',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const taskFields: INodeProperties[] = [
|
||||
...createProperties,
|
||||
...updateProperties,
|
||||
...deleteProperties,
|
||||
...getProperties,
|
||||
...getAllProperties,
|
||||
];
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { IDataObject, IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { addCustomFieldsPreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('addCustomFieldsPreSendAction', () => {
|
||||
let mockThis: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
helpers: {
|
||||
httpRequest: jest.fn(),
|
||||
httpRequestWithAuthentication: jest.fn(),
|
||||
requestWithAuthenticationPaginated: jest.fn(),
|
||||
request: jest.fn(),
|
||||
requestWithAuthentication: jest.fn(),
|
||||
requestOAuth1: jest.fn(),
|
||||
requestOAuth2: jest.fn(),
|
||||
assertBinaryData: jest.fn(),
|
||||
getBinaryDataBuffer: jest.fn(),
|
||||
prepareBinaryData: jest.fn(),
|
||||
setBinaryDataBuffer: jest.fn(),
|
||||
copyBinaryFile: jest.fn(),
|
||||
binaryToBuffer: jest.fn(),
|
||||
binaryToString: jest.fn(),
|
||||
getBinaryPath: jest.fn(),
|
||||
getBinaryStream: jest.fn(),
|
||||
getBinaryMetadata: jest.fn(),
|
||||
createDeferredPromise: jest
|
||||
.fn()
|
||||
.mockReturnValue({ promise: Promise.resolve(), resolve: jest.fn(), reject: jest.fn() }),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should format custom fields correctly when provided', async () => {
|
||||
const mockRequestOptions: IHttpRequestOptions = {
|
||||
body: {
|
||||
customFields: {
|
||||
values: [
|
||||
{
|
||||
fieldId: { value: '123', cachedResultName: 'FieldName' },
|
||||
fieldValue: 'TestValue',
|
||||
},
|
||||
{
|
||||
fieldId: { value: '456' },
|
||||
fieldValue: 'AnotherValue',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as IDataObject,
|
||||
url: '',
|
||||
};
|
||||
|
||||
const result = await addCustomFieldsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
mockRequestOptions,
|
||||
);
|
||||
|
||||
expect((result.body as IDataObject).customFields).toEqual([
|
||||
{ id: '123', key: 'FieldName', field_value: 'TestValue' },
|
||||
{ id: '456', key: 'default_key', field_value: 'AnotherValue' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not modify request body if customFields is not provided', async () => {
|
||||
const mockRequestOptions: IHttpRequestOptions = {
|
||||
body: {
|
||||
otherField: 'SomeValue',
|
||||
} as IDataObject,
|
||||
url: '',
|
||||
};
|
||||
|
||||
const result = await addCustomFieldsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
mockRequestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(mockRequestOptions);
|
||||
});
|
||||
|
||||
it('should handle customFields with empty values', async () => {
|
||||
const mockRequestOptions: IHttpRequestOptions = {
|
||||
body: {
|
||||
customFields: {
|
||||
values: [],
|
||||
},
|
||||
} as IDataObject,
|
||||
url: '',
|
||||
};
|
||||
|
||||
const result = await addCustomFieldsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
mockRequestOptions,
|
||||
);
|
||||
|
||||
expect((result.body as IDataObject).customFields).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { addLocationIdPreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('addLocationIdPreSendAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNodeParameter: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should add locationId to query parameters for contact getAll operation', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock)
|
||||
.mockReturnValueOnce('contact')
|
||||
.mockReturnValueOnce('getAll');
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue({
|
||||
oauthTokenData: { locationId: '123' },
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {},
|
||||
};
|
||||
|
||||
const result = await addLocationIdPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.qs).toEqual({ locationId: '123' });
|
||||
});
|
||||
|
||||
it('should add locationId to the body for contact create operation', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock)
|
||||
.mockReturnValueOnce('contact')
|
||||
.mockReturnValueOnce('create');
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue({
|
||||
oauthTokenData: { locationId: '123' },
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await addLocationIdPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ locationId: '123' });
|
||||
});
|
||||
|
||||
it('should add locationId to query parameters for opportunity getAll operation', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock)
|
||||
.mockReturnValueOnce('opportunity')
|
||||
.mockReturnValueOnce('getAll');
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue({
|
||||
oauthTokenData: { locationId: '123' },
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {},
|
||||
};
|
||||
|
||||
const result = await addLocationIdPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.qs).toEqual({ location_id: '123' });
|
||||
});
|
||||
|
||||
it('should add locationId to the body for opportunity create operation', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock)
|
||||
.mockReturnValueOnce('opportunity')
|
||||
.mockReturnValueOnce('create');
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue({
|
||||
oauthTokenData: { locationId: '123' },
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await addLocationIdPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ locationId: '123' });
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if no resource or operation matches', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock)
|
||||
.mockReturnValueOnce('unknown')
|
||||
.mockReturnValueOnce('unknown');
|
||||
|
||||
(mockThis.getCredentials as jest.Mock).mockResolvedValue({
|
||||
oauthTokenData: { locationId: '123' },
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
qs: {},
|
||||
};
|
||||
|
||||
const result = await addLocationIdPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { highLevelApiRequest } from '../GenericFunctions';
|
||||
|
||||
describe('GenericFunctions - highLevelApiRequest', () => {
|
||||
let mockContext: any;
|
||||
let mockHttpRequestWithAuthentication: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHttpRequestWithAuthentication = jest.fn();
|
||||
mockContext = {
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test('should make a successful request with all parameters', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'POST';
|
||||
const resource = '/example-resource';
|
||||
const body = { key: 'value' };
|
||||
const qs = { query: 'test' };
|
||||
const url = 'https://custom-url.example.com/api';
|
||||
const option = { headers: { Authorization: 'Bearer test-token' } };
|
||||
|
||||
const result = await highLevelApiRequest.call(
|
||||
mockContext,
|
||||
method,
|
||||
resource,
|
||||
body,
|
||||
qs,
|
||||
url,
|
||||
option,
|
||||
);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: { Authorization: 'Bearer test-token' },
|
||||
method: 'POST',
|
||||
body: { key: 'value' },
|
||||
qs: { query: 'test' },
|
||||
url: 'https://custom-url.example.com/api',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should default to the base URL when no custom URL is provided', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'GET';
|
||||
const resource = '/default-resource';
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'GET',
|
||||
url: 'https://services.leadconnectorhq.com/default-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should remove the body property if it is empty', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'DELETE';
|
||||
const resource = '/example-resource';
|
||||
const body = {};
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource, body);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'DELETE',
|
||||
url: 'https://services.leadconnectorhq.com/example-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should remove the query string property if it is empty', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'PATCH';
|
||||
const resource = '/example-resource';
|
||||
const qs = {};
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource, {}, qs);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'PATCH',
|
||||
url: 'https://services.leadconnectorhq.com/example-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions, INode } from 'n8n-workflow';
|
||||
|
||||
import { contactIdentifierPreSendAction, isEmailValid, isPhoneValid } from '../GenericFunctions';
|
||||
|
||||
jest.mock('../GenericFunctions', () => ({
|
||||
...jest.requireActual('../GenericFunctions'),
|
||||
isEmailValid: jest.fn(),
|
||||
isPhoneValid: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('contactIdentifierPreSendAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNode: jest.fn(
|
||||
() =>
|
||||
({
|
||||
id: 'mock-node-id',
|
||||
name: 'mock-node',
|
||||
typeVersion: 1,
|
||||
type: 'n8n-nodes-base.mockNode',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
}) as INode,
|
||||
),
|
||||
getNodeParameter: jest.fn((parameterName: string) => {
|
||||
if (parameterName === 'contactIdentifier') return null;
|
||||
if (parameterName === 'updateFields') return { contactIdentifier: 'default-identifier' };
|
||||
return undefined;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it('should add email to requestOptions.body if identifier is a valid email', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(true);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(false);
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValue('valid@example.com'); // Mock email
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await contactIdentifierPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ email: 'valid@example.com' });
|
||||
});
|
||||
|
||||
it('should add phone to requestOptions.body if identifier is a valid phone', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(false);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(true);
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValue('1234567890'); // Mock phone
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await contactIdentifierPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ phone: '1234567890' });
|
||||
});
|
||||
|
||||
it('should add contactId to requestOptions.body if identifier is neither email nor phone', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(false);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(false);
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValue('contact-id-123'); // Mock contactId
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await contactIdentifierPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ contactId: 'contact-id-123' });
|
||||
});
|
||||
|
||||
it('should use updateFields.contactIdentifier if contactIdentifier is not provided', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(true);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(false);
|
||||
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((parameterName: string) => {
|
||||
if (parameterName === 'contactIdentifier') return null;
|
||||
if (parameterName === 'updateFields')
|
||||
return { contactIdentifier: 'default-email@example.com' };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await contactIdentifierPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ email: 'default-email@example.com' });
|
||||
});
|
||||
|
||||
it('should initialize body as an empty object if it is undefined', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(false);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(false);
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValue('identifier-123');
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: undefined,
|
||||
};
|
||||
|
||||
const result = await contactIdentifierPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({ contactId: 'identifier-123' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { dateTimeToEpochPreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('dateTimeToEpochPreSendAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {};
|
||||
});
|
||||
|
||||
it('should convert startDate and endDate to epoch time', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {
|
||||
startDate: '2024-12-25T00:00:00Z',
|
||||
endDate: '2024-12-26T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await dateTimeToEpochPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.qs).toEqual({
|
||||
startDate: new Date('2024-12-25T00:00:00Z').getTime(),
|
||||
endDate: new Date('2024-12-26T00:00:00Z').getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert only startDate if endDate is not provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {
|
||||
startDate: '2024-12-25T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await dateTimeToEpochPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.qs).toEqual({
|
||||
startDate: new Date('2024-12-25T00:00:00Z').getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert only endDate if startDate is not provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {
|
||||
endDate: '2024-12-26T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await dateTimeToEpochPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.qs).toEqual({
|
||||
endDate: new Date('2024-12-26T00:00:00Z').getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if neither startDate nor endDate are provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: {},
|
||||
};
|
||||
|
||||
const result = await dateTimeToEpochPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if qs is undefined', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
qs: undefined,
|
||||
};
|
||||
|
||||
const result = await dateTimeToEpochPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { dateToIsoSupressMillis } from '../GenericFunctions';
|
||||
|
||||
describe('dateToIsoSupressMillis', () => {
|
||||
it('should return an ISO string without milliseconds (UTC)', () => {
|
||||
const dateTime = '2024-12-25T10:15:30.123Z';
|
||||
const result = dateToIsoSupressMillis(dateTime);
|
||||
expect(result).toBe('2024-12-25T10:15:30.123+00:00');
|
||||
});
|
||||
|
||||
it('should handle dates without milliseconds correctly', () => {
|
||||
const dateTime = '2024-12-25T10:15:30Z';
|
||||
const result = dateToIsoSupressMillis(dateTime);
|
||||
expect(result).toBe('2024-12-25T10:15:30+00:00');
|
||||
});
|
||||
|
||||
it('should handle time zone offsets correctly', () => {
|
||||
const dateTime = '2024-12-25T10:15:30.123+02:00';
|
||||
const result = dateToIsoSupressMillis(dateTime);
|
||||
expect(result).toBe('2024-12-25T08:15:30.123+00:00');
|
||||
});
|
||||
|
||||
it('should handle edge case for empty input', () => {
|
||||
const dateTime = '';
|
||||
const result = dateToIsoSupressMillis(dateTime);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle edge case for null input', () => {
|
||||
const dateTime = null as unknown as string;
|
||||
const result = dateToIsoSupressMillis(dateTime);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions, INode } from 'n8n-workflow';
|
||||
|
||||
import { dueDatePreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('dueDatePreSendAction', () => {
|
||||
let mockThis: IExecuteSingleFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNode: jest.fn(
|
||||
() =>
|
||||
({
|
||||
id: 'mock-node-id',
|
||||
name: 'mock-node',
|
||||
typeVersion: 1,
|
||||
type: 'n8n-nodes-base.mockNode',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
}) as INode,
|
||||
),
|
||||
getNodeParameter: jest.fn(),
|
||||
getInputData: jest.fn(),
|
||||
helpers: {} as any,
|
||||
} as unknown as IExecuteSingleFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should add formatted dueDate to requestOptions.body if dueDate is provided directly', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValueOnce('2024-12-25');
|
||||
|
||||
const requestOptions: IHttpRequestOptions = { url: 'https://example.com/api' };
|
||||
|
||||
const result = await dueDatePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result.body).toEqual({ dueDate: '2024-12-25T00:00:00+00:00' });
|
||||
});
|
||||
|
||||
it('should add formatted dueDate to requestOptions.body if dueDate is provided in updateFields', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockImplementation((paramName: string) => {
|
||||
if (paramName === 'dueDate') return null;
|
||||
if (paramName === 'updateFields') return { dueDate: '2024-12-25' };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const requestOptions: IHttpRequestOptions = { url: 'https://example.com/api' };
|
||||
|
||||
const result = await dueDatePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result.body).toEqual({ dueDate: '2024-12-25T00:00:00+00:00' });
|
||||
});
|
||||
|
||||
it('should initialize body as an empty object if it is undefined', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValueOnce('2024-12-25');
|
||||
|
||||
const requestOptions: IHttpRequestOptions = { url: 'https://example.com/api', body: undefined };
|
||||
|
||||
const result = await dueDatePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result.body).toEqual({ dueDate: '2024-12-25T00:00:00+00:00' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getContacts } from '../GenericFunctions';
|
||||
|
||||
describe('getContacts', () => {
|
||||
const mockHighLevelApiRequest = jest.fn();
|
||||
const mockGetCredentials = jest.fn();
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHighLevelApiRequest,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHighLevelApiRequest.mockClear();
|
||||
mockGetCredentials.mockClear();
|
||||
});
|
||||
|
||||
it('should return a list of contacts', async () => {
|
||||
const mockContacts = [
|
||||
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
||||
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
||||
];
|
||||
|
||||
mockHighLevelApiRequest.mockResolvedValue({ contacts: mockContacts });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getContacts.call(mockContext);
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'alice@example.com', value: '1' },
|
||||
{ name: 'bob@example.com', value: '2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty contacts list', async () => {
|
||||
mockHighLevelApiRequest.mockResolvedValue({ contacts: [] });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getContacts.call(mockContext);
|
||||
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { getPipelineStages } from '../GenericFunctions';
|
||||
|
||||
const mockHighLevelApiRequest = jest.fn();
|
||||
const mockGetNodeParameter = jest.fn();
|
||||
const mockGetCurrentNodeParameter = jest.fn();
|
||||
const mockGetCredentials = jest.fn();
|
||||
|
||||
const mockContext: any = {
|
||||
getNodeParameter: mockGetNodeParameter,
|
||||
getCurrentNodeParameter: mockGetCurrentNodeParameter,
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHighLevelApiRequest,
|
||||
},
|
||||
};
|
||||
|
||||
describe('getPipelineStages', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return pipeline stages for create operation', async () => {
|
||||
mockGetNodeParameter.mockReturnValue('create');
|
||||
mockGetCurrentNodeParameter.mockReturnValue('pipeline-1');
|
||||
mockHighLevelApiRequest.mockResolvedValue({
|
||||
pipelines: [
|
||||
{
|
||||
id: 'pipeline-1',
|
||||
stages: [
|
||||
{ id: 'stage-1', name: 'Stage 1' },
|
||||
{ id: 'stage-2', name: 'Stage 2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await getPipelineStages.call(mockContext);
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'Stage 1', value: 'stage-1' },
|
||||
{ name: 'Stage 2', value: 'stage-2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return pipeline stages for update operation', async () => {
|
||||
mockGetNodeParameter.mockImplementation((param) => {
|
||||
if (param === 'operation') return 'update';
|
||||
if (param === 'updateFields.pipelineId') return 'pipeline-2';
|
||||
});
|
||||
|
||||
mockHighLevelApiRequest.mockResolvedValue({
|
||||
pipelines: [
|
||||
{
|
||||
id: 'pipeline-2',
|
||||
stages: [
|
||||
{ id: 'stage-3', name: 'Stage 3' },
|
||||
{ id: 'stage-4', name: 'Stage 4' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await getPipelineStages.call(mockContext);
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'Stage 3', value: 'stage-3' },
|
||||
{ name: 'Stage 4', value: 'stage-4' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if pipeline is not found', async () => {
|
||||
mockGetNodeParameter.mockReturnValue('create');
|
||||
mockGetCurrentNodeParameter.mockReturnValue('non-existent-pipeline');
|
||||
mockHighLevelApiRequest.mockResolvedValue({
|
||||
pipelines: [
|
||||
{
|
||||
id: 'pipeline-1',
|
||||
stages: [{ id: 'stage-1', name: 'Stage 1' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await getPipelineStages.call(mockContext);
|
||||
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getPipelines } from '../GenericFunctions';
|
||||
|
||||
describe('getPipelines', () => {
|
||||
const mockHighLevelApiRequest = jest.fn();
|
||||
const mockGetCredentials = jest.fn();
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHighLevelApiRequest,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHighLevelApiRequest.mockClear();
|
||||
mockGetCredentials.mockClear();
|
||||
});
|
||||
|
||||
it('should return a list of pipelines', async () => {
|
||||
const mockPipelines = [
|
||||
{ id: '1', name: 'Pipeline A' },
|
||||
{ id: '2', name: 'Pipeline B' },
|
||||
];
|
||||
|
||||
mockHighLevelApiRequest.mockResolvedValue({ pipelines: mockPipelines });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getPipelines.call(mockContext);
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'Pipeline A', value: '1' },
|
||||
{ name: 'Pipeline B', value: '2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty pipelines list', async () => {
|
||||
mockHighLevelApiRequest.mockResolvedValue({ pipelines: [] });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getPipelines.call(mockContext);
|
||||
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getUsers } from '../GenericFunctions';
|
||||
|
||||
describe('getUsers', () => {
|
||||
const mockHighLevelApiRequest = jest.fn();
|
||||
const mockGetCredentials = jest.fn();
|
||||
const mockContext = {
|
||||
getCredentials: mockGetCredentials,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHighLevelApiRequest,
|
||||
},
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHighLevelApiRequest.mockClear();
|
||||
mockGetCredentials.mockClear();
|
||||
});
|
||||
|
||||
it('should return a list of users', async () => {
|
||||
const mockUsers = [
|
||||
{ id: '1', name: 'John Doe', email: 'john.doe@example.com' },
|
||||
{ id: '2', name: 'Jane Smith', email: 'jane.smith@example.com' },
|
||||
];
|
||||
|
||||
mockHighLevelApiRequest.mockResolvedValue({ users: mockUsers });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getUsers.call(mockContext);
|
||||
|
||||
expect(response).toEqual([
|
||||
{ name: 'John Doe', value: '1' },
|
||||
{ name: 'Jane Smith', value: '2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty users list', async () => {
|
||||
mockHighLevelApiRequest.mockResolvedValue({ users: [] });
|
||||
mockGetCredentials.mockResolvedValue({ oauthTokenData: { locationId: '123' } });
|
||||
|
||||
const response = await getUsers.call(mockContext);
|
||||
|
||||
expect(response).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { IExecutePaginationFunctions } from 'n8n-workflow';
|
||||
|
||||
import { highLevelApiPagination } from '../GenericFunctions';
|
||||
|
||||
describe('highLevelApiPagination', () => {
|
||||
let mockContext: Partial<IExecutePaginationFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = {
|
||||
getNodeParameter: jest.fn(),
|
||||
makeRoutingRequest: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should paginate and return all items when returnAll is true', async () => {
|
||||
(mockContext.getNodeParameter as jest.Mock).mockImplementation((parameter) => {
|
||||
if (parameter === 'resource') return 'contact';
|
||||
if (parameter === 'returnAll') return true;
|
||||
});
|
||||
|
||||
(mockContext.makeRoutingRequest as jest.Mock)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
json: {
|
||||
contacts: [{ id: '1' }, { id: '2' }],
|
||||
meta: { startAfterId: '2', startAfter: 2, total: 4 },
|
||||
},
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
json: {
|
||||
contacts: [{ id: '3' }, { id: '4' }],
|
||||
meta: { startAfterId: null, startAfter: null, total: 4 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const requestData = { options: { qs: {} } } as any;
|
||||
|
||||
const result = await highLevelApiPagination.call(
|
||||
mockContext as IExecutePaginationFunctions,
|
||||
requestData,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ json: { id: '1' } },
|
||||
{ json: { id: '2' } },
|
||||
{ json: { id: '3' } },
|
||||
{ json: { id: '4' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return only the first page of items when returnAll is false', async () => {
|
||||
(mockContext.getNodeParameter as jest.Mock).mockImplementation((parameter) => {
|
||||
if (parameter === 'resource') return 'contact';
|
||||
if (parameter === 'returnAll') return false;
|
||||
});
|
||||
|
||||
(mockContext.makeRoutingRequest as jest.Mock).mockResolvedValueOnce([
|
||||
{
|
||||
json: {
|
||||
contacts: [{ id: '1' }, { id: '2' }],
|
||||
meta: { startAfterId: '2', startAfter: 2, total: 4 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const requestData = { options: { qs: {} } } as any;
|
||||
|
||||
const result = await highLevelApiPagination.call(
|
||||
mockContext as IExecutePaginationFunctions,
|
||||
requestData,
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ json: { id: '1' } }, { json: { id: '2' } }]);
|
||||
});
|
||||
|
||||
it('should handle cases with no items', async () => {
|
||||
(mockContext.getNodeParameter as jest.Mock).mockImplementation((parameter) => {
|
||||
if (parameter === 'resource') return 'contact';
|
||||
if (parameter === 'returnAll') return true;
|
||||
});
|
||||
|
||||
(mockContext.makeRoutingRequest as jest.Mock).mockResolvedValueOnce([
|
||||
{
|
||||
json: {
|
||||
contacts: [],
|
||||
meta: { startAfterId: null, startAfter: null, total: 0 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const requestData = { options: { qs: {} } } as any;
|
||||
|
||||
const result = await highLevelApiPagination.call(
|
||||
mockContext as IExecutePaginationFunctions,
|
||||
requestData,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { highLevelApiRequest } from '../GenericFunctions';
|
||||
|
||||
describe('GenericFunctions - highLevelApiRequest', () => {
|
||||
let mockContext: any;
|
||||
let mockHttpRequestWithAuthentication: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockHttpRequestWithAuthentication = jest.fn();
|
||||
mockContext = {
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test('should make a successful request with all parameters', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'POST';
|
||||
const resource = '/example-resource';
|
||||
const body = { key: 'value' };
|
||||
const qs = { query: 'test' };
|
||||
const url = 'https://custom-url.example.com/api';
|
||||
const option = { headers: { Authorization: 'Bearer test-token' } };
|
||||
|
||||
const result = await highLevelApiRequest.call(
|
||||
mockContext,
|
||||
method,
|
||||
resource,
|
||||
body,
|
||||
qs,
|
||||
url,
|
||||
option,
|
||||
);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: { Authorization: 'Bearer test-token' },
|
||||
method: 'POST',
|
||||
body: { key: 'value' },
|
||||
qs: { query: 'test' },
|
||||
url: 'https://custom-url.example.com/api',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should default to the base URL when no custom URL is provided', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'GET';
|
||||
const resource = '/default-resource';
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'GET',
|
||||
url: 'https://services.leadconnectorhq.com/default-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should remove the body property if it is empty', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'DELETE';
|
||||
const resource = '/example-resource';
|
||||
const body = {};
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource, body);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'DELETE',
|
||||
url: 'https://services.leadconnectorhq.com/example-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should remove the query string property if it is empty', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockHttpRequestWithAuthentication.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const method = 'PATCH';
|
||||
const resource = '/example-resource';
|
||||
const qs = {};
|
||||
|
||||
const result = await highLevelApiRequest.call(mockContext, method, resource, {}, qs);
|
||||
|
||||
expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('highLevelOAuth2Api', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Version: '2021-07-28',
|
||||
},
|
||||
method: 'PATCH',
|
||||
url: 'https://services.leadconnectorhq.com/example-resource',
|
||||
json: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { isEmailValid } from '../GenericFunctions';
|
||||
|
||||
describe('isEmailValid', () => {
|
||||
it('should return true for a valid email address', () => {
|
||||
const email = 'test@example.com';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid email address', () => {
|
||||
const email = 'invalid-email';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for an email address with subdomain', () => {
|
||||
const email = 'user@sub.example.com';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an email address without a domain', () => {
|
||||
const email = 'user@';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an email address without a username', () => {
|
||||
const email = '@example.com';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for an email address with a plus sign', () => {
|
||||
const email = 'user+alias@example.com';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an email address with invalid characters', () => {
|
||||
const email = 'user@exa$mple.com';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an email address without a top-level domain', () => {
|
||||
const email = 'user@example';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for an email address with a valid top-level domain', () => {
|
||||
const email = 'user@example.co.uk';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an empty email string', () => {
|
||||
const email = '';
|
||||
const result = isEmailValid(email);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { isPhoneValid } from '../GenericFunctions';
|
||||
|
||||
describe('isPhoneValid', () => {
|
||||
it('should return true for a valid phone number', () => {
|
||||
const phone = '+1234567890';
|
||||
const result = isPhoneValid(phone);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an invalid phone number', () => {
|
||||
const phone = 'invalid-phone';
|
||||
const result = isPhoneValid(phone);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a phone number with invalid characters', () => {
|
||||
const phone = '+123-abc-456';
|
||||
const result = isPhoneValid(phone);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an empty phone number', () => {
|
||||
const phone = '';
|
||||
const result = isPhoneValid(phone);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a phone number with only special characters', () => {
|
||||
const phone = '!!!@@@###';
|
||||
const result = isPhoneValid(phone);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { splitTagsPreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('splitTagsPreSendAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {};
|
||||
});
|
||||
|
||||
it('should return requestOptions unchanged if tags are already an array', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await splitTagsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should split a comma-separated string of tags into an array', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {
|
||||
tags: 'tag1, tag2, tag3',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await splitTagsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should trim whitespace around tags when splitting a string', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {
|
||||
tags: 'tag1 , tag2 , tag3 ',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await splitTagsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return requestOptions unchanged if tags are not provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await splitTagsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should return requestOptions unchanged if body is undefined', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: undefined,
|
||||
};
|
||||
|
||||
const result = await splitTagsPreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
IExecuteSingleFunctions,
|
||||
INodeExecutionData,
|
||||
IN8nHttpFullResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { taskPostReceiceAction } from '../GenericFunctions';
|
||||
|
||||
describe('taskPostReceiceAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNodeParameter: jest.fn((parameterName: string) => {
|
||||
if (parameterName === 'contactId') return '12345';
|
||||
return undefined;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it('should add contactId to each item in items', async () => {
|
||||
const items: INodeExecutionData[] = [
|
||||
{ json: { field1: 'value1' } },
|
||||
{ json: { field2: 'value2' } },
|
||||
];
|
||||
|
||||
const response: IN8nHttpFullResponse = {
|
||||
body: {},
|
||||
headers: {},
|
||||
statusCode: 200,
|
||||
};
|
||||
|
||||
const result = await taskPostReceiceAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
items,
|
||||
response,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ json: { field1: 'value1', contactId: '12345' } },
|
||||
{ json: { field2: 'value2', contactId: '12345' } },
|
||||
]);
|
||||
expect(mockThis.getNodeParameter).toHaveBeenCalledWith('contactId');
|
||||
});
|
||||
|
||||
it('should not modify other fields in items', async () => {
|
||||
const items: INodeExecutionData[] = [{ json: { name: 'John Doe' } }, { json: { age: 30 } }];
|
||||
|
||||
const response: IN8nHttpFullResponse = {
|
||||
body: {},
|
||||
headers: {},
|
||||
statusCode: 200,
|
||||
};
|
||||
|
||||
const result = await taskPostReceiceAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
items,
|
||||
response,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ json: { name: 'John Doe', contactId: '12345' } },
|
||||
{ json: { age: 30, contactId: '12345' } },
|
||||
]);
|
||||
expect(mockThis.getNodeParameter).toHaveBeenCalledWith('contactId');
|
||||
});
|
||||
|
||||
it('should return an empty array if items is empty', async () => {
|
||||
const items: INodeExecutionData[] = [];
|
||||
|
||||
const response: IN8nHttpFullResponse = {
|
||||
body: {},
|
||||
headers: {},
|
||||
statusCode: 200,
|
||||
};
|
||||
|
||||
const result = await taskPostReceiceAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
items,
|
||||
response,
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockThis.getNodeParameter).toHaveBeenCalledWith('contactId');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
|
||||
|
||||
import { taskUpdatePreSendAction } from '../GenericFunctions';
|
||||
|
||||
describe('taskUpdatePreSendAction', () => {
|
||||
let mockThis: Partial<IExecuteSingleFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNodeParameter: jest.fn(),
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: jest.fn(),
|
||||
} as any,
|
||||
};
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if title and dueDate are provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://api.example.com',
|
||||
body: {
|
||||
title: 'Task Title',
|
||||
dueDate: '2024-12-25T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await taskUpdatePreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should fetch missing title and dueDate from the API', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValueOnce('123').mockReturnValueOnce('456');
|
||||
|
||||
const mockApiResponse = {
|
||||
title: 'Fetched Task Title',
|
||||
dueDate: '2024-12-25T02:00:00+02:00',
|
||||
};
|
||||
|
||||
(mockThis.helpers?.httpRequestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://api.example.com',
|
||||
body: {
|
||||
title: undefined,
|
||||
dueDate: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await taskUpdatePreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
title: 'Fetched Task Title',
|
||||
dueDate: '2024-12-25T00:00:00+00:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('should only fetch title if dueDate is provided', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValueOnce('123').mockReturnValueOnce('456');
|
||||
|
||||
const mockApiResponse = {
|
||||
title: 'Fetched Task Title',
|
||||
dueDate: '2024-12-25T02:00:00+02:00',
|
||||
};
|
||||
|
||||
(mockThis.helpers?.httpRequestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://api.example.com',
|
||||
body: {
|
||||
title: undefined,
|
||||
dueDate: '2024-12-24T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await taskUpdatePreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
title: 'Fetched Task Title',
|
||||
dueDate: '2024-12-24T00:00:00Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('should only fetch dueDate if title is provided', async () => {
|
||||
(mockThis.getNodeParameter as jest.Mock).mockReturnValueOnce('123').mockReturnValueOnce('456');
|
||||
|
||||
const mockApiResponse = {
|
||||
title: 'Fetched Task Title',
|
||||
dueDate: '2024-12-25T02:00:00+02:00',
|
||||
};
|
||||
|
||||
(mockThis.helpers?.httpRequestWithAuthentication as jest.Mock).mockResolvedValue(
|
||||
mockApiResponse,
|
||||
);
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://api.example.com',
|
||||
body: {
|
||||
title: 'Existing Task Title',
|
||||
dueDate: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await taskUpdatePreSendAction.call(
|
||||
mockThis as IExecuteSingleFunctions,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
expect(result.body).toEqual({
|
||||
title: 'Existing Task Title',
|
||||
dueDate: '2024-12-25T00:00:00+00:00',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { IExecuteSingleFunctions, IHttpRequestOptions, INode } from 'n8n-workflow';
|
||||
|
||||
import { validEmailAndPhonePreSendAction, isEmailValid, isPhoneValid } from '../GenericFunctions';
|
||||
|
||||
jest.mock('../GenericFunctions', () => ({
|
||||
...jest.requireActual('../GenericFunctions'),
|
||||
isEmailValid: jest.fn(),
|
||||
isPhoneValid: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('validEmailAndPhonePreSendAction', () => {
|
||||
let mockThis: IExecuteSingleFunctions;
|
||||
|
||||
beforeEach(() => {
|
||||
mockThis = {
|
||||
getNode: jest.fn(
|
||||
() =>
|
||||
({
|
||||
id: 'mock-node-id',
|
||||
name: 'mock-node',
|
||||
typeVersion: 1,
|
||||
type: 'n8n-nodes-base.mockNode',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
}) as INode,
|
||||
),
|
||||
} as unknown as IExecuteSingleFunctions;
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return requestOptions unchanged if email and phone are valid', async () => {
|
||||
(isEmailValid as jest.Mock).mockReturnValue(true);
|
||||
(isPhoneValid as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {
|
||||
email: 'valid@example.com',
|
||||
phone: '+1234567890',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await validEmailAndPhonePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if no email or phone is provided', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: {},
|
||||
};
|
||||
|
||||
const result = await validEmailAndPhonePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
|
||||
it('should not modify requestOptions if body is undefined', async () => {
|
||||
const requestOptions: IHttpRequestOptions = {
|
||||
url: 'https://example.com/api',
|
||||
body: undefined,
|
||||
};
|
||||
|
||||
const result = await validEmailAndPhonePreSendAction.call(mockThis, requestOptions);
|
||||
|
||||
expect(result).toEqual(requestOptions);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user