From 238a78f0582dbf439a9799de0edcb2e9bef29978 Mon Sep 17 00:00:00 2001 From: perseus-algol <129220578+perseus-algol@users.noreply.github.com> Date: Mon, 3 Jul 2023 10:31:06 +0300 Subject: [PATCH] feat: Add crowd.dev node and trigger node (#6082) --- .../credentials/CrowdDevApi.credentials.ts | 72 +++++ .../nodes/CrowdDev/CrowdDev.node.json | 18 ++ .../nodes/CrowdDev/CrowdDev.node.ts | 32 ++ .../nodes/CrowdDev/CrowdDevTrigger.node.json | 18 ++ .../nodes/CrowdDev/CrowdDevTrigger.node.ts | 185 ++++++++++++ .../nodes/CrowdDev/GenericFunctions.ts | 221 ++++++++++++++ .../nodes-base/nodes/CrowdDev/crowdDev.svg | 15 + .../CrowdDev/descriptions/activityFields.ts | 186 ++++++++++++ .../CrowdDev/descriptions/automationFields.ts | 129 ++++++++ .../nodes/CrowdDev/descriptions/index.ts | 24 ++ .../CrowdDev/descriptions/memberFields.ts | 275 ++++++++++++++++++ .../nodes/CrowdDev/descriptions/noteFields.ts | 92 ++++++ .../descriptions/organizationFields.ts | 150 ++++++++++ .../nodes/CrowdDev/descriptions/resources.ts | 36 +++ .../nodes/CrowdDev/descriptions/shared.ts | 27 ++ .../nodes/CrowdDev/descriptions/taskFields.ts | 163 +++++++++++ .../nodes/CrowdDev/descriptions/utils.ts | 57 ++++ packages/nodes-base/package.json | 3 + 18 files changed, 1703 insertions(+) create mode 100644 packages/nodes-base/credentials/CrowdDevApi.credentials.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/CrowdDev.node.json create mode 100644 packages/nodes-base/nodes/CrowdDev/CrowdDev.node.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.json create mode 100644 packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/crowdDev.svg create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/activityFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/automationFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/memberFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/noteFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/organizationFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/resources.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/shared.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/taskFields.ts create mode 100644 packages/nodes-base/nodes/CrowdDev/descriptions/utils.ts diff --git a/packages/nodes-base/credentials/CrowdDevApi.credentials.ts b/packages/nodes-base/credentials/CrowdDevApi.credentials.ts new file mode 100644 index 0000000000000..a6ee085a02683 --- /dev/null +++ b/packages/nodes-base/credentials/CrowdDevApi.credentials.ts @@ -0,0 +1,72 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class CrowdDevApi implements ICredentialType { + name = 'crowdDevApi'; + + displayName = 'crowd.dev API'; + + documentationUrl = 'crowdDev'; + + properties: INodeProperties[] = [ + { + displayName: 'URL', + name: 'url', + type: 'string', + default: 'https://app.crowd.dev', + }, + { + displayName: 'Tenant ID', + name: 'tenantId', + type: 'string', + default: '', + }, + { + displayName: 'Token', + name: 'token', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + description: 'Whether to connect even if SSL certificate validation is not possible', + default: false, + }, + ]; + + // This allows the credential to be used by other parts of n8n + // stating how this credential is injected as part of the request + // An example is the Http Request node that can make generic calls + // reusing this credential + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '={{"Bearer " + $credentials.token}}', + }, + }, + }; + + // The block below tells how this credential can be tested + test: ICredentialTestRequest = { + request: { + method: 'POST', + baseURL: '={{$credentials.url.replace(/\\/$/, "") + "/api/tenant/" + $credentials.tenantId}}', + url: '/member/query', + skipSslCertificateValidation: '={{ $credentials.allowUnauthorizedCerts }}', + body: { + limit: 1, + offset: 0, + }, + }, + }; +} diff --git a/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.json b/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.json new file mode 100644 index 0000000000000..8c03ebe23e9ce --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.crowdDev", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Productivity"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/crowdDev" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.crowdDev/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.ts b/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.ts new file mode 100644 index 0000000000000..76cd06d60a569 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/CrowdDev.node.ts @@ -0,0 +1,32 @@ +import type { INodeType, INodeTypeDescription } from 'n8n-workflow'; +import { allProperties } from './descriptions'; + +export class CrowdDev implements INodeType { + description: INodeTypeDescription = { + displayName: 'crowd.dev', + name: 'crowdDev', + icon: 'file:crowdDev.svg', + group: ['transform'], + version: 1, + subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}', + description: + 'crowd.dev is an open-source suite of community and data tools built to unlock community-led growth for your organization.', + defaults: { + name: 'crowd.dev', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'crowdDevApi', + required: true, + }, + ], + requestDefaults: { + baseURL: '={{$credentials.url}}/api/tenant/{{$credentials.tenantId}}', + json: true, + skipSslCertificateValidation: '={{ $credentials.allowUnauthorizedCerts }}', + }, + properties: allProperties, + }; +} diff --git a/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.json b/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.json new file mode 100644 index 0000000000000..2e37220b0c6ce --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.crowdDevTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Productivity"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/crowdDev" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.crowddevtrigger/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.ts b/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.ts new file mode 100644 index 0000000000000..cf30224b4e55f --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/CrowdDevTrigger.node.ts @@ -0,0 +1,185 @@ +import type { + IHookFunctions, + IWebhookFunctions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, + IHttpRequestOptions, +} from 'n8n-workflow'; + +interface ICrowdDevCreds { + url: string; + tenantId: string; + token: string; + allowUnauthorizedCerts: boolean; +} + +const credsName = 'crowdDevApi'; + +const getCreds = async (hookFns: IHookFunctions) => + hookFns.getCredentials(credsName) as unknown as ICrowdDevCreds; + +const createRequest = ( + creds: ICrowdDevCreds, + opts: Partial, +): IHttpRequestOptions => { + const defaults: IHttpRequestOptions = { + baseURL: `${creds.url}/api/tenant/${creds.tenantId}`, + url: '', + json: true, + skipSslCertificateValidation: creds.allowUnauthorizedCerts, + }; + return Object.assign(defaults, opts); +}; + +export class CrowdDevTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'crowd.dev Trigger', + name: 'crowdDevTrigger', + icon: 'file:crowdDev.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when crowd.dev events occur.', + defaults: { + name: 'crowd.dev Trigger', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'crowdDevApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Trigger', + name: 'trigger', + description: 'What will trigger an automation', + type: 'options', + required: true, + default: 'new_activity', + options: [ + { + name: 'New Activity', + value: 'new_activity', + }, + { + name: 'New Member', + value: 'new_member', + }, + ], + }, + ], + }; + + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const creds = await getCreds(this); + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + + if (webhookData.webhookId !== undefined) { + try { + const options = createRequest(creds, { + url: `/automation/${webhookData.webhookId}`, + method: 'GET', + }); + const data = await this.helpers.httpRequestWithAuthentication.call( + this, + credsName, + options, + ); + if (data.settings.url === webhookUrl) { + return true; + } + } catch (error) { + return false; + } + } + + // If it did not error then the webhook exists + return false; + }, + + async create(this: IHookFunctions): Promise { + const creds = await getCreds(this); + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; + const params = { + trigger: this.getNodeParameter('trigger') as string, + }; + + const options = createRequest(creds, { + url: '/automation', + method: 'POST', + body: { + data: { + settings: { + url: webhookUrl, + }, + type: 'webhook', + trigger: params.trigger, + }, + }, + }); + + const responseData = await this.helpers.httpRequestWithAuthentication.call( + this, + 'crowdDevApi', + options, + ); + if (responseData === undefined || responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + + return true; + }, + + async delete(this: IHookFunctions): Promise { + const creds = await getCreds(this); + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + try { + const options = createRequest(creds, { + url: `/automation/${webhookData.webhookId}`, + method: 'DELETE', + }); + await this.helpers.httpRequestWithAuthentication.call(this, credsName, options); + } catch (error) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registered anymore + delete webhookData.webhookId; + delete webhookData.webhookEvents; + delete webhookData.hookSecret; + } + + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + + return { + workflowData: [this.helpers.returnJsonArray(bodyData)], + }; + } +} diff --git a/packages/nodes-base/nodes/CrowdDev/GenericFunctions.ts b/packages/nodes-base/nodes/CrowdDev/GenericFunctions.ts new file mode 100644 index 0000000000000..5bb06fd38ec76 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/GenericFunctions.ts @@ -0,0 +1,221 @@ +import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow'; + +const addOptName = 'additionalOptions'; + +const getAllParams = (execFns: IExecuteSingleFunctions): Record => { + const params = execFns.getNode().parameters; + const keys = Object.keys(params); + const paramsWithValues = keys + .filter((i) => i !== addOptName) + .map((name) => [name, execFns.getNodeParameter(name)]); + + const paramsWithValuesObj = Object.fromEntries(paramsWithValues); + + if (keys.includes(addOptName)) { + const additionalOptions = execFns.getNodeParameter(addOptName); + return Object.assign(paramsWithValuesObj, additionalOptions); + } + + return paramsWithValuesObj; +}; + +const formatParams = ( + obj: Record, + filters?: { [paramName: string]: (value: any) => boolean }, + mappers?: { [paramName: string]: (value: any) => any }, +) => { + return Object.fromEntries( + Object.entries(obj) + .filter(([name, value]) => !filters || (name in filters ? filters[name](value) : false)) + .map(([name, value]) => + !mappers || !(name in mappers) ? [name, value] : [name, mappers[name](value)], + ), + ); +}; + +const objectFromProps = (src: any, props: string[]) => { + const result = props.filter((p) => src.hasOwnProperty(p)).map((p) => [p, src[p]]); + return Object.fromEntries(result); +}; + +const idFn = (i: any) => i; + +const keyValueToObj = (arr: any[]) => { + const obj: any = {}; + arr.forEach((item) => { + obj[item.key] = item.value; + }); + return obj; +}; + +const transformSingleProp = (prop: string) => (values: any) => + (values.itemChoice || []).map((i: any) => i[prop]); + +export async function activityPresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + const isCreateWithMember = params.operation === 'createWithMember'; + const isCreateForMember = params.operation === 'createForMember'; + + if (isCreateWithMember) { + // Move following props into "member" subproperty + const memberProps = ['displayName', 'emails', 'joinedAt', 'username']; + params.member = objectFromProps(params, memberProps); + memberProps.forEach((p) => delete params[p]); + } + opts.body = formatParams( + params, + { + member: (v) => (isCreateWithMember || isCreateForMember) && v, + type: idFn, + timestamp: idFn, + platform: idFn, + title: idFn, + body: idFn, + channel: idFn, + sourceId: idFn, + sourceParentId: idFn, + }, + { + member: (v) => + typeof v === 'object' + ? formatParams( + v as Record, + { + username: (un) => un.itemChoice, + displayName: idFn, + emails: idFn, + joinedAt: idFn, + }, + { + username: (un) => keyValueToObj(un.itemChoice as any[]), + emails: transformSingleProp('email'), + }, + ) + : v, + }, + ); + return opts; +} + +export async function automationPresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + opts.body = { + data: { + settings: { + url: params.url, + }, + type: 'webhook', + trigger: params.trigger, + }, + }; + return opts; +} + +export async function memberPresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + opts.body = formatParams( + params, + { + platform: idFn, + username: idFn, + displayName: idFn, + emails: (i) => i.itemChoice, + joinedAt: idFn, + organizations: (i) => i.itemChoice, + tags: (i) => i.itemChoice, + tasks: (i) => i.itemChoice, + notes: (i) => i.itemChoice, + activities: (i) => i.itemChoice, + }, + { + emails: transformSingleProp('email'), + organizations: (i) => + i.itemChoice.map((org: any) => + formatParams( + org as Record, + { + name: idFn, + url: idFn, + description: idFn, + logo: idFn, + employees: idFn, + members: (j) => j.itemChoice, + }, + { + members: transformSingleProp('member'), + }, + ), + ), + tags: transformSingleProp('tag'), + tasks: transformSingleProp('task'), + notes: transformSingleProp('note'), + activities: transformSingleProp('activity'), + }, + ); + return opts; +} + +export async function notePresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + opts.body = { + body: params.body, + }; + return opts; +} + +export async function organizationPresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + opts.body = formatParams( + params, + { + name: idFn, + url: idFn, + description: idFn, + logo: idFn, + employees: idFn, + members: (j) => j.itemChoice, + }, + { + members: transformSingleProp('member'), + }, + ); + return opts; +} + +export async function taskPresend( + this: IExecuteSingleFunctions, + opts: IHttpRequestOptions, +): Promise { + const params = getAllParams(this); + opts.body = formatParams( + params, + { + name: idFn, + body: idFn, + status: idFn, + members: (i) => i.itemChoice, + activities: (i) => i.itemChoice, + assigneess: idFn, + }, + { + members: transformSingleProp('member'), + activities: transformSingleProp('activity'), + }, + ); + return opts; +} diff --git a/packages/nodes-base/nodes/CrowdDev/crowdDev.svg b/packages/nodes-base/nodes/CrowdDev/crowdDev.svg new file mode 100644 index 0000000000000..0ba87c1306c3e --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/crowdDev.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/activityFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/activityFields.ts new file mode 100644 index 0000000000000..f7fe7c2050b7b --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/activityFields.ts @@ -0,0 +1,186 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { activityPresend } from '../GenericFunctions'; +import { emailsField } from './shared'; +import { getAdditionalOptions, mapWith, showFor } from './utils'; + +const displayOpts = showFor(['activity']); + +const displayFor = { + resource: displayOpts(), + createWithMember: displayOpts(['createWithMember']), + createForMember: displayOpts(['createForMember']), +}; + +const activityOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'createWithMember', + options: [ + { + name: 'Create or Update with a Member', + value: 'createWithMember', + description: 'Create or update an activity with a member', + action: 'Create or update an activity with a member', + routing: { + send: { preSend: [activityPresend] }, + request: { + method: 'POST', + url: '/activity/with-member', + }, + }, + }, + { + name: 'Create', + value: 'createForMember', + description: 'Create an activity for a member', + action: 'Create an activity for a member', + routing: { + send: { preSend: [activityPresend] }, + request: { + method: 'POST', + url: '/activity', + }, + }, + }, + ], +}; + +const createWithMemberFields: INodeProperties[] = [ + { + displayName: 'Username', + name: 'username', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + required: true, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Platform', + description: 'Platform name (e.g twitter, github, etc)', + name: 'key', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Username', + description: 'Username at the specified Platform', + name: 'value', + type: 'string', + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'displayName', + name: 'displayName', + description: 'UI friendly name of the member', + type: 'string', + default: '', + }, + emailsField, + { + displayName: 'Joined At', + name: 'joinedAt', + description: 'Date of joining the community', + type: 'dateTime', + default: '', + }, +]; + +const memberIdField: INodeProperties = { + displayName: 'Member', + name: 'member', + description: 'The ID of the member that performed the activity', + type: 'string', + required: true, + default: '', +}; + +const createCommonFields: INodeProperties[] = [ + { + displayName: 'Type', + name: 'type', + description: 'Type of activity', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + description: 'Date and time when the activity took place', + type: 'dateTime', + required: true, + default: '', + }, + { + displayName: 'Platform', + name: 'platform', + description: 'Platform on which the activity took place', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Source ID', + name: 'sourceId', + description: 'The ID of the activity in the platform (e.g. the ID of the message in Discord)', + type: 'string', + required: true, + default: '', + }, +]; + +const additionalOptions: INodeProperties[] = [ + { + displayName: 'Title', + name: 'title', + description: 'Title of the activity', + type: 'string', + default: '', + }, + { + displayName: 'Body', + name: 'body', + description: 'Body of the activity', + type: 'string', + default: '', + }, + { + displayName: 'Channel', + name: 'channel', + description: 'Channel of the activity', + type: 'string', + default: '', + }, + { + displayName: 'Source Parent ID', + name: 'sourceParentId', + description: + 'The ID of the parent activity in the platform (e.g. the ID of the parent message in Discord)', + type: 'string', + default: '', + }, +]; + +const activityFields: INodeProperties[] = [ + ...createWithMemberFields.map(mapWith(displayFor.createWithMember)), + Object.assign({}, memberIdField, displayFor.createForMember), + ...createCommonFields.map(mapWith(displayFor.resource)), + Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.resource), +]; + +export { activityOperations, activityFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/automationFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/automationFields.ts new file mode 100644 index 0000000000000..3607321a64825 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/automationFields.ts @@ -0,0 +1,129 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { automationPresend } from '../GenericFunctions'; +import { mapWith, showFor } from './utils'; + +const displayOpts = showFor(['automation']); + +const displayFor = { + resource: displayOpts(), + createOrUpdate: displayOpts(['create', 'update']), + id: displayOpts(['destroy', 'find', 'update']), +}; + +const automationOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'list', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new automation for the tenant', + action: 'Create a new automation for the tenant', + routing: { + send: { preSend: [automationPresend] }, + request: { + method: 'POST', + url: '/automation', + }, + }, + }, + { + name: 'Destroy', + value: 'destroy', + description: 'Destroy an existing automation for the tenant', + action: 'Destroy an existing automation for the tenant', + routing: { + request: { + method: 'DELETE', + url: '=/automation/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'Find', + value: 'find', + description: 'Get an existing automation data for the tenant', + action: 'Get an existing automation data for the tenant', + routing: { + request: { + method: 'GET', + url: '=/automation/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'List', + value: 'list', + description: 'Get all existing automation data for tenant', + action: 'Get all existing automation data for tenant', + routing: { + request: { + method: 'GET', + url: '/automation', + }, + }, + }, + { + name: 'Update', + value: 'update', + description: 'Updates an existing automation for the tenant', + action: 'Updates an existing automation for the tenant', + routing: { + send: { preSend: [automationPresend] }, + request: { + method: 'PUT', + url: '=/automation/{{$parameter["id"]}}', + }, + }, + }, + ], +}; + +const idField: INodeProperties = { + displayName: 'ID', + name: 'id', + description: 'The ID of the automation', + type: 'string', + required: true, + default: '', +}; + +const commonFields: INodeProperties[] = [ + { + displayName: 'Trigger', + name: 'trigger', + description: 'What will trigger an automation', + type: 'options', + required: true, + default: 'new_activity', + options: [ + { + name: 'New Activity', + value: 'new_activity', + }, + { + name: 'New Member', + value: 'new_member', + }, + ], + }, + { + displayName: 'URL', + name: 'url', + description: 'URL to POST webhook data to', + type: 'string', + required: true, + default: '', + }, +]; + +const automationFields: INodeProperties[] = [ + Object.assign({}, idField, displayFor.id), + ...commonFields.map(mapWith(displayFor.createOrUpdate)), +]; + +export { automationOperations, automationFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/index.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/index.ts new file mode 100644 index 0000000000000..2d92d79380a1c --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/index.ts @@ -0,0 +1,24 @@ +import { resources } from './resources'; +import { activityOperations, activityFields } from './activityFields'; +import { memberFields, memberOperations } from './memberFields'; +import { noteFields, noteOperations } from './noteFields'; +import { organizationFields, organizationOperations } from './organizationFields'; +import { taskFields, taskOperations } from './taskFields'; +import type { INodeProperties } from 'n8n-workflow'; +import { automationFields, automationOperations } from './automationFields'; + +export const allProperties: INodeProperties[] = [ + resources, + activityOperations, + memberOperations, + noteOperations, + organizationOperations, + taskOperations, + automationOperations, + ...activityFields, + ...memberFields, + ...noteFields, + ...organizationFields, + ...taskFields, + ...automationFields, +]; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/memberFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/memberFields.ts new file mode 100644 index 0000000000000..c42acd25c76e5 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/memberFields.ts @@ -0,0 +1,275 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { getAdditionalOptions, getId, mapWith, showFor } from './utils'; +import * as shared from './shared'; +import { memberPresend } from '../GenericFunctions'; + +const displayOpts = showFor(['member']); + +const displayFor = { + resource: displayOpts(), + createOrUpdate: displayOpts(['createOrUpdate', 'update']), + id: displayOpts(['delete', 'find', 'update']), +}; + +const memberOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'find', + options: [ + { + name: 'Create or Update', + value: 'createOrUpdate', + description: 'Create or update a member', + action: 'Create or update a member', + routing: { + send: { preSend: [memberPresend] }, + request: { + method: 'POST', + url: '/member', + }, + }, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a member', + action: 'Delete a member', + routing: { + request: { + method: 'DELETE', + url: '=/member', + }, + }, + }, + { + name: 'Find', + value: 'find', + description: 'Find a member', + action: 'Find a member', + routing: { + request: { + method: 'GET', + url: '=/member/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'Update', + value: 'update', + description: 'Update a member', + action: 'Update a member', + routing: { + send: { preSend: [memberPresend] }, + request: { + method: 'PUT', + url: '=/member/{{$parameter["id"]}}', + }, + }, + }, + ], +}; + +const commonFields: INodeProperties[] = [ + { + displayName: 'Platform', + name: 'platform', + description: 'Platform for which to check member existence', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Username', + name: 'username', + description: 'Username of the member in platform', + type: 'string', + required: true, + default: '', + }, +]; + +const additionalOptions: INodeProperties[] = [ + { + displayName: 'Display Name', + name: 'displayName', + description: 'UI friendly name of the member', + type: 'string', + default: '', + }, + shared.emailsField, + { + displayName: 'Joined At', + name: 'joinedAt', + description: 'Date of joining the community', + type: 'dateTime', + default: '', + }, + { + displayName: 'Organizations', + name: 'organizations', + description: + 'Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. If the organization does not exist, it will be created.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Name', + name: 'name', + description: 'The name of the organization', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Url', + name: 'url', + description: 'The URL of the organization', + type: 'string', + default: '', + }, + { + displayName: 'Description', + name: 'description', + description: 'A short description of the organization', + type: 'string', + typeOptions: { + rows: 3, + }, + default: '', + }, + { + displayName: 'Logo', + name: 'logo', + description: 'A URL for logo of the organization', + type: 'string', + default: '', + }, + { + displayName: 'Employees', + name: 'employees', + description: 'The number of employees of the organization', + type: 'number', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Tags', + name: 'tags', + description: 'Tags associated with the member. Each element in the array is the ID of the tag.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Tag', + name: 'tag', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Tasks', + name: 'tasks', + description: + 'Tasks associated with the member. Each element in the array is the ID of the task.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Task', + name: 'task', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Notes', + name: 'notes', + description: + 'Notes associated with the member. Each element in the array is the ID of the note.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Activities', + name: 'activities', + description: + 'Activities associated with the member. Each element in the array is the ID of the activity.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Activity', + name: 'activity', + type: 'string', + default: '', + }, + ], + }, + ], + }, +]; + +const memberFields: INodeProperties[] = [ + Object.assign(getId(), { description: 'The ID of the member' }, displayFor.id), + ...commonFields.map(mapWith(displayFor.createOrUpdate)), + Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate), +]; + +export { memberOperations, memberFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/noteFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/noteFields.ts new file mode 100644 index 0000000000000..7184d88f0bbbe --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/noteFields.ts @@ -0,0 +1,92 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { notePresend } from '../GenericFunctions'; +import { getId, mapWith, showFor } from './utils'; + +const displayOpts = showFor(['note']); + +const displayFor = { + resource: displayOpts(), + createOrUpdate: displayOpts(['create', 'update']), + id: displayOpts(['delete', 'find', 'update']), +}; + +const noteOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'find', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a note', + action: 'Create a note', + routing: { + send: { preSend: [notePresend] }, + request: { + method: 'POST', + url: '/note', + }, + }, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a note', + action: 'Delete a note', + routing: { + request: { + method: 'DELETE', + url: '=/note', + }, + }, + }, + { + name: 'Find', + value: 'find', + description: 'Find a note', + action: 'Find a note', + routing: { + request: { + method: 'GET', + url: '=/note/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'Update', + value: 'update', + description: 'Update a note', + action: 'Update a note', + routing: { + send: { preSend: [notePresend] }, + request: { + method: 'PUT', + url: '=/note/{{$parameter["id"]}}', + }, + }, + }, + ], +}; + +const commonFields: INodeProperties[] = [ + { + displayName: 'Body', + name: 'body', + description: 'The body of the note', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + }, +]; + +const noteFields: INodeProperties[] = [ + Object.assign(getId(), { description: 'The ID of the note' }, displayFor.id), + ...commonFields.map(mapWith(displayFor.createOrUpdate)), +]; + +export { noteOperations, noteFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/organizationFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/organizationFields.ts new file mode 100644 index 0000000000000..283c85193ea61 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/organizationFields.ts @@ -0,0 +1,150 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { organizationPresend } from '../GenericFunctions'; +import { getAdditionalOptions, getId, mapWith, showFor } from './utils'; + +const displayOpts = showFor(['organization']); + +const displayFor = { + resource: displayOpts(), + createOrUpdate: displayOpts(['create', 'update']), + id: displayOpts(['delete', 'find', 'update']), +}; + +const organizationOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'find', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an organization', + action: 'Create an organization', + routing: { + send: { preSend: [organizationPresend] }, + request: { + method: 'POST', + url: '/organization', + }, + }, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an organization', + action: 'Delete an organization', + routing: { + request: { + method: 'DELETE', + url: '=/organization', + }, + }, + }, + { + name: 'Find', + value: 'find', + description: 'Find an organization', + action: 'Find an organization', + routing: { + request: { + method: 'GET', + url: '=/organization/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'Update', + value: 'update', + description: 'Update an organization', + action: 'Update an organization', + routing: { + send: { preSend: [organizationPresend] }, + request: { + method: 'PUT', + url: '=/organization/{{$parameter["id"]}}', + }, + }, + }, + ], +}; + +const commonFields: INodeProperties[] = [ + { + displayName: 'Name', + name: 'name', + description: 'The name of the organization', + type: 'string', + required: true, + default: '', + }, +]; + +const additionalOptions: INodeProperties[] = [ + { + displayName: 'Url', + name: 'url', + description: 'The URL of the organization', + type: 'string', + default: '', + }, + { + displayName: 'Description', + name: 'description', + description: 'A short description of the organization', + type: 'string', + typeOptions: { + rows: 3, + }, + default: '', + }, + { + displayName: 'Logo', + name: 'logo', + description: 'A URL for logo of the organization', + type: 'string', + default: '', + }, + { + displayName: 'Employees', + name: 'employees', + description: 'The number of employees of the organization', + type: 'number', + default: '', + }, + { + displayName: 'Members', + name: 'members', + description: + 'Members associated with the organization. Each element in the array is the ID of the member.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Member', + name: 'member', + type: 'string', + default: '', + }, + ], + }, + ], + }, +]; + +const organizationFields: INodeProperties[] = [ + Object.assign(getId(), { description: 'The ID of the organization' }, displayFor.id), + ...commonFields.map(mapWith(displayFor.createOrUpdate)), + Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate), +]; + +export { organizationOperations, organizationFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/resources.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/resources.ts new file mode 100644 index 0000000000000..430b13cb5641b --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/resources.ts @@ -0,0 +1,36 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const resources: INodeProperties = { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + default: 'activity', + placeholder: 'Resourcee', + options: [ + { + name: 'Activity', + value: 'activity', + }, + { + name: 'Automation', + value: 'automation', + }, + { + name: 'Member', + value: 'member', + }, + { + name: 'Note', + value: 'note', + }, + { + name: 'Organization', + value: 'organization', + }, + { + name: 'Task', + value: 'task', + }, + ], +}; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/shared.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/shared.ts new file mode 100644 index 0000000000000..3e0711000b11a --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/shared.ts @@ -0,0 +1,27 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const emailsField: INodeProperties = { + displayName: 'Emails', + name: 'emails', + description: 'Email addresses of the member', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + }, + ], + }, + ], +}; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/taskFields.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/taskFields.ts new file mode 100644 index 0000000000000..8a39b0031a93b --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/taskFields.ts @@ -0,0 +1,163 @@ +import type { INodeProperties } from 'n8n-workflow'; +import { taskPresend } from '../GenericFunctions'; +import { getAdditionalOptions, getId, showFor } from './utils'; + +const displayOpts = showFor(['task']); + +const displayFor = { + resource: displayOpts(), + createOrUpdate: displayOpts(['create', 'update']), + id: displayOpts(['delete', 'find', 'update']), +}; + +const taskOperations: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: displayFor.resource.displayOptions, + noDataExpression: true, + default: 'find', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a task', + action: 'Create a task', + routing: { + send: { preSend: [taskPresend] }, + request: { + method: 'POST', + url: '/task', + }, + }, + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + action: 'Delete a task', + routing: { + request: { + method: 'DELETE', + url: '=/task', + }, + }, + }, + { + name: 'Find', + value: 'find', + description: 'Find a task', + action: 'Find a task', + routing: { + request: { + method: 'GET', + url: '=/task/{{$parameter["id"]}}', + }, + }, + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + action: 'Update a task', + routing: { + send: { preSend: [taskPresend] }, + request: { + method: 'PUT', + url: '=/task/{{$parameter["id"]}}', + }, + }, + }, + ], +}; + +const additionalOptions: INodeProperties[] = [ + { + displayName: 'Name', + name: 'name', + description: 'The name of the task', + type: 'string', + default: '', + }, + { + displayName: 'Body', + name: 'body', + description: 'The body of the task', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + }, + { + displayName: 'Status', + name: 'status', + description: 'The status of the task', + type: 'string', + default: '', + }, + { + displayName: 'Members', + name: 'members', + description: + 'Members associated with the task. Each element in the array is the ID of the member.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Member', + name: 'member', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Activities', + name: 'activities', + description: + 'Activities associated with the task. Each element in the array is the ID of the activity.', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Item Choice', + name: 'itemChoice', + values: [ + { + displayName: 'Activity', + name: 'activity', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Assigneess', + name: 'assigneess', + description: 'Users assigned with the task. Each element in the array is the ID of the user.', + type: 'string', + default: '', + }, +]; + +const taskFields: INodeProperties[] = [ + Object.assign(getId(), { description: 'The ID of the task' }, displayFor.id), + Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate), +]; + +export { taskOperations, taskFields }; diff --git a/packages/nodes-base/nodes/CrowdDev/descriptions/utils.ts b/packages/nodes-base/nodes/CrowdDev/descriptions/utils.ts new file mode 100644 index 0000000000000..35488eea44767 --- /dev/null +++ b/packages/nodes-base/nodes/CrowdDev/descriptions/utils.ts @@ -0,0 +1,57 @@ +import type { INodeProperties } from 'n8n-workflow'; + +export const showFor = + (resources: string[]) => + (operations?: string[]): Partial => { + return operations !== undefined + ? { + displayOptions: { + show: { + resource: resources, + operation: operations, + }, + }, + } + : { + displayOptions: { + show: { + resource: resources, + }, + }, + }; + }; + +export const mapWith = + (...objects: Array>) => + (item: Partial) => + Object.assign({}, item, ...objects); + +export const getId = (): INodeProperties => ({ + displayName: 'ID', + name: 'id', + type: 'string', + required: true, + default: '', + routing: { + send: { + type: 'query', + property: 'ids[]', + }, + }, +}); + +export const getAdditionalOptions = (fields: INodeProperties[]): INodeProperties => { + return { + displayName: 'Additional Options', + name: 'additionalOptions', + type: 'collection', + displayOptions: { + show: { + operation: ['getAll'], + }, + }, + default: {}, + placeholder: 'Add Option', + options: fields, + }; +}; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 05a52f8deb023..06e95615a501b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -77,6 +77,7 @@ "dist/credentials/CopperApi.credentials.js", "dist/credentials/CortexApi.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/CrowdDevApi.credentials.js", "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/DeepLApi.credentials.js", "dist/credentials/DemioApi.credentials.js", @@ -423,6 +424,8 @@ "dist/nodes/Cortex/Cortex.node.js", "dist/nodes/CrateDb/CrateDb.node.js", "dist/nodes/Cron/Cron.node.js", + "dist/nodes/CrowdDev/CrowdDev.node.js", + "dist/nodes/CrowdDev/CrowdDevTrigger.node.js", "dist/nodes/Crypto/Crypto.node.js", "dist/nodes/CustomerIo/CustomerIo.node.js", "dist/nodes/CustomerIo/CustomerIoTrigger.node.js",