diff --git a/docker/images/n8n-custom/Dockerfile copy b/docker/images/n8n-custom/Dockerfile copy deleted file mode 100644 index 19f08a16dd..0000000000 --- a/docker/images/n8n-custom/Dockerfile copy +++ /dev/null @@ -1,49 +0,0 @@ -FROM node:12.16-alpine as builder -# FROM node:12.16-alpine - -# Update everything and install needed dependencies -RUN apk add --update graphicsmagick tzdata git tini su-exec - -USER root - -# Install all needed dependencies -RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \ - npm_config_user=root npm install -g full-icu lerna - -ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu - -WORKDIR /data - -COPY lerna.json . -COPY package.json . -COPY packages/cli/ ./packages/cli/ -COPY packages/core/ ./packages/core/ -COPY packages/editor-ui/ ./packages/editor-ui/ -COPY packages/nodes-base/ ./packages/nodes-base/ -COPY packages/workflow/ ./packages/workflow/ -RUN rm -rf node_modules packages/*/node_modules packages/*/dist - -RUN npm install --loglevel notice -RUN lerna bootstrap --hoist -RUN npm run build - - -FROM node:12.16-alpine - -WORKDIR /data - -# Install all needed dependencies -RUN npm_config_user=root npm install -g full-icu - -USER root - -ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu - -COPY --from=builder /data ./ - -RUN apk add --update graphicsmagick tzdata git tini su-exec - -COPY docker/images/n8n-dev/docker-entrypoint.sh /docker-entrypoint.sh -ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] - -EXPOSE 5678/tcp diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index fd77c75406..44eddb69a4 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,18 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.105.0 + +### What changed? +In the Hubspot Trigger, now multiple events can be provided and the field `App ID` was so moved to the credentials. + +### When is action necessary? +If you are using the Hubspot Trigger node. + +### How to upgrade: +Open the Hubspot Trigger and set the events again. Also open the credentials `Hubspot Developer API` and set your APP ID. + + ## 0.104.0 ### What changed? diff --git a/packages/cli/package.json b/packages/cli/package.json index f29e3e2a43..6bd603f82c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.104.2", + "version": "0.106.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -102,10 +102,10 @@ "localtunnel": "^2.0.0", "lodash.get": "^4.4.2", "mysql2": "~2.1.0", - "n8n-core": "~0.61.0", - "n8n-editor-ui": "~0.74.0", - "n8n-nodes-base": "~0.101.0", - "n8n-workflow": "~0.50.0", + "n8n-core": "~0.62.0", + "n8n-editor-ui": "~0.76.0", + "n8n-nodes-base": "~0.103.0", + "n8n-workflow": "~0.51.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 2bf2a2f7b9..1f35f0e198 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -277,7 +277,7 @@ export class ActiveWorkflowRunner { path = node.parameters.path as string; if (node.parameters.path === undefined) { - path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; + path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path'], mode) as string | undefined; if (path === undefined) { // TODO: Use a proper logger @@ -286,7 +286,7 @@ export class ActiveWorkflowRunner { } } - const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], mode, false) as boolean; const webhook = { workflowId: webhookData.workflowId, diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 4f57bae4dd..07e9f484f7 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -14,6 +14,7 @@ import { INodeTypes, NodeHelpers, Workflow, + WorkflowExecuteMode, } from 'n8n-workflow'; import { @@ -101,7 +102,7 @@ export class CredentialsHelper extends ICredentialsHelper { * @returns {ICredentialDataDecryptedObject} * @memberof CredentialsHelper */ - getDecrypted(name: string, type: string, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject { + getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject { const credentials = this.getCredentials(name, type); const decryptedDataOriginal = credentials.getData(this.encryptionKey); @@ -110,7 +111,7 @@ export class CredentialsHelper extends ICredentialsHelper { return decryptedDataOriginal; } - return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type, expressionResolveValues); + return this.applyDefaultsAndOverwrites(decryptedDataOriginal, type, mode, expressionResolveValues); } @@ -122,7 +123,7 @@ export class CredentialsHelper extends ICredentialsHelper { * @returns {ICredentialDataDecryptedObject} * @memberof CredentialsHelper */ - applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject { + applyDefaultsAndOverwrites(decryptedDataOriginal: ICredentialDataDecryptedObject, type: string, mode: WorkflowExecuteMode, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject { const credentialsProperties = this.getCredentialsProperties(type); // Add the default credential values @@ -137,7 +138,7 @@ export class CredentialsHelper extends ICredentialsHelper { if (expressionResolveValues) { try { const workflow = new Workflow({ nodes: Object.values(expressionResolveValues.workflow.nodes), connections: expressionResolveValues.workflow.connectionsBySourceNode, active: false, nodeTypes: expressionResolveValues.workflow.nodeTypes }); - decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, false, decryptedData) as ICredentialDataDecryptedObject; + decryptedData = workflow.expression.getParameterValue(decryptedData as INodeParameters, expressionResolveValues.runExecutionData, expressionResolveValues.runIndex, expressionResolveValues.itemIndex, expressionResolveValues.node.name, expressionResolveValues.connectionInputData, mode, false, decryptedData) as ICredentialDataDecryptedObject; } catch (e) { e.message += ' [Error resolving credentials]'; throw e; @@ -154,7 +155,7 @@ export class CredentialsHelper extends ICredentialsHelper { const workflow = new Workflow({ nodes: [node!], connections: {}, active: false, nodeTypes: mockNodeTypes }); // Resolve expressions if any are set - decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, undefined, decryptedData) as ICredentialDataDecryptedObject; + decryptedData = workflow.expression.getComplexParameterValue(node!, decryptedData as INodeParameters, mode, undefined, decryptedData) as ICredentialDataDecryptedObject; } // Load and apply the credentials overwrites if any exist diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4b38988b71..6182ff7f24 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -82,6 +82,7 @@ import { IRunData, IWorkflowCredentials, Workflow, + WorkflowExecuteMode, } from 'n8n-workflow'; import { @@ -1078,9 +1079,10 @@ class App { [result.name as string]: result as ICredentialsEncrypted, }, }; + const mode: WorkflowExecuteMode = 'internal'; const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); - const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, mode, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string; @@ -1168,9 +1170,10 @@ class App { [result.name as string]: result as ICredentialsEncrypted, }, }; + const mode: WorkflowExecuteMode = 'internal'; const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); - const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, mode, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const options: OptionsWithUrl = { method: 'POST', @@ -1239,9 +1242,10 @@ class App { [result.name as string]: result as ICredentialsEncrypted, }, }; + const mode: WorkflowExecuteMode = 'internal'; const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); - const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, mode, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const token = new csrf(); // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR @@ -1336,9 +1340,10 @@ class App { [result.name as string]: result as ICredentialsEncrypted, }, }; + const mode: WorkflowExecuteMode = 'internal'; const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey); - const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true); - const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type); + const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, mode, true); + const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type, mode); const token = new csrf(); if (decryptedDataOriginal.csrfSecret === undefined || !token.verify(decryptedDataOriginal.csrfSecret as string, state.token)) { diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 6d3504963a..3da2e2a012 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -115,8 +115,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } // Get the responseMode - const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); - const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; + const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], executionMode, 'onReceived'); + const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], executionMode, 200) as number; if (!['onReceived', 'lastNode'].includes(responseMode as string)) { // If the mode is not known we error. Is probably best like that instead of using @@ -174,7 +174,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { await WorkflowHelpers.saveStaticData(workflow); if (webhookData.webhookDescription['responseHeaders'] !== undefined) { - const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { + const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], executionMode, undefined) as { entries?: Array<{ name: string; value: string; @@ -328,7 +328,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return data; } - const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); + const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], executionMode, 'firstEntryJson'); if (didSendResponse === false) { let data: IDataObject | IDataObject[]; @@ -343,13 +343,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { data = returnData.data!.main[0]![0].json; - const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); + const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], executionMode, undefined); if (responsePropertyName !== undefined) { data = get(data, responsePropertyName as string) as IDataObject; } - const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); + const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], executionMode, undefined); if (responseContentType !== undefined) { // Send the webhook response manually to be able to set the content-type @@ -382,7 +382,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { didSendResponse = true; } - const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); + const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], executionMode, 'data'); if (responseBinaryPropertyName === undefined && didSendResponse === false) { responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); diff --git a/packages/core/package.json b/packages/core/package.json index ee7f4a379d..98dc4eb259 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.61.0", + "version": "0.62.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -47,7 +47,7 @@ "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.50.0", + "n8n-workflow": "~0.51.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index c944c0de72..dcdc2bd4bb 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -299,7 +299,7 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe * @param {IWorkflowExecuteAdditionalData} additionalData * @returns {(ICredentialDataDecryptedObject | undefined)} */ -export function getCredentials(workflow: Workflow, node: INode, type: string, additionalData: IWorkflowExecuteAdditionalData, runExecutionData?: IRunExecutionData | null, runIndex?: number, connectionInputData?: INodeExecutionData[], itemIndex?: number): ICredentialDataDecryptedObject | undefined { +export function getCredentials(workflow: Workflow, node: INode, type: string, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, runExecutionData?: IRunExecutionData | null, runIndex?: number, connectionInputData?: INodeExecutionData[], itemIndex?: number): ICredentialDataDecryptedObject | undefined { // Get the NodeType as it has the information if the credentials are required const nodeType = workflow.nodeTypes.getByName(node.type); @@ -353,7 +353,7 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad const name = node.credentials[type]; - const decryptedDataObject = additionalData.credentialsHelper.getDecrypted(name, type, false, expressionResolveValues); + const decryptedDataObject = additionalData.credentialsHelper.getDecrypted(name, type, mode, false, expressionResolveValues); return decryptedDataObject; } @@ -387,7 +387,7 @@ export function getNode(node: INode): INode { * @param {*} [fallbackValue] * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object)} */ -export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, connectionInputData: INodeExecutionData[], node: INode, parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { //tslint:disable-line:no-any +export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, connectionInputData: INodeExecutionData[], node: INode, parameterName: string, itemIndex: number, mode: WorkflowExecuteMode, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { //tslint:disable-line:no-any const nodeType = workflow.nodeTypes.getByName(node.type); if (nodeType === undefined) { throw new Error(`Node type "${node.type}" is not known so can not return paramter value!`); @@ -401,7 +401,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu let returnData; try { - returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); } catch (e) { e.message += ` [Error in parameter: "${parameterName}"]`; throw e; @@ -436,7 +436,7 @@ export function continueOnFail(node: INode): boolean { * @param {boolean} [isTest] * @returns {(string | undefined)} */ -export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, isTest?: boolean): string | undefined { +export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, isTest?: boolean): string | undefined { let baseUrl = additionalData.webhookBaseUrl; if (isTest === true) { baseUrl = additionalData.webhookTestBaseUrl; @@ -447,12 +447,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, return undefined; } - const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); + const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode); if (path === undefined) { return undefined; } - const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, false) as boolean; return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); } @@ -538,7 +538,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio throw new Error('Overwrite NodeExecuteFunctions.getExecutePullFunctions.__emit function!'); }, getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData); + return getCredentials(workflow, node, type, additionalData, mode); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -552,7 +552,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; @@ -601,7 +601,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi throw new Error('Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function!'); }, getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData); + return getCredentials(workflow, node, type, additionalData, mode); }, getNode: () => { return getNode(node); @@ -615,7 +615,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getRestApiUrl: (): string => { return additionalData.restApiUrl; @@ -667,7 +667,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return continueOnFail(node); }, evaluateExpression: (expression: string, itemIndex: number) => { - return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); }, async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); @@ -676,7 +676,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return NodeHelpers.getContext(runExecutionData, type, node); }, getCredentials(type: string, itemIndex?: number): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData, runExecutionData, runIndex, connectionInputData, itemIndex); + return getCredentials(workflow, node, type, additionalData, mode, runExecutionData, runIndex, connectionInputData, itemIndex); }, getInputData: (inputIndex = 0, inputName = 'main') => { @@ -700,7 +700,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return inputData[inputName][inputIndex] as INodeExecutionData[]; }, getNodeParameter: (parameterName: string, itemIndex: number, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -718,7 +718,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return getWorkflowMetadata(workflow); }, getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => { - const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); return dataProxy.getDataProxy(); }, getWorkflowStaticData(type: string): IDataObject { @@ -765,13 +765,13 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: }, evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; - return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); }, getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData, runExecutionData, runIndex, connectionInputData, itemIndex); + return getCredentials(workflow, node, type, additionalData, mode, runExecutionData, runIndex, connectionInputData, itemIndex); }, getInputData: (inputIndex = 0, inputName = 'main') => { if (!inputData.hasOwnProperty(inputName)) { @@ -811,13 +811,13 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: return getTimezone(workflow, additionalData); }, getNodeParameter: (parameterName: string, fallbackValue?: any): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object => { //tslint:disable-line:no-any - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getWorkflow: () => { return getWorkflowMetadata(workflow); }, getWorkflowDataProxy: (): IWorkflowDataProxyData => { - const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); return dataProxy.getDataProxy(); }, getWorkflowStaticData(type: string): IDataObject { @@ -851,7 +851,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio return ((workflow: Workflow, node: INode) => { const that = { getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData); + return getCredentials(workflow, node, type, additionalData, 'internal'); }, getCurrentNodeParameter: (parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => { const nodeParameters = additionalData.currentNodeParameters; @@ -872,7 +872,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, 'internal' as WorkflowExecuteMode, fallbackValue); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); @@ -910,7 +910,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio return ((workflow: Workflow, node: INode) => { const that = { getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData); + return getCredentials(workflow, node, type, additionalData, mode); }, getMode: (): WorkflowExecuteMode => { return mode; @@ -924,10 +924,10 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getNodeWebhookUrl: (name: string): string | undefined => { - return getNodeWebhookUrl(name, workflow, node, additionalData, isTest); + return getNodeWebhookUrl(name, workflow, node, additionalData, mode, isTest); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); @@ -984,7 +984,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi return additionalData.httpRequest.body; }, getCredentials(type: string): ICredentialDataDecryptedObject | undefined { - return getCredentials(workflow, node, type, additionalData); + return getCredentials(workflow, node, type, additionalData, mode); }, getHeaderData(): object { if (additionalData.httpRequest === undefined) { @@ -1004,7 +1004,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi const runIndex = 0; const connectionInputData: INodeExecutionData[] = []; - return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, fallbackValue); + return getNodeParameter(workflow, runExecutionData, runIndex, connectionInputData, node, parameterName, itemIndex, mode, fallbackValue); }, getParamsData(): object { if (additionalData.httpRequest === undefined) { @@ -1031,7 +1031,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi return additionalData.httpResponse; }, getNodeWebhookUrl: (name: string): string | undefined => { - return getNodeWebhookUrl(name, workflow, node, additionalData); + return getNodeWebhookUrl(name, workflow, node, additionalData, mode); }, getTimezone: (): string => { return getTimezone(workflow, additionalData); diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index c52e3ef7f4..ed9897a771 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -301,7 +301,7 @@ class NodeTypesClass implements INodeTypes { startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { - const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimy]*)$')); + const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); let regex: RegExp; if (!regexMatch) { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 6a045e3d04..7a923ccb03 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.74.0", + "version": "0.76.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -65,7 +65,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.50.0", + "n8n-workflow": "~0.51.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", diff --git a/packages/editor-ui/src/components/DataDisplay.vue b/packages/editor-ui/src/components/DataDisplay.vue index c694173540..1e3985c361 100644 --- a/packages/editor-ui/src/components/DataDisplay.vue +++ b/packages/editor-ui/src/components/DataDisplay.vue @@ -147,7 +147,7 @@ export default Vue.extend({ position: relative; width: 80%; height: 80%; - margin: 6em auto; + margin: 3em auto; background-color: #fff; border-radius: 2px; @media (max-height: 720px) { diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index ab134df115..4d8eb6e5ff 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -138,7 +138,7 @@ export default mixins(nodeBase, workflowHelpers).extend({ } if (this.nodeType !== null && this.nodeType.subtitle !== undefined) { - return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined; + return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle, 'internal') as string | undefined; } if (this.data.parameters.operation !== undefined) { diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue index 1bf23af23c..2f782e0a7d 100644 --- a/packages/editor-ui/src/components/VariableSelector.vue +++ b/packages/editor-ui/src/components/VariableSelector.vue @@ -376,7 +376,7 @@ export default mixins( return returnData; } - const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData); + const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, 'manual'); const proxy = dataProxy.getDataProxy(); // @ts-ignore diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 692c813412..b3ba72b272 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -362,7 +362,7 @@ export const workflowHelpers = mixins( connectionInputData = []; } - return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true); + return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', true); }, // Saves the currently loaded workflow to the database. diff --git a/packages/editor-ui/src/n8n-theme.scss b/packages/editor-ui/src/n8n-theme.scss index 93f90d011a..9b252a6c5f 100644 --- a/packages/editor-ui/src/n8n-theme.scss +++ b/packages/editor-ui/src/n8n-theme.scss @@ -58,7 +58,11 @@ h1, h2, h3, h4, h5, h6 { -webkit-box-shadow: none; box-shadow: none; - @media (max-height: 720px) { + @media (max-height: 1050px) { + margin: 4em auto !important; + } + + @media (max-height: 930px) { margin: 1em auto !important; } diff --git a/packages/nodes-base/credentials/DiscourseApi.credentials.ts b/packages/nodes-base/credentials/DiscourseApi.credentials.ts new file mode 100644 index 0000000000..98f38911e2 --- /dev/null +++ b/packages/nodes-base/credentials/DiscourseApi.credentials.ts @@ -0,0 +1,33 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class DiscourseApi implements ICredentialType { + name = 'discourseApi'; + displayName = 'Discourse API'; + documentationUrl = 'discourse'; + properties = [ + { + displayName: 'URL', + name: 'url', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Key', + name: 'apiKey', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Username', + name: 'username', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} \ No newline at end of file diff --git a/packages/nodes-base/credentials/GoogleApi.credentials.ts b/packages/nodes-base/credentials/GoogleApi.credentials.ts index 4f411a5229..681759b81d 100644 --- a/packages/nodes-base/credentials/GoogleApi.credentials.ts +++ b/packages/nodes-base/credentials/GoogleApi.credentials.ts @@ -14,7 +14,7 @@ export class GoogleApi implements ICredentialType { name: 'email', type: 'string' as NodePropertyTypes, default: '', - description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.
See the tutorial on how to create one.', + description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.', }, { diff --git a/packages/nodes-base/credentials/HubspotDeveloperApi.credentials.ts b/packages/nodes-base/credentials/HubspotDeveloperApi.credentials.ts index 5e21f5077b..2fdfbbc970 100644 --- a/packages/nodes-base/credentials/HubspotDeveloperApi.credentials.ts +++ b/packages/nodes-base/credentials/HubspotDeveloperApi.credentials.ts @@ -20,5 +20,13 @@ export class HubspotDeveloperApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'App ID', + name: 'appId', + type: 'string' as NodePropertyTypes, + required: true, + default: '', + description: 'The App ID', + }, ]; } diff --git a/packages/nodes-base/credentials/MySql.credentials.ts b/packages/nodes-base/credentials/MySql.credentials.ts index 5ebcb8d34c..7d2e05ed9e 100644 --- a/packages/nodes-base/credentials/MySql.credentials.ts +++ b/packages/nodes-base/credentials/MySql.credentials.ts @@ -42,5 +42,12 @@ export class MySql implements ICredentialType { type: 'number' as NodePropertyTypes, default: 3306, }, + { + displayName: 'Connect Timeout', + name: 'connectTimeout', + type: 'number' as NodePropertyTypes, + default: 10000, + description: 'The milliseconds before a timeout occurs during the initial connection to the MySQL server.', + }, ]; } diff --git a/packages/nodes-base/credentials/RedditOAuth2Api.credentials.ts b/packages/nodes-base/credentials/RedditOAuth2Api.credentials.ts new file mode 100644 index 0000000000..fe7def1a7a --- /dev/null +++ b/packages/nodes-base/credentials/RedditOAuth2Api.credentials.ts @@ -0,0 +1,69 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'identity', + 'edit', + 'history', + 'mysubreddits', + 'read', + 'save', + 'submit', +]; + +// https://github.com/reddit-archive/reddit/wiki/OAuth2 + +export class RedditOAuth2Api implements ICredentialType { + name = 'redditOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Reddit OAuth2 API'; + documentationUrl = 'reddit'; + properties = [ + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'response_type=code', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'grant_type=authorization_code', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: 'duration=permanent', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.reddit.com/api/v1/authorize', + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://www.reddit.com/api/v1/access_token', + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/credentials/SecurityScorecardApi.credentials.ts b/packages/nodes-base/credentials/SecurityScorecardApi.credentials.ts index e1eff983dc..db24a112e7 100644 --- a/packages/nodes-base/credentials/SecurityScorecardApi.credentials.ts +++ b/packages/nodes-base/credentials/SecurityScorecardApi.credentials.ts @@ -6,6 +6,7 @@ import { export class SecurityScorecardApi implements ICredentialType { name = 'securityScorecardApi'; displayName = 'SecurityScorecard API'; + documentationUrl = 'securityScorecard'; properties = [ { displayName: 'API Key', diff --git a/packages/nodes-base/credentials/TapfiliateApi.credentials.ts b/packages/nodes-base/credentials/TapfiliateApi.credentials.ts new file mode 100644 index 0000000000..d52df982a1 --- /dev/null +++ b/packages/nodes-base/credentials/TapfiliateApi.credentials.ts @@ -0,0 +1,19 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TapfiliateApi implements ICredentialType { + name = 'tapfiliateApi'; + displayName = 'Tapfiliate API'; + documentationUrl = 'tapfiliate'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + required: true, + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/TimescaleDb.credentials.ts b/packages/nodes-base/credentials/TimescaleDb.credentials.ts new file mode 100644 index 0000000000..7d1e195d6c --- /dev/null +++ b/packages/nodes-base/credentials/TimescaleDb.credentials.ts @@ -0,0 +1,87 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class TimescaleDb implements ICredentialType { + name = 'timescaleDb'; + displayName = 'TimescaleDB'; + documentationUrl = 'timescaleDb'; + properties = [ + { + displayName: 'Host', + name: 'host', + type: 'string' as NodePropertyTypes, + default: 'localhost', + }, + { + displayName: 'Database', + name: 'database', + type: 'string' as NodePropertyTypes, + default: 'postgres', + }, + { + displayName: 'User', + name: 'user', + type: 'string' as NodePropertyTypes, + default: 'postgres', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean' as NodePropertyTypes, + default: false, + description: 'Connect even if SSL certificate validation is not possible.', + }, + { + displayName: 'SSL', + name: 'ssl', + type: 'options' as NodePropertyTypes, + displayOptions: { + show: { + allowUnauthorizedCerts: [ + false, + ], + }, + }, + options: [ + { + name: 'disable', + value: 'disable', + }, + { + name: 'allow', + value: 'allow', + }, + { + name: 'require', + value: 'require', + }, + { + name: 'verify (not implemented)', + value: 'verify', + }, + { + name: 'verify-full (not implemented)', + value: 'verify-full', + }, + ], + default: 'disable', + }, + { + displayName: 'Port', + name: 'port', + type: 'number' as NodePropertyTypes, + default: 5432, + }, + ]; +} diff --git a/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts index 2708af5aff..69cdb874b1 100644 --- a/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/TypeformOAuth2Api.credentials.ts @@ -9,7 +9,6 @@ const scopes = [ 'forms:read', ]; - export class TypeformOAuth2Api implements ICredentialType { name = 'typeformOAuth2Api'; extends = [ @@ -36,7 +35,7 @@ export class TypeformOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden' as NodePropertyTypes, - default: scopes.join(','), + default: scopes.join(' '), }, { displayName: 'Auth URI Query Parameters', diff --git a/packages/nodes-base/nodes/Affinity/Affinity.node.ts b/packages/nodes-base/nodes/Affinity/Affinity.node.ts index 7efbbe4cf6..e422214db3 100644 --- a/packages/nodes-base/nodes/Affinity/Affinity.node.ts +++ b/packages/nodes-base/nodes/Affinity/Affinity.node.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, ILoadOptionsFunctions, @@ -9,27 +10,40 @@ import { INodeType, INodeTypeDescription, } from 'n8n-workflow'; + import { affinityApiRequest, affinityApiRequestAllItems, } from './GenericFunctions'; + import { organizationFields, organizationOperations, } from './OrganizationDescription'; + import { personFields, personOperations, } from './PersonDescription'; + +import { + listFields, + listOperations, +} from './ListDescription'; + +import { + listEntryFields, + listEntryOperations, +} from './ListEntryDescription'; + import { IOrganization, } from './OrganizationInterface'; + import { IPerson, } from './PersonInterface'; -import { snakeCase } from 'change-case'; - export class Affinity implements INodeType { description: INodeTypeDescription = { displayName: 'Affinity', @@ -57,6 +71,14 @@ export class Affinity implements INodeType { name: 'resource', type: 'options', options: [ + { + name: 'List', + value: 'list', + }, + { + name: 'List Entry', + value: 'listEntry', + }, { name: 'Organization', value: 'organization', @@ -69,6 +91,10 @@ export class Affinity implements INodeType { default: 'organization', description: 'Resource to consume.', }, + ...listOperations, + ...listFields, + ...listEntryOperations, + ...listEntryFields, ...organizationOperations, ...organizationFields, ...personOperations, @@ -101,7 +127,7 @@ export class Affinity implements INodeType { for (const person of persons) { let personName = `${person.first_name} ${person.last_name}`; if (person.primary_email !== null) { - personName+= ` (${person.primary_email})`; + personName += ` (${person.primary_email})`; } const personId = person.id; returnData.push({ @@ -111,6 +137,19 @@ export class Affinity implements INodeType { } return returnData; }, + // Get all the available lists to display them to user so that he can + // select them easily + async getLists(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const lists = await affinityApiRequest.call(this, 'GET', `/lists`); + for (const list of lists) { + returnData.push({ + name: list.name, + value: list.id, + }); + } + return returnData; + }, }, }; @@ -123,6 +162,59 @@ export class Affinity implements INodeType { const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { + if (resource === 'list') { + //https://api-docs.affinity.co/#get-a-specific-list + if (operation === 'get') { + const listId = this.getNodeParameter('listId', i) as string; + responseData = await affinityApiRequest.call(this, 'GET', `/lists/${listId}`, {}, qs); + } + //https://api-docs.affinity.co/#get-all-lists + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + responseData = await affinityApiRequest.call(this, 'GET', `/lists`, {}, qs); + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } + if (resource === 'listEntry') { + //https://api-docs.affinity.co/#create-a-new-list-entry + if (operation === 'create') { + const listId = this.getNodeParameter('listId', i) as string; + const entityId = this.getNodeParameter('entityId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + entity_id: parseInt(entityId, 10), + }; + Object.assign(body, additionalFields); + responseData = await affinityApiRequest.call(this, 'POST', `/lists/${listId}/list-entries`, body); + } + //https://api-docs.affinity.co/#get-a-specific-list-entry + if (operation === 'get') { + const listId = this.getNodeParameter('listId', i) as string; + const listEntryId = this.getNodeParameter('listEntryId', i) as string; + responseData = await affinityApiRequest.call(this, 'GET', `/lists/${listId}/list-entries/${listEntryId}`, {}, qs); + } + //https://api-docs.affinity.co/#get-all-list-entries + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const listId = this.getNodeParameter('listId', i) as string; + if (returnAll === true) { + responseData = await affinityApiRequestAllItems.call(this, 'list_entries', 'GET', `/lists/${listId}/list-entries`, {}, qs); + } else { + qs.page_size = this.getNodeParameter('limit', i) as number; + responseData = await affinityApiRequest.call(this, 'GET', `/lists/${listId}/list-entries`, {}, qs); + responseData = responseData.list_entries; + } + } + //https://api-docs.affinity.co/#delete-a-specific-list-entry + if (operation === 'delete') { + const listId = this.getNodeParameter('listId', i) as string; + const listEntryId = this.getNodeParameter('listEntryId', i) as string; + responseData = await affinityApiRequest.call(this, 'DELETE', `/lists/${listId}/list-entries/${listEntryId}`, {}, qs); + } + } if (resource === 'person') { //https://api-docs.affinity.co/#create-a-new-person if (operation === 'create') { @@ -166,7 +258,7 @@ export class Affinity implements INodeType { if (options.withInteractionDates) { qs.with_interaction_dates = options.withInteractionDates as boolean; } - responseData = await affinityApiRequest.call(this,'GET', `/persons/${personId}`, {}, qs); + responseData = await affinityApiRequest.call(this, 'GET', `/persons/${personId}`, {}, qs); } //https://api-docs.affinity.co/#search-for-persons if (operation === 'getAll') { @@ -230,7 +322,7 @@ export class Affinity implements INodeType { if (options.withInteractionDates) { qs.with_interaction_dates = options.withInteractionDates as boolean; } - responseData = await affinityApiRequest.call(this,'GET', `/organizations/${organizationId}`, {}, qs); + responseData = await affinityApiRequest.call(this, 'GET', `/organizations/${organizationId}`, {}, qs); } //https://api-docs.affinity.co/#search-for-organizations if (operation === 'getAll') { diff --git a/packages/nodes-base/nodes/Affinity/GenericFunctions.ts b/packages/nodes-base/nodes/Affinity/GenericFunctions.ts index 1d3ca50db4..cabc297e65 100644 --- a/packages/nodes-base/nodes/Affinity/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Affinity/GenericFunctions.ts @@ -1,4 +1,6 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; import { BINARY_ENCODING, @@ -6,7 +8,11 @@ import { ILoadOptionsFunctions, } from 'n8n-core'; -import { IDataObject, IHookFunctions, IWebhookFunctions } from 'n8n-workflow'; +import { + IDataObject, + IHookFunctions, + IWebhookFunctions, +} from 'n8n-workflow'; export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any @@ -43,7 +49,7 @@ export async function affinityApiRequest(this: IExecuteFunctions | IWebhookFunct } catch (error) { if (error.response) { const errorMessage = error.response.body.message || error.response.body.description || error.message; - throw new Error(`Affinity error response: ${errorMessage}`); + throw new Error(`Affinity error response [${error.statusCode}]: ${errorMessage}`); } throw error; } diff --git a/packages/nodes-base/nodes/Affinity/ListDescription.ts b/packages/nodes-base/nodes/Affinity/ListDescription.ts new file mode 100644 index 0000000000..f284ffab33 --- /dev/null +++ b/packages/nodes-base/nodes/Affinity/ListDescription.ts @@ -0,0 +1,100 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const listOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'list', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a list', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all lists', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const listFields = [ + /* -------------------------------------------------------------------------- */ + /* list:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'list', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The unique id of the list object to be retrieved.', + }, + /* -------------------------------------------------------------------------- */ + /* list:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'list', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'list', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Affinity/ListEntryDescription.ts b/packages/nodes-base/nodes/Affinity/ListEntryDescription.ts new file mode 100644 index 0000000000..04eae3a2e1 --- /dev/null +++ b/packages/nodes-base/nodes/Affinity/ListEntryDescription.ts @@ -0,0 +1,263 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const listEntryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a list entry', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a list entry', + }, + { + name: 'Get', + value: 'get', + description: 'Get a list entry', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all list entries', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const listEntryFields = [ + + /* -------------------------------------------------------------------------- */ + /* listEntry:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getLists', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The unique id of the list whose list entries are to be retrieved.', + }, + { + displayName: 'Entity ID', + name: 'entityId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The unique id of the entity (person, organization, or opportunity) to add to this list.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creator_id', + type: 'string', + default: '', + description: `The id of a Person resource who should be recorded as adding the entry to the list.
+ Must be a person who can access Affinity. If not provided the creator defaults to the owner of the API key.`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* listEntry:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getLists', + }, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The unique id of the list that contains the specified list_entry_id.', + }, + { + displayName: 'List Entry ID', + name: 'listEntryId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The unique id of the list entry object to be retrieved.', + }, + /* -------------------------------------------------------------------------- */ + /* listEntry:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'The unique id of the list whose list entries are to be retrieved.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 10, + }, + default: 5, + description: 'How many results to return.', + }, + /* -------------------------------------------------------------------------- */ + /* listEntry:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'List ID', + name: 'listId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The unique id of the list that contains the specified list_entry_id.', + }, + { + displayName: 'List Entry ID', + name: 'listEntryId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'listEntry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The unique id of the list entry object to be deleted.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Affinity/OrganizationDescription.ts b/packages/nodes-base/nodes/Affinity/OrganizationDescription.ts index 36d27ce9b4..dd591c63c5 100644 --- a/packages/nodes-base/nodes/Affinity/OrganizationDescription.ts +++ b/packages/nodes-base/nodes/Affinity/OrganizationDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; export const organizationOperations = [ { @@ -46,9 +48,9 @@ export const organizationOperations = [ export const organizationFields = [ -/* -------------------------------------------------------------------------- */ -/* organization:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* organization:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Name', name: 'name', @@ -114,9 +116,9 @@ export const organizationFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* organization:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* organization:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Organization ID', name: 'organizationId', @@ -178,9 +180,9 @@ export const organizationFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* organization:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* organization:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Organization ID', name: 'organizationId', @@ -225,9 +227,9 @@ export const organizationFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* organization:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* organization:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -302,9 +304,9 @@ export const organizationFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* organization:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* organization:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Organization ID', name: 'organizationId', diff --git a/packages/nodes-base/nodes/Affinity/PersonDescription.ts b/packages/nodes-base/nodes/Affinity/PersonDescription.ts index 99e493c788..c97634a8a3 100644 --- a/packages/nodes-base/nodes/Affinity/PersonDescription.ts +++ b/packages/nodes-base/nodes/Affinity/PersonDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; export const personOperations = [ { @@ -46,9 +48,9 @@ export const personOperations = [ export const personFields = [ -/* -------------------------------------------------------------------------- */ -/* person:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* person:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Fist Name', name: 'firstName', @@ -136,9 +138,9 @@ export const personFields = [ placeholder: 'info@example.com', default: [], }, -/* -------------------------------------------------------------------------- */ -/* person:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* person:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Person ID', name: 'personId', @@ -222,9 +224,9 @@ export const personFields = [ placeholder: 'info@example.com', default: [], }, -/* -------------------------------------------------------------------------- */ -/* person:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* person:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Person ID', name: 'personId', @@ -269,9 +271,9 @@ export const personFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* person:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* person:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -346,9 +348,9 @@ export const personFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* person:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* person:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Person ID', name: 'personId', diff --git a/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts b/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts index 6cb1d3a6c9..abba82b256 100644 --- a/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts +++ b/packages/nodes-base/nodes/Airtable/AirtableTrigger.node.ts @@ -11,6 +11,7 @@ import { import { apiRequestAllItems, + downloadRecordAttachments, } from './GenericFunctions'; import * as moment from 'moment'; @@ -64,6 +65,28 @@ export class AirtableTrigger implements INodeType { because without this field trigger will not work correctly.`, required: true, }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + description: `When set to true the attachment fields define in 'Download Fields' will be downloaded.`, + }, + { + displayName: 'Download Fields', + name: 'downloadFieldNames', + type: 'string', + required: true, + displayOptions: { + show: { + downloadAttachments: [ + true, + ], + }, + }, + default: '', + description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -100,6 +123,7 @@ export class AirtableTrigger implements INodeType { }; async poll(this: IPollFunctions): Promise { + const downloadAttachments = this.getNodeParameter('downloadAttachments', 0) as boolean; const webhookData = this.getWorkflowStaticData('node'); @@ -149,6 +173,12 @@ export class AirtableTrigger implements INodeType { throw new Error(`The Field "${triggerField}" does not exist.`); } + if (downloadAttachments === true) { + const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', 0) as string).split(','); + const data = await downloadRecordAttachments.call(this, records, downloadFieldNames); + return [data]; + } + return [this.helpers.returnJsonArray(records)]; } diff --git a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts index 09be7c40bd..d13b3a5723 100644 --- a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts @@ -130,7 +130,7 @@ export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunction }; } -export async function downloadRecordAttachments(this: IExecuteFunctions, records: IRecord[], fieldNames: string[]): Promise { +export async function downloadRecordAttachments(this: IExecuteFunctions | IPollFunctions, records: IRecord[], fieldNames: string[]): Promise { const elements: INodeExecutionData[] = []; for (const record of records) { const element: INodeExecutionData = { json: {}, binary: {} }; diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index 92e6f75fa1..e0fed24e1b 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -106,6 +106,10 @@ export class Asana implements INodeType { name: 'Task Tag', value: 'taskTag', }, + { + name: 'Task Project', + value: 'taskProject', + }, { name: 'User', value: 'user', @@ -921,6 +925,16 @@ export class Asana implements INodeType { default: '', description: 'The task notes', }, + { + displayName: 'Project IDs', + name: 'projects', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + default: [], + description: 'The project to filter tasks on.', + }, ], }, @@ -1093,6 +1107,161 @@ export class Asana implements INodeType { description: 'The ID of the comment to be removed', }, + // ---------------------------------- + // taskProject + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'taskProject', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a task to a project', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a task from a project', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, + // ---------------------------------- + // taskProject:add + // ---------------------------------- + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskProject', + ], + }, + }, + description: 'The ID of the task to add the project to', + }, + { + displayName: 'Project ID', + name: 'project', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskProject', + ], + }, + }, + description: 'The project where the task will be added', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'taskProject', + ], + operation: [ + 'add', + ], + }, + }, + default: {}, + description: 'Other properties to set', + placeholder: 'Add Field', + options: [ + { + displayName: 'Insert After', + name: 'insert_after', + type: 'string', + default: '', + description: 'A task in the project to insert the task after, or null to insert at the beginning of the list.', + }, + { + displayName: 'Insert Before', + name: 'insert_before', + type: 'string', + default: '', + description: 'A task in the project to insert the task before, or null to insert at the end of the list.', + }, + { + displayName: 'Section', + name: 'section', + type: 'string', + default: '', + description: 'A section in the project to insert the task into. The task will be inserted at the bottom of the section.', + }, + ], + }, + + // ---------------------------------- + // taskProject:remove + // ---------------------------------- + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'taskProject', + ], + }, + }, + description: 'The ID of the task to add the project to', + }, + { + displayName: 'Project ID', + name: 'project', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'taskProject', + ], + }, + }, + description: 'The project where the task will be removed from', + }, // ---------------------------------- // taskTag // ---------------------------------- @@ -1952,7 +2121,49 @@ export class Asana implements INodeType { responseData = { success: true }; } } + if (resource === 'taskProject') { + if (operation === 'add') { + // ---------------------------------- + // taskProject:add + // ---------------------------------- + + const taskId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + requestMethod = 'POST'; + + endpoint = `/tasks/${taskId}/addProject`; + + body.project = this.getNodeParameter('project', i) as string; + + Object.assign(body, additionalFields); + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + + if (operation === 'remove') { + + // ---------------------------------- + // taskProject:remove + // ---------------------------------- + + const taskId = this.getNodeParameter('id', i) as string; + + requestMethod = 'POST'; + + endpoint = `/tasks/${taskId}/removeProject`; + + body.project = this.getNodeParameter('project', i) as string; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + } if (resource === 'user') { if (operation === 'get') { // ---------------------------------- diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index db0b978a04..c480beb078 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -76,7 +76,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | } } -export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; @@ -95,12 +95,12 @@ export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOpt return returnData; } -export async function getWorkspaces(this: ILoadOptionsFunctions): Promise < INodePropertyOptions[] > { +export async function getWorkspaces(this: ILoadOptionsFunctions): Promise { const endpoint = '/workspaces'; const responseData = await asanaApiRequestAllItems.call(this, 'GET', endpoint, {}); const returnData: INodePropertyOptions[] = []; - for(const workspaceData of responseData) { + for (const workspaceData of responseData) { if (workspaceData.resource_type !== 'workspace') { // Not sure if for some reason also ever other resources // get returned but just in case filter them out @@ -113,7 +113,7 @@ export async function getWorkspaces(this: ILoadOptionsFunctions): Promise < INod }); } - returnData.sort((a, b) => { + returnData.sort((a, b) => { if (a.name < b.name) { return -1; } if (a.name > b.name) { return 1; } return 0; diff --git a/packages/nodes-base/nodes/Compression.node.json b/packages/nodes-base/nodes/Compression.node.json new file mode 100644 index 0000000000..c7de9ba61b --- /dev/null +++ b/packages/nodes-base/nodes/Compression.node.json @@ -0,0 +1,16 @@ +{ + "node": "n8n-nodes-base.compression", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Core Nodes", + "Data & Storage" + ], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.compression/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Discourse/CategoryDescription.ts b/packages/nodes-base/nodes/Discourse/CategoryDescription.ts new file mode 100644 index 0000000000..71c8de21e6 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/CategoryDescription.ts @@ -0,0 +1,216 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const categoryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a category', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all categories', + }, + { + name: 'Update', + value: 'update', + description: 'Update a category', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const categoryFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* category:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Name of the category.', + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '0000FF', + description: 'Color of the category.', + }, + { + displayName: 'Text Color', + name: 'textColor', + type: 'color', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'create', + ], + }, + }, + default: '0000FF', + description: 'Text color of the category.', + }, + + /* -------------------------------------------------------------------------- */ + /* category:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* category:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Category ID', + name: 'categoryId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the category.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'New name of the category.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'category', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '0000FF', + description: 'Color of the category', + }, + { + displayName: 'Text Color', + name: 'textColor', + type: 'color', + default: '0000FF', + description: 'Text color of the category', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/Discourse.node.json b/packages/nodes-base/nodes/Discourse/Discourse.node.json new file mode 100644 index 0000000000..1ad6d7b1b0 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/Discourse.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.discourse", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/discourse" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.discourse/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Discourse/Discourse.node.ts b/packages/nodes-base/nodes/Discourse/Discourse.node.ts new file mode 100644 index 0000000000..78f68374f5 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/Discourse.node.ts @@ -0,0 +1,500 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + discourseApiRequest, +} from './GenericFunctions'; + +import { + postFields, + postOperations, +} from './PostDescription'; + +import { + categoryFields, + categoryOperations, +} from './CategoryDescription'; + +import { + groupFields, + groupOperations, +} from './GroupDescription'; + +// import { +// searchFields, +// searchOperations, +// } from './SearchDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +import { + userGroupFields, + userGroupOperations, +} from './UserGroupDescription'; + +//import * as moment from 'moment'; + +export class Discourse implements INodeType { + description: INodeTypeDescription = { + displayName: 'Discourse', + name: 'discourse', + icon: 'file:discourse.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Discourse API.', + defaults: { + name: 'Discourse', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'discourseApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Category', + value: 'category', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Post', + value: 'post', + }, + // { + // name: 'Search', + // value: 'search', + // }, + { + name: 'User', + value: 'user', + }, + { + name: 'User Group', + value: 'userGroup', + }, + ], + default: 'post', + description: 'The resource to operate on.', + }, + ...categoryOperations, + ...categoryFields, + ...groupOperations, + ...groupFields, + ...postOperations, + ...postFields, + // ...searchOperations, + // ...searchFields, + ...userOperations, + ...userFields, + ...userGroupOperations, + ...userGroupFields, + ], + }; + + methods = { + loadOptions: { + // Get all the calendars to display them to user so that he can + // select them easily + async getCategories( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const { category_list } = await discourseApiRequest.call( + this, + 'GET', + `/categories.json`, + ); + for (const category of category_list.categories) { + returnData.push({ + name: category.name, + value: category.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'category') { + //https://docs.discourse.org/#tag/Categories/paths/~1categories.json/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const color = this.getNodeParameter('color', i) as string; + const textColor = this.getNodeParameter('textColor', i) as string; + + const body: IDataObject = { + name, + color, + text_color: textColor, + }; + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/categories.json`, + body, + ); + + responseData = responseData.category; + } + //https://docs.discourse.org/#tag/Categories/paths/~1categories.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/categories.json`, + {}, + qs, + ); + + responseData = responseData.category_list.categories; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Categories/paths/~1categories~1{id}/put + if (operation === 'update') { + const categoryId = this.getNodeParameter('categoryId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + name, + }; + + Object.assign(body, updateFields); + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/categories/${categoryId}.json`, + body, + ); + + responseData = responseData.category; + } + } + if (resource === 'group') { + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/admin/groups.json`, + { group: body }, + ); + + responseData = responseData.basic_group; + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{name}.json/get + if (operation === 'get') { + const name = this.getNodeParameter('name', i) as string; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/groups/${name}`, + {}, + qs, + ); + + responseData = responseData.group; + + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/groups.json`, + {}, + qs, + ); + + responseData = responseData.groups; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/put + if (operation === 'update') { + const groupId = this.getNodeParameter('groupId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/groups/${groupId}.json`, + { group: body }, + ); + } + } + if (resource === 'post') { + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/post + if (operation === 'create') { + const content = this.getNodeParameter('content', i) as string; + const title = this.getNodeParameter('title', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + title, + raw: content, + }; + + Object.assign(body, additionalFields); + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/posts.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/get + if (operation === 'get') { + const postId = this.getNodeParameter('postId', i) as string; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/posts/${postId}`, + {}, + qs, + ); + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts.json/get + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/posts.json`, + {}, + qs, + ); + + responseData = responseData.latest_posts; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://docs.discourse.org/#tag/Posts/paths/~1posts~1{id}.json/put + if (operation === 'update') { + const postId = this.getNodeParameter('postId', i) as string; + + const content = this.getNodeParameter('content', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = { + raw: content, + }; + + Object.assign(body, updateFields); + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/posts/${postId}.json`, + body, + ); + + responseData = responseData.post; + } + } + // TODO figure how to paginate the results + // if (resource === 'search') { + // //https://docs.discourse.org/#tag/Search/paths/~1search~1query/get + // if (operation === 'query') { + // qs.term = this.getNodeParameter('term', i) as string; + + // const simple = this.getNodeParameter('simple', i) as boolean; + + // const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + // Object.assign(qs, updateFields); + + // qs.page = 1; + + // responseData = await discourseApiRequest.call( + // this, + // 'GET', + // `/search/query`, + // {}, + // qs, + // ); + + // if (simple === true) { + // const response = []; + // for (const key of Object.keys(responseData)) { + // console.log(key) + // for (const data of responseData[key]) { + // response.push(Object.assign(data, { __type: key })); + // } + // } + // responseData = response; + // } + // } + // } + if (resource === 'user') { + //https://docs.discourse.org/#tag/Users/paths/~1users/post + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const email = this.getNodeParameter('email', i) as string; + const password = this.getNodeParameter('password', i) as string; + const username = this.getNodeParameter('username', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name, + password, + email, + username, + }; + + Object.assign(body, additionalFields); + + responseData = await discourseApiRequest.call( + this, + 'POST', + `/users.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Users/paths/~1users~1{username}.json/get + if (operation === 'get') { + const by = this.getNodeParameter('by', i) as string; + let endpoint = ''; + if (by === 'username') { + const username = this.getNodeParameter('username', i) as string; + endpoint = `/users/${username}`; + } else if (by === 'externalId') { + const externalId = this.getNodeParameter('externalId', i) as string; + endpoint = `/u/by-external/${externalId}.json`; + } + + responseData = await discourseApiRequest.call( + this, + 'GET', + endpoint, + ); + } + //https://docs.discourse.org/#tag/Users/paths/~1admin~1users~1{id}.json/delete + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const flag = this.getNodeParameter('flag', i) as boolean; + + responseData = await discourseApiRequest.call( + this, + 'GET', + `/admin/users/list/${flag}.json`, + {}, + qs, + ); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + } + if (resource === 'userGroup') { + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{group_id}~1members.json/put + if (operation === 'add') { + const usernames = this.getNodeParameter('usernames', i) as string; + const groupId = this.getNodeParameter('groupId', i) as string; + const body: IDataObject = { + usernames, + }; + + responseData = await discourseApiRequest.call( + this, + 'PUT', + `/groups/${groupId}/members.json`, + body, + ); + } + //https://docs.discourse.org/#tag/Groups/paths/~1groups~1{group_id}~1members.json/delete + if (operation === 'remove') { + const usernames = this.getNodeParameter('usernames', i) as string; + const groupId = this.getNodeParameter('groupId', i) as string; + const body: IDataObject = { + usernames, + }; + + responseData = await discourseApiRequest.call( + this, + 'DELETE', + `/groups/${groupId}/members.json`, + body, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Discourse/GenericFunctions.ts b/packages/nodes-base/nodes/Discourse/GenericFunctions.ts new file mode 100644 index 0000000000..0a8057bd08 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function discourseApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('discourseApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + 'Api-Username': credentials.username, + }, + method, + body, + qs, + uri: `${credentials.url}${path}`, + json: true, + }; + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.errors) { + + const errors = error.response.body.errors; + // Try to return the error prettier + throw new Error( + `Discourse error response [${error.statusCode}]: ${errors.join('|')}`, + ); + } + throw error; + } +} + +export async function discourseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + do { + responseData = await discourseApiRequest.call(this, method, endpoint, body, query); + returnData.push.apply(returnData, responseData); + query.page++; + } while ( + responseData.length !== 0 + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Discourse/GroupDescription.ts b/packages/nodes-base/nodes/Discourse/GroupDescription.ts new file mode 100644 index 0000000000..45448d89c0 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/GroupDescription.ts @@ -0,0 +1,153 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const groupOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a group', + }, + { + name: 'Get', + value: 'get', + description: 'Get a group', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all groups', + }, + { + name: 'Update', + value: 'update', + description: 'Update a group', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const groupFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* group:create & get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'get', + 'create', + ], + }, + }, + default: '', + description: 'Name of the group.', + }, + + /* -------------------------------------------------------------------------- */ + /* group:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + + /* -------------------------------------------------------------------------- */ + /* group:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the group to update.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'group', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'New name of the group.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/PostDescription.ts b/packages/nodes-base/nodes/Discourse/PostDescription.ts new file mode 100644 index 0000000000..858ce64ac6 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/PostDescription.ts @@ -0,0 +1,270 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a post', + }, + { + name: 'Get', + value: 'get', + description: 'Get a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all posts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a post', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const postFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* post:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Title of the post.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Content of the post.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'post', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Category ID', + name: 'category', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCategories', + }, + default: '', + description: 'ID of the category', + }, + { + displayName: 'Reply To Post Number', + name: 'reply_to_post_number', + type: 'string', + default: '', + description: 'The number of the post to reply to', + }, + { + displayName: 'Topic ID', + name: 'topic_id', + type: 'string', + default: '', + description: 'ID of the topic', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* post:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'ID of the post.', + }, + + /* -------------------------------------------------------------------------- */ + /* post:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + /* -------------------------------------------------------------------------- */ + /* post:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'ID of the post.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Content of the post. HTML is supported.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Edit Reason', + name: 'edit_reason', + type: 'string', + default: '', + }, + { + displayName: 'Cooked', + name: 'cooked', + type: 'boolean', + default: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/SearchDescription.ts b/packages/nodes-base/nodes/Discourse/SearchDescription.ts new file mode 100644 index 0000000000..f9bc7b9756 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/SearchDescription.ts @@ -0,0 +1,69 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const searchOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'search', + ], + }, + }, + options: [ + { + name: 'Query', + value: 'query', + description: 'Search for something', + }, + ], + default: 'query', + }, +] as INodeProperties[]; + +export const searchFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* search:query */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Term', + name: 'term', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + default: '', + description: 'Term to search for.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/UserDescription.ts b/packages/nodes-base/nodes/Discourse/UserDescription.ts new file mode 100644 index 0000000000..3c32eef7a3 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/UserDescription.ts @@ -0,0 +1,308 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a user', + }, + { + name: 'Get', + value: 'get', + description: 'Get a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const userFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* user:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Name of the user to create.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Email of the user to create.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The username of the user to create.`, + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { + password: true, + }, + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The password of the user to create.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Active', + name: 'active', + type: 'boolean', + default: false, + }, + { + displayName: 'Approved', + name: 'approved', + type: 'boolean', + default: false, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* user:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'By', + name: 'by', + type: 'options', + options: [ + { + name: 'Username', + value: 'username', + }, + { + name: 'SSO External ID', + value: 'externalId', + }, + ], + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + default: 'username', + description: 'What to search by.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + by: [ + 'username', + ], + }, + }, + default: '', + description: `The username of the user to return.`, + }, + { + displayName: 'SSO External ID', + name: 'externalId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + by: [ + 'externalId', + ], + }, + }, + default: '', + description: `Discourse SSO external ID.`, + }, + + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Flag', + name: 'flag', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Blocked', + value: 'blocked', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Staff', + value: 'staff', + }, + { + name: 'Suspect', + value: 'suspect', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: `User flags to search for.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +]; diff --git a/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts b/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts new file mode 100644 index 0000000000..cdd4fd0a77 --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/UserGroupDescription.ts @@ -0,0 +1,116 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userGroupOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + description: 'Choose an operation', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Create a user to group', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove user from group', + }, + ], + default: 'add', + }, +] as INodeProperties[]; + +export const userGroupFields: INodeProperties[] = [ + /* -------------------------------------------------------------------------- */ + /* userGroup:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Usernames', + name: 'usernames', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'Usernames to add to group. Multiples can be defined separated by comma', + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'ID of the group.', + }, + + /* -------------------------------------------------------------------------- */ + /* userGroup:remove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Usernames', + name: 'usernames', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'Usernames to remove from group. Multiples can be defined separated by comma.', + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'userGroup', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'ID of the group to remove.', + }, + +]; diff --git a/packages/nodes-base/nodes/Discourse/discourse.svg b/packages/nodes-base/nodes/Discourse/discourse.svg new file mode 100644 index 0000000000..73e7d63ece --- /dev/null +++ b/packages/nodes-base/nodes/Discourse/discourse.svg @@ -0,0 +1 @@ +Discourse_logo \ No newline at end of file diff --git a/packages/nodes-base/nodes/Hubspot/CompanyDescription.ts b/packages/nodes-base/nodes/Hubspot/CompanyDescription.ts index e319a0e38c..0eefd20ec1 100644 --- a/packages/nodes-base/nodes/Hubspot/CompanyDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/CompanyDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export const companyOperations = [ { @@ -63,9 +63,9 @@ export const companyOperations = [ export const companyFields = [ -/* -------------------------------------------------------------------------- */ -/* company:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* company:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Name', name: 'name', @@ -157,6 +157,41 @@ export const companyFields = [ default: '', description: 'The country/region in which the company or organization is located.', }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCompanyCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + }, { displayName: 'Description', name: 'description', @@ -370,7 +405,7 @@ export const companyFields = [ description: 'The main website of the company or organization. This property is used to identify unique companies. Powered by HubSpot Insights.', }, { - displayName: 'Year Founded', + displayName: 'Year Founded', name: 'yearFounded', type: 'string', default: '', @@ -378,9 +413,10 @@ export const companyFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* company:update */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Company ID', name: 'companyId', @@ -473,6 +509,41 @@ export const companyFields = [ default: '', description: 'The country/region in which the company or organization is located.', }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCompanyCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + }, { displayName: 'Description', name: 'description', @@ -692,7 +763,7 @@ export const companyFields = [ description: 'The main website of the company or organization. This property is used to identify unique companies. Powered by HubSpot Insights.', }, { - displayName: 'Year Founded', + displayName: 'Year Founded', name: 'yearFounded', type: 'string', default: '', @@ -700,9 +771,10 @@ export const companyFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* company:get */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Company ID', name: 'companyId', @@ -747,9 +819,10 @@ export const companyFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* company:getAll */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -838,9 +911,10 @@ export const companyFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* company:delete */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Company ID', name: 'companyId', @@ -859,9 +933,10 @@ export const companyFields = [ default: '', description: 'Unique identifier for a particular company', }, -/* -------------------------------------------------------------------------- */ -/* company:getRecentlyCreated company:getRecentlyModifie */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:getRecentlyCreated company:getRecentlyModifie */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -939,9 +1014,10 @@ export const companyFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* company:searchByDomain */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* company:searchByDomain */ + /* -------------------------------------------------------------------------- */ { displayName: 'Domain', name: 'domain', diff --git a/packages/nodes-base/nodes/Hubspot/CompanyInterface.ts b/packages/nodes-base/nodes/Hubspot/CompanyInterface.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts index 1191cca13e..dbb2be43ed 100644 --- a/packages/nodes-base/nodes/Hubspot/ContactDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/ContactDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export const contactOperations = [ { @@ -53,9 +53,9 @@ export const contactOperations = [ export const contactFields = [ -/* -------------------------------------------------------------------------- */ -/* contact:upsert */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* contact:upsert */ + /* -------------------------------------------------------------------------- */ { displayName: 'Email', name: 'email', @@ -121,7 +121,7 @@ export const contactFields = [ name: 'associatedCompanyId', type: 'options', typeOptions: { - loadOptionsMethod:'getCompanies' , + loadOptionsMethod: 'getCompanies', }, default: '', description: 'Companies associated with the ticket', @@ -501,9 +501,10 @@ export const contactFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* contact:get */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Contact ID', name: 'contactId', @@ -603,9 +604,10 @@ export const contactFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* contact:getAll */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -728,9 +730,10 @@ export const contactFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* contact:delete */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Contact ID', name: 'contactId', @@ -749,9 +752,10 @@ export const contactFields = [ default: '', description: 'Unique identifier for a particular contact', }, -/* -------------------------------------------------------------------------- */ -/* contact:getRecentlyCreatedUpdated */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* contact:getRecentlyCreatedUpdated */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -875,9 +879,9 @@ export const contactFields = [ ], }, -//*-------------------------------------------------------------------------- */ -/* contact:search */ -/* -------------------------------------------------------------------------- */ + //*-------------------------------------------------------------------------- */ + /* contact:search */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', diff --git a/packages/nodes-base/nodes/Hubspot/ContactListDescription.ts b/packages/nodes-base/nodes/Hubspot/ContactListDescription.ts index 071a0ff1c5..f0746e94fc 100644 --- a/packages/nodes-base/nodes/Hubspot/ContactListDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/ContactListDescription.ts @@ -120,6 +120,7 @@ export const contactListFields = [ }, default: '', }, + /* -------------------------------------------------------------------------- */ /* contactList:remove */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Hubspot/DealInterface.ts b/packages/nodes-base/nodes/Hubspot/DealInterface.ts index fe422ff5be..2bd20ddb1b 100644 --- a/packages/nodes-base/nodes/Hubspot/DealInterface.ts +++ b/packages/nodes-base/nodes/Hubspot/DealInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export interface IAssociation { associatedCompanyIds?: number[]; diff --git a/packages/nodes-base/nodes/Hubspot/FormDescription.ts b/packages/nodes-base/nodes/Hubspot/FormDescription.ts index 15dd2592af..684dfad4c4 100644 --- a/packages/nodes-base/nodes/Hubspot/FormDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/FormDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export const formOperations = [ { @@ -33,9 +33,9 @@ export const formOperations = [ export const formFields = [ -/* -------------------------------------------------------------------------- */ -/* form:submit */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* form:submit */ + /* -------------------------------------------------------------------------- */ { displayName: 'Form', name: 'formId', @@ -301,9 +301,10 @@ export const formFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* form:getFields */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* form:getFields */ + /* -------------------------------------------------------------------------- */ { displayName: 'Form', name: 'formId', diff --git a/packages/nodes-base/nodes/Hubspot/FormInterface.ts b/packages/nodes-base/nodes/Hubspot/FormInterface.ts index 98edbf67ff..f6dd2e9e3d 100644 --- a/packages/nodes-base/nodes/Hubspot/FormInterface.ts +++ b/packages/nodes-base/nodes/Hubspot/FormInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export interface IContext { goToWebinarWebinarKey?: string; diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index 53953d9856..a5a05a562d 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -41,7 +41,6 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions const credentials = this.getCredentials('hubspotDeveloperApi'); options.qs.hapikey = credentials!.apiKey as string; - return await this.helpers.request!(options); } else { // @ts-ignore @@ -112,3 +111,1790 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } + +export const propertyEvents = [ + 'contact.propertyChange', + 'company.propertyChange', + 'deal.propertyChange', +]; + +export const contactFields = [ + { + 'id': 'company_size', + 'label': 'testingricardo', + }, + { + 'id': 'date', + 'label': 'Date', + }, + { + 'id': 'date_of_birth', + 'label': 'Date of birth', + }, + { + 'id': 'days_to_close', + 'label': 'Days To Close', + }, + { + 'id': 'degree', + 'label': 'Degree', + }, + { + 'id': 'field_of_study', + 'label': 'Field of study', + }, + { + 'id': 'first_conversion_date', + 'label': 'First Conversion Date', + }, + { + 'id': 'first_conversion_event_name', + 'label': 'First Conversion', + }, + { + 'id': 'first_deal_created_date', + 'label': 'First Deal Created Date', + }, + { + 'id': 'gender', + 'label': 'Gender', + }, + { + 'id': 'graduation_date', + 'label': 'Graduation date', + }, + { + 'id': 'hs_additional_emails', + 'label': 'Additional email addresses', + }, + { + 'id': 'hs_all_contact_vids', + 'label': 'All vids for a contact', + }, + { + 'id': 'hs_analytics_first_touch_converting_campaign', + 'label': 'First Touch Converting Campaign', + }, + { + 'id': 'hs_analytics_last_touch_converting_campaign', + 'label': 'Last Touch Converting Campaign', + }, + { + 'id': 'hs_avatar_filemanager_key', + 'label': 'Avatar FileManager key', + }, + { + 'id': 'hs_buying_role', + 'label': 'Buying Role', + }, + { + 'id': 'hs_calculated_form_submissions', + 'label': 'All form submissions for a contact', + }, + { + 'id': 'hs_calculated_merged_vids', + 'label': 'Merged vids with timestamps of a contact', + }, + { + 'id': 'hs_calculated_mobile_number', + 'label': 'Calculated Mobile Number in International Format', + }, + { + 'id': 'hs_calculated_phone_number', + 'label': 'Calculated Phone Number in International Format', + }, + { + 'id': 'hs_calculated_phone_number_area_code', + 'label': 'Calculated Phone Number Area Code', + }, + { + 'id': 'hs_calculated_phone_number_country_code', + 'label': 'Calculated Phone Number Country Code', + }, + { + 'id': 'hs_calculated_phone_number_region_code', + 'label': 'Calculated Phone Number Region', + }, + { + 'id': 'hs_content_membership_email_confirmed', + 'label': 'Email Confirmed', + }, + { + 'id': 'hs_content_membership_notes', + 'label': 'Membership Notes', + }, + { + 'id': 'hs_content_membership_registered_at', + 'label': 'Registered At', + }, + { + 'id': 'hs_content_membership_registration_domain_sent_to', + 'label': 'Domain to which registration email was sent', + }, + { + 'id': 'hs_content_membership_registration_email_sent_at', + 'label': 'Time registration email was sent', + }, + { + 'id': 'hs_content_membership_status', + 'label': 'Status', + }, + { + 'id': 'hs_conversations_visitor_email', + 'label': 'Conversations visitor email', + }, + { + 'id': 'hs_count_is_unworked', + 'label': 'Count of unengaged contacts', + }, + { + 'id': 'hs_count_is_worked', + 'label': 'Count of engaged contacts', + }, + { + 'id': 'hs_created_by_conversations', + 'label': 'Created By Conversations', + }, + { + 'id': 'hs_created_by_user_id', + 'label': 'Created by user ID', + }, + { + 'id': 'hs_createdate', + 'label': 'Object create date/time', + }, + { + 'id': 'hs_document_last_revisited', + 'label': 'Recent Document Revisit Date', + }, + { + 'id': 'hs_email_bad_address', + 'label': 'Invalid email address', + }, + { + 'id': 'hs_email_customer_quarantined_reason', + 'label': 'Email address quarantine reason', + }, + { + 'id': 'hs_email_domain', + 'label': 'Email Domain', + }, + { + 'id': 'hs_email_hard_bounce_reason', + 'label': 'Email hard bounce reason', + }, + { + 'id': 'hs_email_hard_bounce_reason_enum', + 'label': 'Email hard bounce reason', + }, + { + 'id': 'hs_email_quarantined', + 'label': 'Email Address Quarantined', + }, + { + 'id': 'hs_email_quarantined_reason', + 'label': 'Email address internal quarantine reason', + }, + { + 'id': 'hs_email_recipient_fatigue_recovery_time', + 'label': 'Email Address Recipient Fatigue Next Available Sending Time', + }, + { + 'id': 'hs_email_sends_since_last_engagement', + 'label': 'Sends Since Last Engagement', + }, + { + 'id': 'hs_emailconfirmationstatus', + 'label': 'Marketing email confirmation status', + }, + { + 'id': 'hs_facebook_ad_clicked', + 'label': 'Clicked Facebook ad', + }, + { + 'id': 'hs_facebook_click_id', + 'label': 'Facebook click id', + }, + { + 'id': 'hs_facebookid', + 'label': 'Facebook ID', + }, + { + 'id': 'hs_feedback_last_nps_follow_up', + 'label': 'Last NPS survey comment', + }, + { + 'id': 'hs_feedback_last_nps_rating', + 'label': 'Last NPS survey rating', + }, + { + 'id': 'hs_feedback_last_survey_date', + 'label': 'Last NPS survey date', + }, + { + 'id': 'hs_feedback_show_nps_web_survey', + 'label': 'Should be shown an NPS web survey', + }, + { + 'id': 'hs_first_engagement_object_id', + 'label': 'ID of first engagement', + }, + { + 'id': 'hs_google_click_id', + 'label': 'Google ad click id', + }, + { + 'id': 'hs_googleplusid', + 'label': 'googleplus ID', + }, + { + 'id': 'hs_ip_timezone', + 'label': 'IP Timezone', + }, + { + 'id': 'hs_is_contact', + 'label': 'Is a contact', + }, + { + 'id': 'hs_is_unworked', + 'label': 'Contact unworked', + }, + { + 'id': 'hs_last_sales_activity_date', + 'label': 'last sales activity date old', + }, + { + 'id': 'hs_last_sales_activity_timestamp', + 'label': 'Last Engagement Date', + }, + { + 'id': 'hs_lastmodifieddate', + 'label': 'Object last modified date/time', + }, + { + 'id': 'hs_lead_status', + 'label': 'Lead Status', + }, + { + 'id': 'hs_legal_basis', + 'label': 'Legal basis for processing contact\'s data', + }, + { + 'id': 'hs_linkedinid', + 'label': 'Linkedin ID', + }, + { + 'id': 'hs_marketable_reason_id', + 'label': 'Marketing contact status source name', + }, + { + 'id': 'hs_marketable_reason_type', + 'label': 'Marketing contact status source type', + }, + { + 'id': 'hs_marketable_status', + 'label': 'Marketing contact status', + }, + { + 'id': 'hs_marketable_until_renewal', + 'label': 'Marketing contact until next update', + }, + { + 'id': 'hs_merged_object_ids', + 'label': 'Merged object IDs', + }, + { + 'id': 'hs_object_id', + 'label': 'Contact ID', + }, + { + 'id': 'hs_predictivecontactscore_v2', + 'label': 'Likelihood to close', + }, + { + 'id': 'hs_predictivescoringtier', + 'label': 'Contact priority', + }, + { + 'id': 'hs_sa_first_engagement_date', + 'label': 'Date of first engagement', + }, + { + 'id': 'hs_sa_first_engagement_descr', + 'label': 'Description of first engagement', + }, + { + 'id': 'hs_sa_first_engagement_object_type', + 'label': 'Type of first engagement', + }, + { + 'id': 'hs_sales_email_last_clicked', + 'label': 'Recent Sales Email Clicked Date', + }, + { + 'id': 'hs_sales_email_last_opened', + 'label': 'Recent Sales Email Opened Date', + }, + { + 'id': 'hs_searchable_calculated_international_mobile_number', + 'label': 'Calculated Mobile Number with country code', + }, + { + 'id': 'hs_searchable_calculated_international_phone_number', + 'label': 'Calculated Phone Number with country code', + }, + { + 'id': 'hs_searchable_calculated_mobile_number', + 'label': 'Calculated Mobile Number without country code', + }, + { + 'id': 'hs_searchable_calculated_phone_number', + 'label': 'Calculated Phone Number without country code', + }, + { + 'id': 'hs_sequences_is_enrolled', + 'label': 'Currently in Sequence', + }, + { + 'id': 'hs_testpurge', + 'label': 'testpurge', + }, + { + 'id': 'hs_testrollback', + 'label': 'testrollback', + }, + { + 'id': 'hs_time_between_contact_creation_and_deal_close', + 'label': 'Time between contact creation and deal close', + }, + { + 'id': 'hs_time_between_contact_creation_and_deal_creation', + 'label': 'Time between contact creation and deal creation', + }, + { + 'id': 'hs_time_to_first_engagement', + 'label': 'Lead response time', + }, + { + 'id': 'hs_time_to_move_from_lead_to_customer', + 'label': 'Time to move from lead to customer', + }, + { + 'id': 'hs_time_to_move_from_marketingqualifiedlead_to_customer', + 'label': 'Time to move from marketing qualified lead to customer', + }, + { + 'id': 'hs_time_to_move_from_opportunity_to_customer', + 'label': 'Time to move from opportunity to customer', + }, + { + 'id': 'hs_time_to_move_from_salesqualifiedlead_to_customer', + 'label': 'Time to move from sales qualified lead to customer', + }, + { + 'id': 'hs_time_to_move_from_subscriber_to_customer', + 'label': 'Time to move from subscriber to customer', + }, + { + 'id': 'hs_twitterid', + 'label': 'Twitter ID', + }, + { + 'id': 'hs_updated_by_user_id', + 'label': 'Updated by user ID', + }, + { + 'id': 'hs_user_ids_of_all_owners', + 'label': 'User IDs of all owners', + }, + { + 'id': 'hubspot_owner_assigneddate', + 'label': 'Owner Assigned Date', + }, + { + 'id': 'ip_city', + 'label': 'IP City', + }, + { + 'id': 'ip_country', + 'label': 'IP Country', + }, + { + 'id': 'ip_country_code', + 'label': 'IP Country Code', + }, + { + 'id': 'ip_latlon', + 'label': 'IP Latitude & Longitude', + }, + { + 'id': 'ip_state', + 'label': 'IP State/Region', + }, + { + 'id': 'ip_state_code', + 'label': 'IP State Code/Region Code', + }, + { + 'id': 'ip_zipcode', + 'label': 'IP Zipcode', + }, + { + 'id': 'job_function', + 'label': 'Job function', + }, + { + 'id': 'lastmodifieddate', + 'label': 'Last Modified Date', + }, + { + 'id': 'marital_status', + 'label': 'Marital Status', + }, + { + 'id': 'military_status', + 'label': 'Military status', + }, + { + 'id': 'num_associated_deals', + 'label': 'Associated Deals', + }, + { + 'id': 'num_conversion_events', + 'label': 'Number of Form Submissions', + }, + { + 'id': 'num_unique_conversion_events', + 'label': 'Number of Unique Forms Submitted', + }, + { + 'id': 'recent_conversion_date', + 'label': 'Recent Conversion Date', + }, + { + 'id': 'recent_conversion_event_name', + 'label': 'Recent Conversion', + }, + { + 'id': 'recent_deal_amount', + 'label': 'Recent Deal Amount', + }, + { + 'id': 'recent_deal_close_date', + 'label': 'Recent Deal Close Date', + }, + { + 'id': 'relationship_status', + 'label': 'Relationship Status', + }, + { + 'id': 'school', + 'label': 'School', + }, + { + 'id': 'seniority', + 'label': 'Seniority', + }, + { + 'id': 'start_date', + 'label': 'Start date', + }, + { + 'id': 'testing', + 'label': 'testing', + }, + { + 'id': 'total_revenue', + 'label': 'Total Revenue', + }, + { + 'id': 'work_email', + 'label': 'Work email', + }, + { + 'id': 'firstname', + 'label': 'First Name', + }, + { + 'id': 'hs_analytics_first_url', + 'label': 'First Page Seen', + }, + { + 'id': 'hs_email_delivered', + 'label': 'Marketing emails delivered', + }, + { + 'id': 'hs_email_optout_6871816', + 'label': 'Opted out of email: Marketing Information', + }, + { + 'id': 'hs_email_optout_8363428', + 'label': 'Opted out of email: One to One', + }, + { + 'id': 'twitterhandle', + 'label': 'Twitter Username', + }, + { + 'id': 'currentlyinworkflow', + 'label': 'Currently in workflow', + }, + { + 'id': 'followercount', + 'label': 'Follower Count', + }, + { + 'id': 'hs_analytics_last_url', + 'label': 'Last Page Seen', + }, + { + 'id': 'hs_email_open', + 'label': 'Marketing emails opened', + }, + { + 'id': 'lastname', + 'label': 'Last Name', + }, + { + 'id': 'hs_analytics_num_page_views', + 'label': 'Number of Pageviews', + }, + { + 'id': 'hs_email_click', + 'label': 'Marketing emails clicked', + }, + { + 'id': 'salutation', + 'label': 'Salutation', + }, + { + 'id': 'twitterprofilephoto', + 'label': 'Twitter Profile Photo', + }, + { + 'id': 'email', + 'label': 'Email', + }, + { + 'id': 'hs_analytics_num_visits', + 'label': 'Number of Sessions', + }, + { + 'id': 'hs_email_bounce', + 'label': 'Marketing emails bounced', + }, + { + 'id': 'hs_persona', + 'label': 'Persona', + }, + { + 'id': 'hs_social_last_engagement', + 'label': 'Most Recent Social Click', + }, + { + 'id': 'hs_analytics_num_event_completions', + 'label': 'Number of event completions', + }, + { + 'id': 'hs_email_optout', + 'label': 'Unsubscribed from all email', + }, + { + 'id': 'hs_social_twitter_clicks', + 'label': 'Twitter Clicks', + }, + { + 'id': 'mobilephone', + 'label': 'Mobile Phone Number', + }, + { + 'id': 'phone', + 'label': 'Phone Number', + }, + { + 'id': 'fax', + 'label': 'Fax Number', + }, + { + 'id': 'hs_analytics_first_timestamp', + 'label': 'Time First Seen', + }, + { + 'id': 'hs_email_last_email_name', + 'label': 'Last marketing email name', + }, + { + 'id': 'hs_email_last_send_date', + 'label': 'Last marketing email send date', + }, + { + 'id': 'hs_social_facebook_clicks', + 'label': 'Facebook Clicks', + }, + { + 'id': 'address', + 'label': 'Street Address', + }, + { + 'id': 'engagements_last_meeting_booked', + 'label': 'Date of last meeting booked in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_campaign', + 'label': 'Campaign of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_medium', + 'label': 'Medium of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_source', + 'label': 'Source of last booking in meetings tool', + }, + { + 'id': 'hs_analytics_first_visit_timestamp', + 'label': 'Time of First Session', + }, + { + 'id': 'hs_email_last_open_date', + 'label': 'Last marketing email open date', + }, + { + 'id': 'hs_latest_meeting_activity', + 'label': 'Latest meeting activity', + }, + { + 'id': 'hs_sales_email_last_replied', + 'label': 'Recent Sales Email Replied Date', + }, + { + 'id': 'hs_social_linkedin_clicks', + 'label': 'LinkedIn Clicks', + }, + { + 'id': 'hubspot_owner_id', + 'label': 'Contact owner', + }, + { + 'id': 'notes_last_contacted', + 'label': 'Last Contacted', + }, + { + 'id': 'notes_last_updated', + 'label': 'Last Activity Date', + }, + { + 'id': 'notes_next_activity_date', + 'label': 'Next Activity Date', + }, + { + 'id': 'num_contacted_notes', + 'label': 'Number of times contacted', + }, + { + 'id': 'num_notes', + 'label': 'Number of Sales Activities', + }, + { + 'id': 'owneremail', + 'label': 'HubSpot Owner Email (legacy)', + }, + { + 'id': 'ownername', + 'label': 'HubSpot Owner Name (legacy)', + }, + { + 'id': 'surveymonkeyeventlastupdated', + 'label': 'SurveyMonkey Event Last Updated', + }, + { + 'id': 'webinareventlastupdated', + 'label': 'Webinar Event Last Updated', + }, + { + 'id': 'city', + 'label': 'City', + }, + { + 'id': 'hs_analytics_last_timestamp', + 'label': 'Time Last Seen', + }, + { + 'id': 'hs_email_last_click_date', + 'label': 'Last marketing email click date', + }, + { + 'id': 'hs_social_google_plus_clicks', + 'label': 'Google Plus Clicks', + }, + { + 'id': 'hubspot_team_id', + 'label': 'HubSpot Team', + }, + { + 'id': 'linkedinbio', + 'label': 'LinkedIn Bio', + }, + { + 'id': 'twitterbio', + 'label': 'Twitter Bio', + }, + { + 'id': 'hs_all_owner_ids', + 'label': 'All owner ids', + }, + { + 'id': 'hs_analytics_last_visit_timestamp', + 'label': 'Time of Last Session', + }, + { + 'id': 'hs_email_first_send_date', + 'label': 'First marketing email send date', + }, + { + 'id': 'hs_social_num_broadcast_clicks', + 'label': 'Broadcast Clicks', + }, + { + 'id': 'state', + 'label': 'State/Region', + }, + { + 'id': 'hs_all_team_ids', + 'label': 'All team ids', + }, + { + 'id': 'hs_analytics_source', + 'label': 'Original Source', + }, + { + 'id': 'hs_email_first_open_date', + 'label': 'First marketing email open date', + }, + { + 'id': 'zip', + 'label': 'Postal Code', + }, + { + 'id': 'country', + 'label': 'Country/Region', + }, + { + 'id': 'hs_all_accessible_team_ids', + 'label': 'All accessible team ids', + }, + { + 'id': 'hs_analytics_source_data_1', + 'label': 'Original Source Drill-Down 1', + }, + { + 'id': 'hs_email_first_click_date', + 'label': 'First marketing email click date', + }, + { + 'id': 'linkedinconnections', + 'label': 'LinkedIn Connections', + }, + { + 'id': 'hs_analytics_source_data_2', + 'label': 'Original Source Drill-Down 2', + }, + { + 'id': 'hs_email_is_ineligible', + 'label': 'Is globally ineligible', + }, + { + 'id': 'hs_language', + 'label': 'Preferred language', + }, + { + 'id': 'kloutscoregeneral', + 'label': 'Klout Score', + }, + { + 'id': 'hs_analytics_first_referrer', + 'label': 'First Referring Site', + }, + { + 'id': 'hs_email_first_reply_date', + 'label': 'First marketing email reply date', + }, + { + 'id': 'jobtitle', + 'label': 'Job Title', + }, + { + 'id': 'photo', + 'label': 'Photo', + }, + { + 'id': 'hs_analytics_last_referrer', + 'label': 'Last Referring Site', + }, + { + 'id': 'hs_email_last_reply_date', + 'label': 'Last marketing email reply date', + }, + { + 'id': 'message', + 'label': 'Message', + }, + { + 'id': 'closedate', + 'label': 'Close Date', + }, + { + 'id': 'hs_analytics_average_page_views', + 'label': 'Average Pageviews', + }, + { + 'id': 'hs_email_replied', + 'label': 'Marketing emails replied', + }, + { + 'id': 'hs_analytics_revenue', + 'label': 'Event Revenue', + }, + { + 'id': 'hs_lifecyclestage_lead_date', + 'label': 'Became a Lead Date', + }, + { + 'id': 'hs_lifecyclestage_marketingqualifiedlead_date', + 'label': 'Became a Marketing Qualified Lead Date', + }, + { + 'id': 'hs_lifecyclestage_opportunity_date', + 'label': 'Became an Opportunity Date', + }, + { + 'id': 'lifecyclestage', + 'label': 'Lifecycle Stage', + }, + { + 'id': 'hs_lifecyclestage_salesqualifiedlead_date', + 'label': 'Became a Sales Qualified Lead Date', + }, + { + 'id': 'createdate', + 'label': 'Create Date', + }, + { + 'id': 'hs_lifecyclestage_evangelist_date', + 'label': 'Became an Evangelist Date', + }, + { + 'id': 'hs_lifecyclestage_customer_date', + 'label': 'Became a Customer Date', + }, + { + 'id': 'hubspotscore', + 'label': 'HubSpot Score', + }, + { + 'id': 'company', + 'label': 'Company Name', + }, + { + 'id': 'hs_lifecyclestage_subscriber_date', + 'label': 'Became a Subscriber Date', + }, + { + 'id': 'hs_lifecyclestage_other_date', + 'label': 'Became an Other Lifecycle Date', + }, + { + 'id': 'website', + 'label': 'Website URL', + }, + { + 'id': 'numemployees', + 'label': 'Number of Employees', + }, + { + 'id': 'annualrevenue', + 'label': 'Annual Revenue', + }, + { + 'id': 'industry', + 'label': 'Industry', + }, + { + 'id': 'associatedcompanyid', + 'label': 'Associated Company ID', + }, + { + 'id': 'associatedcompanylastupdated', + 'label': 'Associated Company Last Updated', + }, + { + 'id': 'hs_predictivecontactscorebucket', + 'label': 'Lead Rating', + }, + { + 'id': 'hs_predictivecontactscore', + 'label': 'Predictive Lead Score', + }, +]; + +export const companyFields = [ + { + 'id': 'about_us', + 'label': 'About Us', + }, + { + 'id': 'closedate_timestamp_earliest_value_a2a17e6e', + 'label': 'closedate_timestamp_earliest_value_a2a17e6e', + }, + { + 'id': 'facebookfans', + 'label': 'Facebook Fans', + }, + { + 'id': 'first_contact_createdate_timestamp_earliest_value_78b50eea', + 'label': 'first_contact_createdate_timestamp_earliest_value_78b50eea', + }, + { + 'id': 'first_conversion_date', + 'label': 'First Conversion Date', + }, + { + 'id': 'first_conversion_date_timestamp_earliest_value_61f58f2c', + 'label': 'first_conversion_date_timestamp_earliest_value_61f58f2c', + }, + { + 'id': 'first_conversion_event_name', + 'label': 'First Conversion', + }, + { + 'id': 'first_conversion_event_name_timestamp_earliest_value_68ddae0a', + 'label': 'first_conversion_event_name_timestamp_earliest_value_68ddae0a', + }, + { + 'id': 'first_deal_created_date', + 'label': 'First Deal Created Date', + }, + { + 'id': 'founded_year', + 'label': 'Year Founded', + }, + { + 'id': 'hs_additional_domains', + 'label': 'Additional Domains', + }, + { + 'id': 'hs_analytics_first_timestamp', + 'label': 'Time First Seen', + }, + { + 'id': 'hs_analytics_first_timestamp_timestamp_earliest_value_11e3a63a', + 'label': 'hs_analytics_first_timestamp_timestamp_earliest_value_11e3a63a', + }, + { + 'id': 'hs_analytics_first_touch_converting_campaign', + 'label': 'First Touch Converting Campaign', + }, + { + 'id': 'hs_analytics_first_touch_converting_campaign_timestamp_earliest_value_4757fe10', + 'label': 'hs_analytics_first_touch_converting_campaign_timestamp_earliest_value_4757fe10', + }, + { + 'id': 'hs_analytics_first_visit_timestamp', + 'label': 'Time of First Session', + }, + { + 'id': 'hs_analytics_first_visit_timestamp_timestamp_earliest_value_accc17ae', + 'label': 'hs_analytics_first_visit_timestamp_timestamp_earliest_value_accc17ae', + }, + { + 'id': 'hs_analytics_last_timestamp', + 'label': 'Time Last Seen', + }, + { + 'id': 'hs_analytics_last_timestamp_timestamp_latest_value_4e16365a', + 'label': 'hs_analytics_last_timestamp_timestamp_latest_value_4e16365a', + }, + { + 'id': 'hs_analytics_last_touch_converting_campaign', + 'label': 'Last Touch Converting Campaign', + }, + { + 'id': 'hs_analytics_last_touch_converting_campaign_timestamp_latest_value_81a64e30', + 'label': 'hs_analytics_last_touch_converting_campaign_timestamp_latest_value_81a64e30', + }, + { + 'id': 'hs_analytics_last_visit_timestamp', + 'label': 'Time of Last Session', + }, + { + 'id': 'hs_analytics_last_visit_timestamp_timestamp_latest_value_999a0fce', + 'label': 'hs_analytics_last_visit_timestamp_timestamp_latest_value_999a0fce', + }, + { + 'id': 'hs_analytics_num_page_views', + 'label': 'Number of Pageviews', + }, + { + 'id': 'hs_analytics_num_page_views_cardinality_sum_e46e85b0', + 'label': 'hs_analytics_num_page_views_cardinality_sum_e46e85b0', + }, + { + 'id': 'hs_analytics_num_visits', + 'label': 'Number of Sessions', + }, + { + 'id': 'hs_analytics_num_visits_cardinality_sum_53d952a6', + 'label': 'hs_analytics_num_visits_cardinality_sum_53d952a6', + }, + { + 'id': 'hs_analytics_source', + 'label': 'Original Source Type', + }, + { + 'id': 'hs_analytics_source_data_1', + 'label': 'Original Source Data 1', + }, + { + 'id': 'hs_analytics_source_data_1_timestamp_earliest_value_9b2f1fa1', + 'label': 'hs_analytics_source_data_1_timestamp_earliest_value_9b2f1fa1', + }, + { + 'id': 'hs_analytics_source_data_2', + 'label': 'Original Source Data 2', + }, + { + 'id': 'hs_analytics_source_data_2_timestamp_earliest_value_9b2f9400', + 'label': 'hs_analytics_source_data_2_timestamp_earliest_value_9b2f9400', + }, + { + 'id': 'hs_analytics_source_timestamp_earliest_value_25a3a52c', + 'label': 'hs_analytics_source_timestamp_earliest_value_25a3a52c', + }, + { + 'id': 'hs_avatar_filemanager_key', + 'label': 'Avatar FileManager key', + }, + { + 'id': 'hs_created_by_user_id', + 'label': 'Created by user ID', + }, + { + 'id': 'hs_createdate', + 'label': 'Object create date/time', + }, + { + 'id': 'hs_ideal_customer_profile', + 'label': 'Ideal Customer Profile Tier', + }, + { + 'id': 'hs_is_target_account', + 'label': 'Target Account', + }, + { + 'id': 'hs_last_booked_meeting_date', + 'label': 'Last Booked Meeting Date', + }, + { + 'id': 'hs_last_logged_call_date', + 'label': 'Last Logged Call Date', + }, + { + 'id': 'hs_last_open_task_date', + 'label': 'Last Open Task Date', + }, + { + 'id': 'hs_last_sales_activity_date', + 'label': 'last sales activity date old', + }, + { + 'id': 'hs_last_sales_activity_timestamp', + 'label': 'Last Engagement Date', + }, + { + 'id': 'hs_lastmodifieddate', + 'label': 'Last Modified Date', + }, + { + 'id': 'hs_merged_object_ids', + 'label': 'Merged object IDs', + }, + { + 'id': 'hs_num_blockers', + 'label': 'Number of blockers', + }, + { + 'id': 'hs_num_contacts_with_buying_roles', + 'label': 'Number of contacts with a buying role', + }, + { + 'id': 'hs_num_decision_makers', + 'label': 'Number of decision makers', + }, + { + 'id': 'hs_num_open_deals', + 'label': 'Number of open deals', + }, + { + 'id': 'hs_object_id', + 'label': 'Company ID', + }, + { + 'id': 'hs_predictivecontactscore_v2', + 'label': 'Likelihood to close', + }, + { + 'id': 'hs_predictivecontactscore_v2_next_max_max_d4e58c1e', + 'label': 'hs_predictivecontactscore_v2_next_max_max_d4e58c1e', + }, + { + 'id': 'hs_target_account', + 'label': 'Target Account', + }, + { + 'id': 'hs_target_account_probability', + 'label': 'Target Account Probability', + }, + { + 'id': 'hs_target_account_recommendation_snooze_time', + 'label': 'Target Account Recommendation Snooze Time', + }, + { + 'id': 'hs_target_account_recommendation_state', + 'label': 'Target Account Recommendation State', + }, + { + 'id': 'hs_total_deal_value', + 'label': 'Total open deal value', + }, + { + 'id': 'hs_updated_by_user_id', + 'label': 'Updated by user ID', + }, + { + 'id': 'hs_user_ids_of_all_owners', + 'label': 'User IDs of all owners', + }, + { + 'id': 'hubspot_owner_assigneddate', + 'label': 'Owner Assigned Date', + }, + { + 'id': 'is_public', + 'label': 'Is Public', + }, + { + 'id': 'num_associated_contacts', + 'label': 'Associated Contacts', + }, + { + 'id': 'num_associated_deals', + 'label': 'Associated Deals', + }, + { + 'id': 'num_conversion_events', + 'label': 'Number of Form Submissions', + }, + { + 'id': 'num_conversion_events_cardinality_sum_d095f14b', + 'label': 'num_conversion_events_cardinality_sum_d095f14b', + }, + { + 'id': 'recent_conversion_date', + 'label': 'Recent Conversion Date', + }, + { + 'id': 'recent_conversion_date_timestamp_latest_value_72856da1', + 'label': 'recent_conversion_date_timestamp_latest_value_72856da1', + }, + { + 'id': 'recent_conversion_event_name', + 'label': 'Recent Conversion', + }, + { + 'id': 'recent_conversion_event_name_timestamp_latest_value_66c820bf', + 'label': 'recent_conversion_event_name_timestamp_latest_value_66c820bf', + }, + { + 'id': 'recent_deal_amount', + 'label': 'Recent Deal Amount', + }, + { + 'id': 'recent_deal_close_date', + 'label': 'Recent Deal Close Date', + }, + { + 'id': 'timezone', + 'label': 'Time Zone', + }, + { + 'id': 'total_money_raised', + 'label': 'Total Money Raised', + }, + { + 'id': 'total_revenue', + 'label': 'Total Revenue', + }, + { + 'id': 'name', + 'label': 'Name', + }, + { + 'id': 'owneremail', + 'label': 'HubSpot Owner Email', + }, + { + 'id': 'twitterhandle', + 'label': 'Twitter Handle', + }, + { + 'id': 'ownername', + 'label': 'HubSpot Owner Name', + }, + { + 'id': 'phone', + 'label': 'Phone Number', + }, + { + 'id': 'twitterbio', + 'label': 'Twitter Bio', + }, + { + 'id': 'twitterfollowers', + 'label': 'Twitter Followers', + }, + { + 'id': 'address', + 'label': 'Street Address', + }, + { + 'id': 'address2', + 'label': 'Street Address 2', + }, + { + 'id': 'facebook_company_page', + 'label': 'Facebook Company Page', + }, + { + 'id': 'city', + 'label': 'City', + }, + { + 'id': 'linkedin_company_page', + 'label': 'LinkedIn Company Page', + }, + { + 'id': 'linkedinbio', + 'label': 'LinkedIn Bio', + }, + { + 'id': 'state', + 'label': 'State/Region', + }, + { + 'id': 'googleplus_page', + 'label': 'Google Plus Page', + }, + { + 'id': 'engagements_last_meeting_booked', + 'label': 'Date of last meeting booked in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_campaign', + 'label': 'Campaign of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_medium', + 'label': 'Medium of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_source', + 'label': 'Source of last booking in meetings tool', + }, + { + 'id': 'hs_latest_meeting_activity', + 'label': 'Latest meeting activity', + }, + { + 'id': 'hs_sales_email_last_replied', + 'label': 'Recent Sales Email Replied Date', + }, + { + 'id': 'hubspot_owner_id', + 'label': 'Company owner', + }, + { + 'id': 'notes_last_contacted', + 'label': 'Last Contacted', + }, + { + 'id': 'notes_last_updated', + 'label': 'Last Activity Date', + }, + { + 'id': 'notes_next_activity_date', + 'label': 'Next Activity Date', + }, + { + 'id': 'num_contacted_notes', + 'label': 'Number of times contacted', + }, + { + 'id': 'num_notes', + 'label': 'Number of Sales Activities', + }, + { + 'id': 'zip', + 'label': 'Postal Code', + }, + { + 'id': 'country', + 'label': 'Country/Region', + }, + { + 'id': 'hubspot_team_id', + 'label': 'HubSpot Team', + }, + { + 'id': 'hs_all_owner_ids', + 'label': 'All owner ids', + }, + { + 'id': 'website', + 'label': 'Website URL', + }, + { + 'id': 'domain', + 'label': 'Company Domain Name', + }, + { + 'id': 'hs_all_team_ids', + 'label': 'All team ids', + }, + { + 'id': 'hs_all_accessible_team_ids', + 'label': 'All accessible team ids', + }, + { + 'id': 'numberofemployees', + 'label': 'Number of Employees', + }, + { + 'id': 'industry', + 'label': 'Industry', + }, + { + 'id': 'annualrevenue', + 'label': 'Annual Revenue', + }, + { + 'id': 'lifecyclestage', + 'label': 'Lifecycle Stage', + }, + { + 'id': 'hs_lead_status', + 'label': 'Lead Status', + }, + { + 'id': 'hs_parent_company_id', + 'label': 'Parent Company', + }, + { + 'id': 'type', + 'label': 'Type', + }, + { + 'id': 'description', + 'label': 'Description', + }, + { + 'id': 'hs_num_child_companies', + 'label': 'Number of child companies', + }, + { + 'id': 'hubspotscore', + 'label': 'HubSpot Score', + }, + { + 'id': 'createdate', + 'label': 'Create Date', + }, + { + 'id': 'closedate', + 'label': 'Close Date', + }, + { + 'id': 'first_contact_createdate', + 'label': 'First Contact Create Date', + }, + { + 'id': 'days_to_close', + 'label': 'Days to Close', + }, + { + 'id': 'web_technologies', + 'label': 'Web Technologies', + }, +]; + +export const dealFields = [ + { + 'id': 'amount_in_home_currency', + 'label': 'Amount in company currency', + }, + { + 'id': 'days_to_close', + 'label': 'Days to close', + }, + { + 'id': 'deal_currency_code', + 'label': 'Currency', + }, + { + 'id': 'hs_acv', + 'label': 'Annual contract value', + }, + { + 'id': 'hs_analytics_source', + 'label': 'Original Source Type', + }, + { + 'id': 'hs_analytics_source_data_1', + 'label': 'Original Source Data 1', + }, + { + 'id': 'hs_analytics_source_data_2', + 'label': 'Original Source Data 2', + }, + { + 'id': 'hs_arr', + 'label': 'Annual recurring revenue', + }, + { + 'id': 'hs_campaign', + 'label': 'HubSpot Campaign', + }, + { + 'id': 'hs_closed_amount', + 'label': 'Closed Deal Amount', + }, + { + 'id': 'hs_closed_amount_in_home_currency', + 'label': 'Closed Deal Amount In Home Currency', + }, + { + 'id': 'hs_created_by_user_id', + 'label': 'Created by user ID', + }, + { + 'id': 'hs_date_entered_appointmentscheduled', + 'label': 'Date entered \'Appointment Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_closedlost', + 'label': 'Date entered \'Closed Lost (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_closedwon', + 'label': 'Date entered \'Closed Won (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_contractsent', + 'label': 'Date entered \'Contract Sent (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_decisionmakerboughtin', + 'label': 'Date entered \'Decision Maker Bought-In (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_presentationscheduled', + 'label': 'Date entered \'Presentation Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_entered_qualifiedtobuy', + 'label': 'Date entered \'Qualified To Buy (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_appointmentscheduled', + 'label': 'Date exited \'Appointment Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_closedlost', + 'label': 'Date exited \'Closed Lost (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_closedwon', + 'label': 'Date exited \'Closed Won (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_contractsent', + 'label': 'Date exited \'Contract Sent (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_decisionmakerboughtin', + 'label': 'Date exited \'Decision Maker Bought-In (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_presentationscheduled', + 'label': 'Date exited \'Presentation Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_date_exited_qualifiedtobuy', + 'label': 'Date exited \'Qualified To Buy (Sales Pipeline)\'', + }, + { + 'id': 'hs_deal_amount_calculation_preference', + 'label': 'Deal amount calculation preference', + }, + { + 'id': 'hs_deal_stage_probability', + 'label': 'Deal Stage Probability', + }, + { + 'id': 'hs_forecast_amount', + 'label': 'Forecast Amount', + }, + { + 'id': 'hs_forecast_probability', + 'label': 'Forecast Probability', + }, + { + 'id': 'hs_is_closed', + 'label': 'Is Deal Closed?', + }, + { + 'id': 'hs_lastmodifieddate', + 'label': 'Last Modified Date', + }, + { + 'id': 'hs_likelihood_to_close', + 'label': 'Likelihood to close by the close date', + }, + { + 'id': 'hs_line_item_global_term_hs_discount_percentage', + 'label': 'Global Term Line Item Discount Percentage', + }, + { + 'id': 'hs_line_item_global_term_hs_discount_percentage_enabled', + 'label': 'Global Term Line Item Discount Percentage Enabled', + }, + { + 'id': 'hs_line_item_global_term_hs_recurring_billing_period', + 'label': 'Global Term Line Item Recurring Billing Period', + }, + { + 'id': 'hs_line_item_global_term_hs_recurring_billing_period_enabled', + 'label': 'Global Term Line Item Recurring Billing Period Enabled', + }, + { + 'id': 'hs_line_item_global_term_hs_recurring_billing_start_date', + 'label': 'Global Term Line Item Recurring Billing Start Date', + }, + { + 'id': 'hs_line_item_global_term_hs_recurring_billing_start_date_enabled', + 'label': 'Global Term Line Item Recurring Billing Start Date Enabled', + }, + { + 'id': 'hs_line_item_global_term_recurringbillingfrequency', + 'label': 'Global Term Line Item Recurring Billing Frequency', + }, + { + 'id': 'hs_line_item_global_term_recurringbillingfrequency_enabled', + 'label': 'Global Term Line Item Recurring Billing Frequency Enabled', + }, + { + 'id': 'hs_manual_forecast_category', + 'label': 'Forecast category', + }, + { + 'id': 'hs_merged_object_ids', + 'label': 'Merged object IDs', + }, + { + 'id': 'hs_mrr', + 'label': 'Monthly recurring revenue', + }, + { + 'id': 'hs_next_step', + 'label': 'Next step', + }, + { + 'id': 'hs_object_id', + 'label': 'Deal ID', + }, + { + 'id': 'hs_predicted_amount', + 'label': 'The predicted deal amount', + }, + { + 'id': 'hs_predicted_amount_in_home_currency', + 'label': 'The predicted deal amount in your company\'s currency', + }, + { + 'id': 'hs_projected_amount', + 'label': 'Projected Deal Amount', + }, + { + 'id': 'hs_projected_amount_in_home_currency', + 'label': 'Projected Deal Amount in Home Currency', + }, + { + 'id': 'hs_tcv', + 'label': 'Total contract value', + }, + { + 'id': 'hs_time_in_appointmentscheduled', + 'label': 'Time in \'Appointment Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_closedlost', + 'label': 'Time in \'Closed Lost (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_closedwon', + 'label': 'Time in \'Closed Won (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_contractsent', + 'label': 'Time in \'Contract Sent (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_decisionmakerboughtin', + 'label': 'Time in \'Decision Maker Bought-In (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_presentationscheduled', + 'label': 'Time in \'Presentation Scheduled (Sales Pipeline)\'', + }, + { + 'id': 'hs_time_in_qualifiedtobuy', + 'label': 'Time in \'Qualified To Buy (Sales Pipeline)\'', + }, + { + 'id': 'hs_updated_by_user_id', + 'label': 'Updated by user ID', + }, + { + 'id': 'hs_user_ids_of_all_owners', + 'label': 'User IDs of all owners', + }, + { + 'id': 'hubspot_owner_assigneddate', + 'label': 'Owner Assigned Date', + }, + { + 'id': 'testing', + 'label': 'testing', + }, + { + 'id': 'dealname', + 'label': 'Deal Name', + }, + { + 'id': 'amount', + 'label': 'Amount', + }, + { + 'id': 'dealstage', + 'label': 'Deal Stage', + }, + { + 'id': 'pipeline', + 'label': 'Pipeline', + }, + { + 'id': 'closedate', + 'label': 'Close Date', + }, + { + 'id': 'createdate', + 'label': 'Create Date', + }, + { + 'id': 'engagements_last_meeting_booked', + 'label': 'Date of last meeting booked in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_campaign', + 'label': 'Campaign of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_medium', + 'label': 'Medium of last booking in meetings tool', + }, + { + 'id': 'engagements_last_meeting_booked_source', + 'label': 'Source of last booking in meetings tool', + }, + { + 'id': 'hs_latest_meeting_activity', + 'label': 'Latest meeting activity', + }, + { + 'id': 'hs_sales_email_last_replied', + 'label': 'Recent Sales Email Replied Date', + }, + { + 'id': 'hubspot_owner_id', + 'label': 'Deal owner', + }, + { + 'id': 'notes_last_contacted', + 'label': 'Last Contacted', + }, + { + 'id': 'notes_last_updated', + 'label': 'Last Activity Date', + }, + { + 'id': 'notes_next_activity_date', + 'label': 'Next Activity Date', + }, + { + 'id': 'num_contacted_notes', + 'label': 'Number of times contacted', + }, + { + 'id': 'num_notes', + 'label': 'Number of Sales Activities', + }, + { + 'id': 'hs_createdate', + 'label': 'HubSpot Create Date', + }, + { + 'id': 'hubspot_team_id', + 'label': 'HubSpot Team', + }, + { + 'id': 'dealtype', + 'label': 'Deal Type', + }, + { + 'id': 'hs_all_owner_ids', + 'label': 'All owner ids', + }, + { + 'id': 'description', + 'label': 'Deal Description', + }, + { + 'id': 'hs_all_team_ids', + 'label': 'All team ids', + }, + { + 'id': 'hs_all_accessible_team_ids', + 'label': 'All accessible team ids', + }, + { + 'id': 'num_associated_contacts', + 'label': 'Number of Contacts', + }, + { + 'id': 'closed_lost_reason', + 'label': 'Closed Lost Reason', + }, + { + 'id': 'closed_won_reason', + 'label': 'Closed Won Reason', + }, +]; diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index a7bcefc5a4..b99fbaf0d7 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -63,7 +63,7 @@ export class Hubspot implements INodeType { description: INodeTypeDescription = { displayName: 'HubSpot', name: 'hubspot', - icon: 'file:hubspot.png', + icon: 'file:hubspot.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -526,6 +526,26 @@ export class Hubspot implements INodeType { } return returnData; }, + + // Get all the company custom properties to display them to user so that he can + // select them easily + async getCompanyCustomProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/companies/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.hubspotDefined === null) { + const propertyName = property.label; + const propertyId = property.name; + returnData.push({ + name: propertyName, + value: propertyId, + }); + } + } + return returnData; + }, + /* -------------------------------------------------------------------------- */ /* DEAL */ /* -------------------------------------------------------------------------- */ @@ -1535,6 +1555,18 @@ export class Hubspot implements INodeType { value: additionalFields.yearFounded, }); } + if (additionalFields.customPropertiesUi) { + const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + + if (customProperties) { + for (const customProperty of customProperties) { + body.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } const endpoint = '/companies/v2/companies'; responseData = await hubspotApiRequest.call(this, 'POST', endpoint, { properties: body }); } @@ -1747,6 +1779,18 @@ export class Hubspot implements INodeType { value: updateFields.yearFounded, }); } + if (updateFields.customPropertiesUi) { + const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + + if (customProperties) { + for (const customProperty of customProperties) { + body.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } const endpoint = `/companies/v2/companies/${companyId}`; responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, { properties: body }); } diff --git a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts index 1274d929fe..8f29418dac 100644 --- a/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts +++ b/packages/nodes-base/nodes/Hubspot/HubspotTrigger.node.ts @@ -5,27 +5,36 @@ import { import { IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, INodeType, INodeTypeDescription, IWebhookResponseData, } from 'n8n-workflow'; import { + companyFields, + contactFields, + dealFields, hubspotApiRequest, + propertyEvents, } from './GenericFunctions'; import { createHash, - } from 'crypto'; +} from 'crypto'; + +import { + capitalCase, +} from 'change-case'; export class HubspotTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'HubSpot Trigger', name: 'hubspotTrigger', - icon: 'file:hubspot.png', + icon: 'file:hubspot.svg', group: ['trigger'], version: 1, - subtitle: '={{($parameter["appId"]) ? $parameter["event"] : ""}}', description: 'Starts the workflow when HubSpot events occur.', defaults: { name: 'Hubspot Trigger', @@ -55,87 +64,132 @@ export class HubspotTrigger implements INodeType { ], properties: [ { - displayName: 'App ID', - name: 'appId', - type: 'string', - default: '', - required: true, - description: 'App ID', - }, - { - displayName: 'Event', - name: 'event', - type: 'options', + displayName: 'Events', + name: 'eventsUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Event', + default: {}, options: [ { - name: 'contact.creation', - value: 'contact.creation', - description: `To get notified if any contact is created in a customer's account.`, - }, - { - name: 'contact.deletion', - value: 'contact.deletion', - description: `To get notified if any contact is deleted in a customer's account.`, - }, - { - name: 'contact.privacyDeletion', - value: 'contact.privacyDeletion', - description: `To get notified if a contact is deleted for privacy compliance reasons. `, - }, - { - name: 'contact.propertyChange', - value: 'contact.propertyChange', - description: `to get notified if a specified property is changed for any contact in a customer's account. `, - }, - { - name: 'company.creation', - value: 'company.creation', - description: `To get notified if any company is created in a customer's account.`, - }, - { - name: 'company.deletion', - value: 'company.deletion', - description: `To get notified if any company is deleted in a customer's account.`, - }, - { - name: 'company.propertyChange', - value: 'company.propertyChange', - description: `To get notified if a specified property is changed for any company in a customer's account.`, - }, - { - name: 'deal.creation', - value: 'deal.creation', - description: `To get notified if any deal is created in a customer's account.`, - }, - { - name: 'deal.deletion', - value: 'deal.deletion', - description: `To get notified if any deal is deleted in a customer's account.`, - }, - { - name: 'deal.propertyChange', - value: 'deal.propertyChange', - description: `To get notified if a specified property is changed for any deal in a customer's account.`, - }, - ], - default: 'contact.creation', - required: true, - }, - { - displayName: 'Property', - name: 'property', - type: 'string', - displayOptions: { - show: { - event: [ - 'contact.propertyChange', - 'company.propertyChange', - 'deal.propertyChange', + displayName: 'Event', + name: 'eventValues', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'options', + options: [ + { + name: 'Contact Created', + value: 'contact.creation', + description: `To get notified if any contact is created in a customer's account.`, + }, + { + name: 'Contact Deleted', + value: 'contact.deletion', + description: `To get notified if any contact is deleted in a customer's account.`, + }, + { + name: 'Contact Privacy Deleted', + value: 'contact.privacyDeletion', + description: `To get notified if a contact is deleted for privacy compliance reasons. `, + }, + { + name: 'Contact Property Changed', + value: 'contact.propertyChange', + description: `to get notified if a specified property is changed for any contact in a customer's account. `, + }, + { + name: 'Company Created', + value: 'company.creation', + description: `To get notified if any company is created in a customer's account.`, + }, + { + name: 'Company Deleted', + value: 'company.deletion', + description: `To get notified if any company is deleted in a customer's account.`, + }, + { + name: 'Company Property Changed', + value: 'company.propertyChange', + description: `To get notified if a specified property is changed for any company in a customer's account.`, + }, + { + name: 'Deal Created', + value: 'deal.creation', + description: `To get notified if any deal is created in a customer's account.`, + }, + { + name: 'Deal Deleted', + value: 'deal.deletion', + description: `To get notified if any deal is deleted in a customer's account.`, + }, + { + name: 'Deal Property Changed', + value: 'deal.propertyChange', + description: `To get notified if a specified property is changed for any deal in a customer's account.`, + }, + ], + default: 'contact.creation', + required: true, + }, + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getContactProperties', + }, + displayOptions: { + show: { + name: [ + 'contact.propertyChange', + ], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCompanyProperties', + }, + displayOptions: { + show: { + name: [ + 'company.propertyChange', + ], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealProperties', + }, + displayOptions: { + show: { + name: [ + 'deal.propertyChange', + ], + }, + }, + default: '', + required: true, + }, ], }, - }, - default: '', - required: true, + ], }, { displayName: 'Additional Fields', @@ -156,7 +210,62 @@ export class HubspotTrigger implements INodeType { ], }, ], + }; + methods = { + loadOptions: { + // Get all the available contacts to display them to user so that he can + // select them easily + async getContactProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const field of contactFields) { + returnData.push({ + name: capitalCase(field.label), + value: field.id, + }); + } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; + }, + // Get all the available companies to display them to user so that he can + // select them easily + async getCompanyProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const field of companyFields) { + returnData.push({ + name: capitalCase(field.label), + value: field.id, + }); + } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; + }, + // Get all the available deals to display them to user so that he can + // select them easily + async getDealProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + for (const field of dealFields) { + returnData.push({ + name: capitalCase(field.label), + value: field.id, + }); + } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; + }, + }, }; // @ts-ignore (because of request) @@ -165,80 +274,78 @@ export class HubspotTrigger implements INodeType { async checkExists(this: IHookFunctions): Promise { // Check all the webhooks which exist already if it is identical to the // one that is supposed to get created. - const app = parseInt(this.getNodeParameter('appId') as string, 10); - const event = this.getNodeParameter('event') as string; - const webhookUrlUi = this.getNodeWebhookUrl('default') as string; - let endpoint = `/webhooks/v1/${app}/settings`; - const { webhookUrl , appId } = await hubspotApiRequest.call(this, 'GET', endpoint, {}); - endpoint = `/webhooks/v1/${app}/subscriptions`; - const subscriptions = await hubspotApiRequest.call(this, 'GET', endpoint, {}); - for (const subscription of subscriptions) { - if (webhookUrl === webhookUrlUi - && appId === app - && subscription.subscriptionDetails.subscriptionType === event - && subscription.enabled === true) { - return true; + const currentWebhookUrl = this.getNodeWebhookUrl('default') as string; + const { appId } = this.getCredentials('hubspotDeveloperApi') as IDataObject; + + try { + const { targetUrl } = await hubspotApiRequest.call(this, 'GET', `/webhooks/v3/${appId}/settings`, {}); + if (targetUrl !== currentWebhookUrl) { + throw new Error(`The APP ID ${appId} already has a target url ${targetUrl}. Delete it or use another APP ID before executing the trigger. Due to Hubspot API limitations, you can have just one trigger per APP.`); + } + } catch (error) { + if (error.statusCode === 404) { + return false; } } + // if the app is using the current webhook url. Delete everything and create it again with the current events + + const { results: subscriptions } = await hubspotApiRequest.call(this, 'GET', `/webhooks/v3/${appId}/subscriptions`, {}); + + // delete all subscriptions + for (const subscription of subscriptions) { + await hubspotApiRequest.call(this, 'DELETE', `/webhooks/v3/${appId}/subscriptions/${subscription.id}`, {}); + } + + await hubspotApiRequest.call(this, 'DELETE', `/webhooks/v3/${appId}/settings`, {}); + return false; }, async create(this: IHookFunctions): Promise { const webhookUrl = this.getNodeWebhookUrl('default'); - const app = this.getNodeParameter('appId') as string; - const event = this.getNodeParameter('event') as string; + const { appId } = this.getCredentials('hubspotDeveloperApi') as IDataObject; + const events = (this.getNodeParameter('eventsUi') as IDataObject || {}).eventValues as IDataObject[] || []; const additionalFields = this.getNodeParameter('additionalFields') as IDataObject; - const propertyEvents = [ - 'contact.propertyChange', - 'company.propertyChange', - 'deal.propertyChange', - ]; - let endpoint = `/webhooks/v1/${app}/settings`; + let endpoint = `/webhooks/v3/${appId}/settings`; let body: IDataObject = { - webhookUrl, + targetUrl: webhookUrl, maxConcurrentRequests: additionalFields.maxConcurrentRequests || 5, }; + await hubspotApiRequest.call(this, 'PUT', endpoint, body); - endpoint = `/webhooks/v1/${app}/subscriptions`; - body = { - subscriptionDetails: { - subscriptionType: event, - }, - enabled: true, - }; - if (propertyEvents.includes(event)) { - const property = this.getNodeParameter('property') as string; - //@ts-ignore - body.subscriptionDetails.propertyName = property; + endpoint = `/webhooks/v3/${appId}/subscriptions`; + + if (Array.isArray(events) && events.length === 0) { + throw new Error(`You must define at least one event`); } - const responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body); - - if (responseData.id === undefined) { - // Required data is missing so was not successful - return false; + for (const event of events) { + body = { + eventType: event.name, + active: true, + }; + if (propertyEvents.includes(event.name as string)) { + const property = event.property; + body.propertyName = property; + } + await hubspotApiRequest.call(this, 'POST', endpoint, body); } - const webhookData = this.getWorkflowStaticData('node'); - webhookData.webhookId = responseData.id as string; return true; }, async delete(this: IHookFunctions): Promise { - const webhookData = this.getWorkflowStaticData('node'); - const app = this.getNodeParameter('appId') as string; - if (webhookData.webhookId !== undefined) { - const endpoint = `/webhooks/v1/${app}/subscriptions/${webhookData.webhookId}`; + const { appId } = this.getCredentials('hubspotDeveloperApi') as IDataObject; - const body = {}; + const { results: subscriptions } = await hubspotApiRequest.call(this, 'GET', `/webhooks/v3/${appId}/subscriptions`, {}); - try { - await hubspotApiRequest.call(this, 'DELETE', endpoint, body); - } catch (e) { - return false; - } - // Remove from the static workflow data so that it is clear - // that no webhooks are registred anymore - delete webhookData.webhookId; + for (const subscription of subscriptions) { + await hubspotApiRequest.call(this, 'DELETE', `/webhooks/v3/${appId}/subscriptions/${subscription.id}`, {}); + } + + try { + await hubspotApiRequest.call(this, 'DELETE', `/webhooks/v3/${appId}/settings`, {}); + } catch (e) { + return false; } return true; }, @@ -265,7 +372,7 @@ export class HubspotTrigger implements INodeType { if (credentials.clientSecret !== '') { const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`; - const signature = createHash('sha256').update(hash).digest('hex'); + const signature = createHash('sha256').update(hash).digest('hex'); //@ts-ignore if (signature !== headerData['x-hubspot-signature']) { return {}; diff --git a/packages/nodes-base/nodes/Hubspot/TicketDescription.ts b/packages/nodes-base/nodes/Hubspot/TicketDescription.ts index adf6236b50..0836a6de4b 100644 --- a/packages/nodes-base/nodes/Hubspot/TicketDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/TicketDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export const ticketOperations = [ { @@ -48,9 +48,9 @@ export const ticketOperations = [ export const ticketFields = [ -/* -------------------------------------------------------------------------- */ -/* ticket:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* ticket:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Pipeline ID', name: 'pipelineId', @@ -70,7 +70,7 @@ export const ticketFields = [ }, }, default: '', - description: 'The ID of the pipeline the ticket is in. ', + description: 'The ID of the pipeline the ticket is in.', }, { displayName: 'Stage ID', @@ -94,7 +94,7 @@ export const ticketFields = [ }, }, default: '', - description: 'The ID of the pipeline the ticket is in. ', + description: 'The ID of the pipeline the ticket is in.', }, { displayName: 'Ticket Name', @@ -112,7 +112,7 @@ export const ticketFields = [ }, }, default: '', - description: 'The ID of the pipeline the ticket is in. ', + description: 'The ID of the pipeline the ticket is in.', }, { displayName: 'Additional Fields', @@ -136,7 +136,7 @@ export const ticketFields = [ name: 'associatedCompanyIds', type: 'multiOptions', typeOptions: { - loadOptionsMethod:'getCompanies' , + loadOptionsMethod: 'getCompanies', }, default: [], description: 'Companies associated with the ticket', @@ -146,7 +146,7 @@ export const ticketFields = [ name: 'associatedContactIds', type: 'multiOptions', typeOptions: { - loadOptionsMethod:'getContacts' , + loadOptionsMethod: 'getContacts', }, default: [], description: 'Contacts associated with the ticket', @@ -228,9 +228,9 @@ export const ticketFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* ticket:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* ticket:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Ticket ID', name: 'ticketId', @@ -247,7 +247,7 @@ export const ticketFields = [ }, }, default: '', - description: 'Unique identifier for a particular ticket', + description: 'Unique identifier for a particular ticket.', }, { displayName: 'Update Fields', @@ -271,20 +271,20 @@ export const ticketFields = [ name: 'associatedCompanyIds', type: 'multiOptions', typeOptions: { - loadOptionsMethod:'getCompanies' , + loadOptionsMethod: 'getCompanies', }, default: [], - description: 'Companies associated with the ticket', + description: 'Companies associated with the ticket.', }, { displayName: 'Contact Ids', name: 'associatedContactIds', type: 'multiOptions', typeOptions: { - loadOptionsMethod:'getContacts' , + loadOptionsMethod: 'getContacts', }, default: [], - description: 'Contact associated with the ticket', + description: 'Contact associated with the ticket.', }, { displayName: 'Category', @@ -294,21 +294,21 @@ export const ticketFields = [ loadOptionsMethod: 'getTicketCategories', }, default: '', - description: 'Main reason customer reached out for help', + description: 'Main reason customer reached out for help.', }, { displayName: 'Close Date', name: 'closeDate', type: 'dateTime', default: '', - description: 'The date the ticket was closed', + description: 'The date the ticket was closed.', }, { displayName: 'Create Date', name: 'createDate', type: 'dateTime', default: '', - description: 'the date the ticket was created', + description: 'The date the ticket was created.', }, { displayName: 'Description', @@ -328,7 +328,7 @@ export const ticketFields = [ loadOptionsMethod: 'getTicketPipelines', }, default: '', - description: 'The ID of the pipeline the ticket is in. ', + description: 'The ID of the pipeline the ticket is in.', }, { displayName: 'Priority', @@ -358,7 +358,7 @@ export const ticketFields = [ loadOptionsMethod: 'getTicketSources', }, default: '', - description: 'Channel where ticket was originally submitted', + description: 'Channel where ticket was originally submitted.', }, { displayName: 'Ticket Name', @@ -380,176 +380,179 @@ export const ticketFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* ticket:get */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Ticket ID', - name: 'ticketId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'get', - ], - }, - }, - default: '', - description: 'Unique identifier for a particular ticket', -}, -{ - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'get', - ], - }, - }, - options: [ - { - displayName: 'Include Deleted', - name: 'includeDeleted', - type: 'boolean', - default: false, - }, - { - displayName: 'Properties', - name: 'properties', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getTicketProperties', + + /* -------------------------------------------------------------------------- */ + /* ticket:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Ticket ID', + name: 'ticketId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], }, - default: [], - description: `Used to include specific ticket properties in the results.
+ }, + default: '', + description: 'Unique identifier for a particular ticket', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include Deleted', + name: 'includeDeleted', + type: 'boolean', + default: false, + }, + { + displayName: 'Properties', + name: 'properties', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTicketProperties', + }, + default: [], + description: `Used to include specific ticket properties in the results.
By default, the results will only include ticket ID and will not include the values for any properties for your tickets.
Including this parameter will include the data for the specified property in the results.
You can include this parameter multiple times to request multiple properties separed by ,.`, - }, - { - displayName: 'Properties With History', - name: 'propertiesWithHistory', - type: 'string', - default: '', - description: `Works similarly to properties=, but this parameter will include the history for the specified property,
- instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, - }, - ], -}, -/* -------------------------------------------------------------------------- */ -/* ticket:getAll */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'If all results should be returned or only up to a given limit.', -}, -{ - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 250, - }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Properties', - name: 'properties', - type: 'multiOptions', - typeOptions: { - loadOptionsMethod: 'getTicketProperties', }, - default: [], - description: `Used to include specific ticket properties in the results.
+ { + displayName: 'Properties With History', + name: 'propertiesWithHistory', + type: 'string', + default: '', + description: `Works similarly to properties=, but this parameter will include the history for the specified property,
+ instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* ticket:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 250, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Properties', + name: 'properties', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTicketProperties', + }, + default: [], + description: `Used to include specific ticket properties in the results.
By default, the results will only include ticket ID and will not include the values for any properties for your tickets.
Including this parameter will include the data for the specified property in the results.
You can include this parameter multiple times to request multiple properties separed by ,.`, - }, - { - displayName: 'Properties With History', - name: 'propertiesWithHistory', - type: 'string', - default: '', - description: `Works similarly to properties=, but this parameter will include the history for the specified property,
+ }, + { + displayName: 'Properties With History', + name: 'propertiesWithHistory', + type: 'string', + default: '', + description: `Works similarly to properties=, but this parameter will include the history for the specified property,
instead of just including the current value. Use this parameter when you need the full history of changes to a property's value.`, - }, - ], -}, -/* -------------------------------------------------------------------------- */ -/* ticket:delete */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Ticket ID', - name: 'ticketId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'ticket', - ], - operation: [ - 'delete', - ], - }, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* ticket:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Ticket ID', + name: 'ticketId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular ticket', }, - default: '', - description: 'Unique identifier for a particular ticket', -}, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Hubspot/hubspot.png b/packages/nodes-base/nodes/Hubspot/hubspot.png deleted file mode 100644 index 026c1ade08..0000000000 Binary files a/packages/nodes-base/nodes/Hubspot/hubspot.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Hubspot/hubspot.svg b/packages/nodes-base/nodes/Hubspot/hubspot.svg new file mode 100644 index 0000000000..0fd4c993e1 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/hubspot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/If.node.ts b/packages/nodes-base/nodes/If.node.ts index d36a875295..1e35d43d41 100644 --- a/packages/nodes-base/nodes/If.node.ts +++ b/packages/nodes-base/nodes/If.node.ts @@ -270,7 +270,7 @@ export class If implements INodeType { startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { - const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimy]*)$')); + const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); let regex: RegExp; if (!regexMatch) { diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 3d2198d34f..3ad028c680 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -1,6 +1,6 @@ import { OptionsWithUri, - } from 'request'; +} from 'request'; import { IExecuteFunctions, @@ -14,7 +14,7 @@ import { IDataObject, } from 'n8n-workflow'; -export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any +export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let data; let domain; const jiraVersion = this.getNodeParameter('jiraVersion', 0) as string; @@ -43,6 +43,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut Authorization: `Basic ${data}`, Accept: 'application/json', 'Content-Type': 'application/json', + 'X-Atlassian-Token': 'no-check', }, method, qs: query, @@ -51,6 +52,18 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut json: true, }; + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(query || {}).length === 0) { + delete options.qs; + } + try { return await this.helpers.request!(options); } catch (error) { @@ -82,7 +95,7 @@ export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | query.startAt = 0; body.startAt = 0; query.maxResults = 100; - body.maxResults = 100; + body.maxResults = 100; do { responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, method, body, query); @@ -106,7 +119,7 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- return result; } -export function eventExists (currentEvents : string[], webhookEvents: string[]) { +export function eventExists(currentEvents: string[], webhookEvents: string[]) { for (const currentEvent of currentEvents) { if (!webhookEvents.includes(currentEvent)) { return false; @@ -115,7 +128,7 @@ export function eventExists (currentEvents : string[], webhookEvents: string[]) return true; } -export function getId (url: string) { +export function getId(url: string) { return url.split('/').pop(); } @@ -159,4 +172,4 @@ export const allEvents = [ 'worklog_created', 'worklog_updated', 'worklog_deleted', -]; +]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts new file mode 100644 index 0000000000..39ad30a1a4 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts @@ -0,0 +1,266 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const issueAttachmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add attachment to issue', + }, + { + name: 'Get', + value: 'get', + description: 'Get an attachment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all attachments', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove an attachment', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const issueAttachmentFields = [ + + /* -------------------------------------------------------------------------- */ + /* issueAttachment:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Binary Property', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'add', + ], + }, + }, + name: 'binaryPropertyName', + type: 'string', + default: 'data', + description: 'Object property name which holds binary data.', + required: true, + }, + + /* -------------------------------------------------------------------------- */ + /* issueAttachment:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'The ID of the attachment.', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'get', + ], + download: [ + true, + ], + }, + }, + description: 'Object property name which holds binary data.', + required: true, + }, + /* -------------------------------------------------------------------------- */ + /* issueAttachment:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Issue Key', + name: 'issueKey', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'Issue Key', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + default: 'data', + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'getAll', + ], + download: [ + true, + ], + }, + }, + description: 'Object property name which holds binary data.', + required: true, + }, + /* -------------------------------------------------------------------------- */ + /* issueAttachment:remove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Attachment ID', + name: 'attachmentId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'issueAttachment', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'The ID of the attachment.', + }, +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts b/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts index 11d12a73a5..e4a491c861 100644 --- a/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueCommentDescription.ts @@ -485,4 +485,4 @@ export const issueCommentFields = [ }, ], }, -] as INodeProperties[]; +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueDescription.ts b/packages/nodes-base/nodes/Jira/IssueDescription.ts index 53e5c5224f..f67375477f 100644 --- a/packages/nodes-base/nodes/Jira/IssueDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueDescription.ts @@ -1,4 +1,4 @@ -import { +import { INodeProperties, } from 'n8n-workflow'; @@ -63,9 +63,9 @@ export const issueOperations = [ export const issueFields = [ -/* -------------------------------------------------------------------------- */ -/* issue:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Project', name: 'project', @@ -155,7 +155,6 @@ export const issueFields = [ loadOptionsMethod: 'getUsers', }, default: '', - required : false, description: 'Assignee', }, { @@ -163,9 +162,46 @@ export const issueFields = [ name: 'description', type: 'string', default: '', - required : false, description: 'Description', }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + loadOptionsDependsOn: [ + 'project', + ], + }, + description: 'ID of the field to set.', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + description: 'Value of the field to set.', + default: '', + }, + ], + }, + ], + }, { displayName: 'Labels', name: 'labels', @@ -174,9 +210,8 @@ export const issueFields = [ loadOptionsMethod: 'getLabels', }, default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'cloud', @@ -189,9 +224,8 @@ export const issueFields = [ name: 'serverLabels', type: 'string', default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'server', @@ -206,7 +240,6 @@ export const issueFields = [ displayName: 'Parent Issue Key', name: 'parentIssueKey', type: 'string', - required: false, default: '', description: 'Parent Issue Key', }, @@ -218,7 +251,6 @@ export const issueFields = [ loadOptionsMethod: 'getPriorities', }, default: '', - required : false, description: 'Priority', }, { @@ -226,16 +258,15 @@ export const issueFields = [ name: 'updateHistory', type: 'boolean', default: false, - required : false, description: `Whether the project in which the issue is created is added to the user's
Recently viewed project list, as shown under Projects in Jira.`, }, ], }, -/* -------------------------------------------------------------------------- */ -/* issue:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -279,7 +310,6 @@ export const issueFields = [ loadOptionsMethod: 'getUsers', }, default: '', - required : false, description: 'Assignee', }, { @@ -287,14 +317,50 @@ export const issueFields = [ name: 'description', type: 'string', default: '', - required : false, description: 'Description', }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'fieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + loadOptionsDependsOn: [ + 'issueKey', + ], + }, + description: 'ID of the field to set.', + default: '', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + description: 'Value of the field to set.', + default: '', + }, + ], + }, + ], + }, { displayName: 'Issue Type', name: 'issueType', type: 'options', - required: false, typeOptions: { loadOptionsMethod: 'getIssueTypes', }, @@ -309,9 +375,8 @@ export const issueFields = [ loadOptionsMethod: 'getLabels', }, default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'cloud', @@ -324,9 +389,8 @@ export const issueFields = [ name: 'serverLabels', type: 'string', default: [], - required : false, description: 'Labels', - displayOptions: { + displayOptions: { show: { '/jiraVersion': [ 'server', @@ -341,7 +405,6 @@ export const issueFields = [ displayName: 'Parent Issue Key', name: 'parentIssueKey', type: 'string', - required: false, default: '', description: 'Parent Issue Key', }, @@ -353,14 +416,12 @@ export const issueFields = [ loadOptionsMethod: 'getPriorities', }, default: '', - required : false, description: 'Priority', }, { displayName: 'Summary', name: 'summary', type: 'string', - required: false, default: '', description: 'Summary', }, @@ -371,16 +432,15 @@ export const issueFields = [ typeOptions: { loadOptionsMethod: 'getTransitions', }, - required: false, default: '', description: 'The ID of the issue status.', }, ], }, -/* -------------------------------------------------------------------------- */ -/* issue:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -418,9 +478,9 @@ export const issueFields = [ description: 'Delete Subtasks', }, -/* -------------------------------------------------------------------------- */ -/* issue:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -460,7 +520,6 @@ export const issueFields = [ displayName: 'Expand', name: 'expand', type: 'string', - required: false, default: '', description: `Use expand to include additional information about the issues in the response.
This parameter accepts a comma-separated list. Expand options include:
@@ -477,7 +536,6 @@ export const issueFields = [ displayName: 'Fields', name: 'fields', type: 'string', - required: false, default: '', description: `A list of fields to return for the issue.
This parameter accepts a comma-separated list.
@@ -490,7 +548,6 @@ export const issueFields = [ displayName: 'Fields By Key', name: 'fieldsByKey', type: 'boolean', - required: false, default: false, description: `Indicates whether fields in fields are referenced by keys rather than IDs.
This parameter is useful where fields have been added by a connect app and a field's key
@@ -500,7 +557,6 @@ export const issueFields = [ displayName: 'Properties', name: 'properties', type: 'string', - required: false, default: '', description: `A list of issue properties to return for the issue.
This parameter accepts a comma-separated list. Allowed values:
@@ -516,7 +572,6 @@ export const issueFields = [ displayName: 'Update History', name: 'updateHistory', type: 'boolean', - required: false, default: false, description: `Whether the project in which the issue is created is added to the user's Recently viewed project list, as shown under Projects in Jira. This also populates the @@ -525,9 +580,9 @@ export const issueFields = [ ], }, -/* -------------------------------------------------------------------------- */ -/* issue:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -649,7 +704,6 @@ export const issueFields = [ displayName: 'Fields By Key', name: 'fieldsByKey', type: 'boolean', - required: false, default: false, description: `Indicates whether fields in fields are referenced by keys rather than IDs.
This parameter is useful where fields have been added by a connect app and a field's key
@@ -667,9 +721,9 @@ export const issueFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* issue:changelog */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:changelog */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -729,9 +783,9 @@ export const issueFields = [ default: 50, description: 'How many results to return.', }, -/* -------------------------------------------------------------------------- */ -/* issue:notify */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:notify */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -791,7 +845,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, default: '', description: 'The HTML body of the email notification for the issue.', }, @@ -799,7 +852,6 @@ export const issueFields = [ displayName: 'Subject', name: 'subject', type: 'string', - required: false, default: '', description: `The subject of the email notification for the issue. If this is not specified, then the subject is set to the issue key and summary.`, @@ -811,7 +863,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, default: '', description: `The subject of the email notification for the issue. If this is not specified, then the subject is set to the issue key and summary.`, @@ -906,7 +957,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, displayOptions: { show: { resource: [ @@ -983,7 +1033,6 @@ export const issueFields = [ typeOptions: { alwaysOpenEditWindow: true, }, - required: false, displayOptions: { show: { resource: [ @@ -1001,9 +1050,9 @@ export const issueFields = [ description: 'Restricts the notifications to users with the specified permissions.', }, -/* -------------------------------------------------------------------------- */ -/* issue:transitions */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* issue:transitions */ + /* -------------------------------------------------------------------------- */ { displayName: 'Issue Key', name: 'issueKey', @@ -1043,7 +1092,6 @@ export const issueFields = [ displayName: 'Expand', name: 'expand', type: 'string', - required: false, default: '', description: `Use expand to include additional information about transitions in the response.
This parameter accepts transitions.fields, which returns information about the fields in the
@@ -1054,7 +1102,6 @@ export const issueFields = [ displayName: 'Transition ID', name: 'transitionId', type: 'string', - required: false, default: '', description: 'The ID of the transition.', }, @@ -1062,11 +1109,10 @@ export const issueFields = [ displayName: 'Skip Remote Only Condition', name: 'skipRemoteOnlyCondition', type: 'boolean', - required: false, default: false, description: `Indicates whether transitions with the condition Hide
From User Condition are included in the response.`, }, ], }, -] as INodeProperties[]; +] as INodeProperties[]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/IssueInterface.ts b/packages/nodes-base/nodes/Jira/IssueInterface.ts index fd7a948e29..a3a75f2d59 100644 --- a/packages/nodes-base/nodes/Jira/IssueInterface.ts +++ b/packages/nodes-base/nodes/Jira/IssueInterface.ts @@ -1,6 +1,6 @@ import { IDataObject, - } from 'n8n-workflow'; +} from 'n8n-workflow'; export interface IFields { assignee?: IDataObject; diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index da959be857..6a89019268 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -1,8 +1,11 @@ import { + BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; import { + IBinaryData, + IBinaryKeyData, IDataObject, ILoadOptionsFunctions, INodeExecutionData, @@ -17,10 +20,15 @@ import { validateJSON, } from './GenericFunctions'; +import { + issueAttachmentFields, + issueAttachmentOperations, +} from './IssueAttachmentDescription'; + import { issueCommentFields, issueCommentOperations, - } from './IssueCommentDescription'; +} from './IssueCommentDescription'; import { issueFields, @@ -33,13 +41,13 @@ import { INotificationRecipients, INotify, NotificationRecipientsRestrictions, - } from './IssueInterface'; +} from './IssueInterface'; export class Jira implements INodeType { description: INodeTypeDescription = { displayName: 'Jira Software', name: 'jira', - icon: 'file:jira.png', + icon: 'file:jira.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -101,6 +109,11 @@ export class Jira implements INodeType { value: 'issue', description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask', }, + { + name: 'Issue Attachment', + value: 'issueAttachment', + description: 'Add, remove, and get an attachment from an issue.', + }, { name: 'Issue Comment', value: 'issueComment', @@ -112,6 +125,8 @@ export class Jira implements INodeType { }, ...issueOperations, ...issueFields, + ...issueAttachmentOperations, + ...issueAttachmentFields, ...issueCommentOperations, ...issueCommentFields, ], @@ -176,7 +191,7 @@ export class Jira implements INodeType { } } else { for (const issueType of issueTypes) { - if (issueType.scope === undefined || issueType.scope.project.id === projectId) { + if (issueType.scope !== undefined && issueType.scope.project.id === projectId) { const issueTypeName = issueType.name; const issueTypeId = issueType.id; @@ -193,7 +208,6 @@ export class Jira implements INodeType { if (a.name > b.name) { return 1; } return 0; }); - return returnData; }, @@ -342,6 +356,32 @@ export class Jira implements INodeType { return returnData; }, + + // Get all the custom fields to display them to user so that he can + // select them easily + async getCustomFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const operation = this.getCurrentNodeParameter('operation') as string; + let projectId; + if (operation === 'create') { + projectId = this.getCurrentNodeParameter('project'); + } else { + const issueKey = this.getCurrentNodeParameter('issueKey'); + const { fields: { project: { id } } } = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, {}); + projectId = id; + } + + const fields = await jiraSoftwareCloudApiRequest.call(this, `/api/2/field`, 'GET'); + for (const field of fields) { + if (field.custom === true && field.scope && field.scope.project && field.scope.project.id === projectId) { + returnData.push({ + name: field.name, + value: field.id, + }); + } + } + return returnData; + }, }, }; @@ -356,11 +396,10 @@ export class Jira implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; const jiraVersion = this.getNodeParameter('jiraVersion', 0) as string; - - for (let i = 0; i < length; i++) { - if (resource === 'issue') { - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post - if (operation === 'create') { + if (resource === 'issue') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-post + if (operation === 'create') { + for (let i = 0; i < length; i++) { const summary = this.getNodeParameter('summary', i) as string; const projectId = this.getNodeParameter('project', i) as string; const issueTypeId = this.getNodeParameter('issueType', i) as string; @@ -403,6 +442,13 @@ export class Jira implements INodeType { if (additionalFields.updateHistory) { qs.updateHistory = additionalFields.updateHistory as boolean; } + if (additionalFields.customFieldsUi) { + const customFields = (additionalFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + if (customFields) { + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); + Object.assign(fields, data); + } + } const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body, qs); const subtaskIssues = []; for (const issueType of issueTypes) { @@ -422,9 +468,12 @@ export class Jira implements INodeType { } body.fields = fields; responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issue', 'POST', body); + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put - if (operation === 'update') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put + if (operation === 'update') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; const body: IIssue = {}; @@ -462,6 +511,13 @@ export class Jira implements INodeType { if (updateFields.description) { fields.description = updateFields.description as string; } + if (updateFields.customFieldsUi) { + const customFields = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + if (customFields) { + const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }), {}); + Object.assign(fields, data); + } + } const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body); const subtaskIssues = []; for (const issueType of issueTypes) { @@ -486,10 +542,12 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'PUT', body); - responseData = { success: true }; + returnData.push({ success: true }); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get - if (operation === 'get') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.fields) { @@ -507,12 +565,13 @@ export class Jira implements INodeType { if (additionalFields.updateHistory) { qs.updateHistory = additionalFields.updateHistory as string; } - responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post - if (operation === 'getAll') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; const body: IDataObject = {}; @@ -533,21 +592,27 @@ export class Jira implements INodeType { responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/search`, 'POST', body); responseData = responseData.issues; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get - if (operation === 'changelog') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-changelog-get + if (operation === 'changelog') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; if (returnAll) { - responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/api/2/issue/${issueKey}/changelog`, 'GET'); + responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values', `/api/2/issue/${issueKey}/changelog`, 'GET'); } else { qs.maxResults = this.getNodeParameter('limit', i) as number; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/changelog`, 'GET', {}, qs); responseData = responseData.values; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post - if (operation === 'notify') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-notify-post + if (operation === 'notify') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const jsonActive = this.getNodeParameter('jsonParameters', 0) as boolean; @@ -606,7 +671,7 @@ export class Jira implements INodeType { const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {}; if (notificationRecipientsRestrictionsValues) { // @ts-ignore - if (notificationRecipientsRestrictionsValues.groups. length > 0) { + if (notificationRecipientsRestrictionsValues.groups.length > 0) { // @ts-ignore notificationRecipientsRestrictions.groups = notificationRecipientsRestrictionsValues.groups.map(group => { return { @@ -627,10 +692,12 @@ export class Jira implements INodeType { } } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/notify`, 'POST', body, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get - if (operation === 'transitions') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get + if (operation === 'transitions') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; if (additionalFields.transitionId) { @@ -644,19 +711,118 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/transitions`, 'GET', {}, qs); responseData = responseData.transitions; - + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete - if (operation === 'delete') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-delete + if (operation === 'delete') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean; qs.deleteSubtasks = deleteSubtasks; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'DELETE', {}, qs); + returnData.push({ success: true }); } } - if (resource === 'issueComment') { - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post - if (operation === 'add') { + } + if (resource === 'issueAttachment') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-issue-issueidorkey-attachments-post + if (operation === 'add') { + for (let i = 0; i < length; i++) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const issueKey = this.getNodeParameter('issueKey', i) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await jiraSoftwareCloudApiRequest.call( + this, + `/api/3/issue/${issueKey}/attachments`, + 'POST', + {}, + {}, + undefined, + { + formData: { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + }, + }, + ); + returnData.push.apply(returnData, responseData); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-delete + if (operation === 'remove') { + for (let i = 0; i < length; i++) { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/attachment/${attachmentId}`, 'DELETE', {}, qs); + returnData.push({ success: true }); + } + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-get + if (operation === 'get') { + const download = this.getNodeParameter('download', 0) as boolean; + for (let i = 0; i < length; i++) { + const attachmentId = this.getNodeParameter('attachmentId', i) as string; + responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/attachment/${attachmentId}`, 'GET', {}, qs); + returnData.push({ json: responseData }); + } + if (download) { + const binaryPropertyName = this.getNodeParameter('binaryProperty', 0) as string; + for (const [index, attachment] of returnData.entries()) { + returnData[index]['binary'] = {}; + //@ts-ignore + const buffer = await jiraSoftwareCloudApiRequest.call(this, '', 'GET', {}, {}, attachment?.json!.content, { json: false, encoding: null }); + //@ts-ignore + returnData[index]['binary'][binaryPropertyName] = await this.helpers.prepareBinaryData(buffer, attachment.json.filename, attachment.json.mimeType); + } + } + } + if (operation === 'getAll') { + const download = this.getNodeParameter('download', 0) as boolean; + for (let i = 0; i < length; i++) { + const issueKey = this.getNodeParameter('issueKey', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const { fields: { attachment } } = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, qs); + responseData = attachment; + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + responseData = responseData.map((data: IDataObject) => ({ json: data })); + returnData.push.apply(returnData, responseData); + } + if (download) { + const binaryPropertyName = this.getNodeParameter('binaryProperty', 0) as string; + for (const [index, attachment] of returnData.entries()) { + returnData[index]['binary'] = {}; + //@ts-ignore + const buffer = await jiraSoftwareCloudApiRequest.call(this, '', 'GET', {}, {}, attachment.json.content, { json: false, encoding: null }); + //@ts-ignore + returnData[index]['binary'][binaryPropertyName] = await this.helpers.prepareBinaryData(buffer, attachment.json.filename, attachment.json.mimeType); + } + } + } + } + + if (resource === 'issueComment') { + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post + if (operation === 'add') { + for (let i = 0; i < length; i++) { const jsonParameters = this.getNodeParameter('jsonParameters', 0) as boolean; const issueKey = this.getNodeParameter('issueKey', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; @@ -697,18 +863,23 @@ export class Jira implements INodeType { } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'POST', body, qs); + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get - if (operation === 'get') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get + if (operation === 'get') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; Object.assign(qs, options); responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'GET', {}, qs); - + returnData.push(responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get - if (operation === 'getAll') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; @@ -722,16 +893,21 @@ export class Jira implements INodeType { responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'GET', body, qs); responseData = responseData.comments; } + returnData.push.apply(returnData, responseData); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-delete - if (operation === 'remove') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-delete + if (operation === 'remove') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'DELETE', {}, qs); - responseData = { success: true }; + returnData.push({ success: true }); } - //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put - if (operation === 'update') { + } + //https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put + if (operation === 'update') { + for (let i = 0; i < length; i++) { const issueKey = this.getNodeParameter('issueKey', i) as string; const commentId = this.getNodeParameter('commentId', i) as string; const options = this.getNodeParameter('options', i) as IDataObject; @@ -771,14 +947,15 @@ export class Jira implements INodeType { Object.assign(body, { body: json }); } responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'PUT', body, qs); + returnData.push(responseData); } } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else { - returnData.push(responseData as IDataObject); - } } - return [this.helpers.returnJsonArray(returnData)]; + + if (resource === 'issueAttachment' && (operation === 'getAll' || operation === 'get')) { + return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } } -} +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts index 72923f9ae8..68ada4ecf0 100644 --- a/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts +++ b/packages/nodes-base/nodes/Jira/JiraTrigger.node.ts @@ -23,7 +23,7 @@ export class JiraTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Jira Trigger', name: 'jiraTrigger', - icon: 'file:jira.png', + icon: 'file:jira.svg', group: ['trigger'], version: 1, description: 'Starts the workflow when Jira events occurs.', diff --git a/packages/nodes-base/nodes/Jira/jira.png b/packages/nodes-base/nodes/Jira/jira.png deleted file mode 100644 index 977265189f..0000000000 Binary files a/packages/nodes-base/nodes/Jira/jira.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Jira/jira.svg b/packages/nodes-base/nodes/Jira/jira.svg new file mode 100644 index 0000000000..c1ee7fe198 --- /dev/null +++ b/packages/nodes-base/nodes/Jira/jira.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Reddit/GenericFunctions.ts b/packages/nodes-base/nodes/Reddit/GenericFunctions.ts new file mode 100644 index 0000000000..32aa3d899f --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/GenericFunctions.ts @@ -0,0 +1,145 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +/** + * Make an authenticated or unauthenticated API request to Reddit. + */ +export async function redditApiRequest( + this: IHookFunctions | IExecuteFunctions, + method: string, + endpoint: string, + qs: IDataObject, +): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + + const authRequired = ['profile', 'post', 'postComment'].includes(resource); + + qs.api_type = 'json'; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + }, + method, + uri: authRequired ? `https://oauth.reddit.com/${endpoint}` : `https://www.reddit.com/${endpoint}`, + qs, + json: true, + }; + + if (!Object.keys(qs).length) { + delete options.qs; + } + + if (authRequired) { + let response; + + try { + response = await this.helpers.requestOAuth2.call(this, 'redditOAuth2Api', options); + } catch (error) { + if (error.response.body && error.response.body.message) { + const message = error.response.body.message; + throw new Error(`Reddit error response [${error.statusCode}]: ${message}`); + } + } + + if ((response.errors && response.errors.length !== 0) || (response.json && response.json.errors && response.json.errors.length !== 0)) { + const errors = response?.errors || response?.json?.errors; + const errorMessage = errors.map((error: []) => error.join('-')); + + throw new Error(`Reddit error response [400]: ${errorMessage.join('|')}`); + } + + return response; + + } else { + + try { + return await this.helpers.request.call(this, options); + } catch (error) { + const errorMessage = error?.response?.body?.message; + if (errorMessage) { + throw new Error(`Reddit error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } + } +} + +/** + * Make an unauthenticated API request to Reddit and return all results. + */ +export async function redditApiRequestAllItems( + this: IHookFunctions | IExecuteFunctions, + method: string, + endpoint: string, + qs: IDataObject, +): Promise { // tslint:disable-line:no-any + + let responseData; + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + + qs.limit = 100; + + do { + responseData = await redditApiRequest.call(this, method, endpoint, qs); + if (!Array.isArray(responseData)) { + qs.after = responseData.data.after; + } + + if (endpoint === 'api/search_subreddits.json') { + responseData.subreddits.forEach((child: any) => returnData.push(child)); // tslint:disable-line:no-any + } else if (resource === 'postComment' && operation === 'getAll') { + responseData[1].data.children.forEach((child: any) => returnData.push(child.data)); // tslint:disable-line:no-any + } else { + responseData.data.children.forEach((child: any) => returnData.push(child.data)); // tslint:disable-line:no-any + } + if (qs.limit && returnData.length >= qs.limit && returnAll === false) { + return returnData; + } + + } while (responseData.data && responseData.data.after); + + return returnData; +} + +/** + * Handles a large Reddit listing by returning all items or up to a limit. + */ +export async function handleListing( + this: IExecuteFunctions, + i: number, + endpoint: string, + qs: IDataObject = {}, + requestMethod: 'GET' | 'POST' = 'GET', +): Promise { // tslint:disable-line:no-any + + let responseData; + + const returnAll = this.getNodeParameter('returnAll', i); + + if (returnAll) { + responseData = await redditApiRequestAllItems.call(this, requestMethod, endpoint, qs); + } else { + const limit = this.getNodeParameter('limit', i); + qs.limit = limit; + responseData = await redditApiRequestAllItems.call(this, requestMethod, endpoint, qs); + responseData = responseData.slice(0, limit); + } + + return responseData; +} diff --git a/packages/nodes-base/nodes/Reddit/PostCommentDescription.ts b/packages/nodes-base/nodes/Reddit/PostCommentDescription.ts new file mode 100644 index 0000000000..ec2880df7b --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/PostCommentDescription.ts @@ -0,0 +1,231 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postCommentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'create', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a top-level comment in a post', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all comments in a post', + }, + { + name: 'Delete', + value: 'delete', + description: 'Remove a comment from a post', + }, + { + name: 'Reply', + value: 'reply', + description: 'Write a reply to a comment in a post', + }, + ], + displayOptions: { + show: { + resource: [ + 'postComment', + ], + }, + }, + }, +] as INodeProperties[]; + +export const postCommentFields = [ + // ---------------------------------- + // postComment: create + // ---------------------------------- + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + default: '', + description: 'ID of the post to write the comment to. Found in the post URL:
/r/[subreddit_name]/comments/[post_id]/[post_title]', + placeholder: 'l0me7x', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Comment Text', + name: 'commentText', + type: 'string', + required: true, + default: '', + description: 'Text of the comment. Markdown supported.', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------- + // postComment: getAll + // ---------------------------------- + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'The name of subreddit where the post is.', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + default: '', + description: 'ID of the post to get all comments from. Found in the post URL:
/r/[subreddit_name]/comments/[post_id]/[post_title]', + placeholder: 'l0me7x', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // postComment: delete + // ---------------------------------- + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + required: true, + default: '', + description: 'ID of the comment to remove. Found in the comment URL:
/r/[subreddit_name]/comments/[post_id]/[post_title]/[comment_id]', + placeholder: 'gla7fmt', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // postComment: reply + // ---------------------------------- + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + required: true, + default: '', + description: 'ID of the comment to reply to. To be found in the comment URL:
www.reddit.com/r/[subreddit_name]/comments/[post_id]/[post_title]/[comment_id]', + placeholder: 'gl9iroa', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'reply', + ], + }, + }, + }, + { + displayName: 'Reply Text', + name: 'replyText', + type: 'string', + required: true, + default: '', + description: 'Text of the reply. Markdown supported.', + displayOptions: { + show: { + resource: [ + 'postComment', + ], + operation: [ + 'reply', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/PostDescription.ts b/packages/nodes-base/nodes/Reddit/PostDescription.ts new file mode 100644 index 0000000000..cce6a0f582 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/PostDescription.ts @@ -0,0 +1,351 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const postOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'create', + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + description: 'Submit a post to a subreddit', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a post from a subreddit', + }, + { + name: 'Get', + value: 'get', + description: 'Get a post from a subreddit', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all posts from a subreddit', + }, + ], + displayOptions: { + show: { + resource: [ + 'post', + ], + }, + }, + }, +] as INodeProperties[]; + +export const postFields = [ + // ---------------------------------- + // post: create + // ---------------------------------- + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'Subreddit to create the post in.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Kind', + name: 'kind', + type: 'options', + options: [ + { + name: 'Text Post', + value: 'self', + }, + { + name: 'Link Post', + value: 'link', + }, + { + name: 'Image Post', + value: 'image', + }, + ], + default: 'self', + description: 'The kind of the post to create.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + description: 'Title of the post, up to 300 characters long.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + required: true, + default: '', + description: 'URL of the post.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + kind: [ + 'link', + 'image', + ], + }, + }, + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + description: 'Text of the post. Markdown supported.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + kind: [ + 'self', + ], + }, + }, + }, + { + displayName: 'Resubmit', + name: 'resubmit', + type: 'boolean', + default: false, + description: 'If toggled on, the URL will be posted even if
it was already posted to the subreddit before.
Otherwise, the re-posting will trigger an error.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'create', + ], + kind: [ + 'link', + 'image', + ], + }, + }, + }, + + // ---------------------------------- + // post: delete + // ---------------------------------- + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + default: '', + description: 'ID of the post to delete. Found in the post URL:
/r/[subreddit_name]/comments/[post_id]/[post_title]', + placeholder: 'gla7fmt', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------- + // post: get + // ---------------------------------- + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'The name of subreddit to retrieve the post from.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Post ID', + name: 'postId', + type: 'string', + required: true, + default: '', + description: 'ID of the post to retrieve. Found in the post URL:
/r/[subreddit_name]/comments/[post_id]/[post_title]', + placeholder: 'l0me7x', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // post: getAll + // ---------------------------------- + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'The name of subreddit to retrieve the posts from.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'post', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Category', + name: 'category', + type: 'options', + required: true, + default: 'top', + description: 'Category of the posts to retrieve.', + options: [ + { + name: 'Top Posts', + value: 'top', + }, + { + name: 'Hot Posts', + value: 'hot', + }, + { + name: 'New Posts', + value: 'new', + }, + { + name: 'Rising Posts', + value: 'rising', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/ProfileDescription.ts b/packages/nodes-base/nodes/Reddit/ProfileDescription.ts new file mode 100644 index 0000000000..1ff0730603 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/ProfileDescription.ts @@ -0,0 +1,80 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const profileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'profile', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + description: 'Operation to perform', + }, +] as INodeProperties[]; + + +export const profileFields = [ + { + displayName: 'Details', + name: 'details', + type: 'options', + required: true, + default: 'identity', + description: 'Details of my account to retrieve.', + options: [ + { + name: 'Identity', + value: 'identity', + description: 'Return the identity of the logged-in user', + }, + { + name: 'Blocked Users', + value: 'blockedUsers', + description: 'Return the blocked users of the logged-in user', + }, + { + name: 'Friends', + value: 'friends', + description: 'Return the friends of the logged-in user', + }, + { + name: 'Karma', + value: 'karma', + description: 'Return the subreddit karma for the logged-in user', + }, + { + name: 'Preferences', + value: 'prefs', + description: 'Return the settings preferences of the logged-in user', + }, + { + name: 'Trophies', + value: 'trophies', + description: 'Return the trophies of the logged-in user', + }, + ], + displayOptions: { + show: { + resource: [ + 'profile', + ], + operation: [ + 'get', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/Reddit.node.ts b/packages/nodes-base/nodes/Reddit/Reddit.node.ts new file mode 100644 index 0000000000..29d41a39d3 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/Reddit.node.ts @@ -0,0 +1,425 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + handleListing, + redditApiRequest, +} from './GenericFunctions'; + +import { + postCommentFields, + postCommentOperations, +} from './PostCommentDescription'; + +import { + postFields, + postOperations, +} from './PostDescription'; + +import { + profileFields, + profileOperations, +} from './ProfileDescription'; + +import { + subredditFields, + subredditOperations, +} from './SubredditDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + +export class Reddit implements INodeType { + description: INodeTypeDescription = { + displayName: 'Reddit', + name: 'reddit', + icon: 'file:reddit.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Reddit API', + defaults: { + name: 'Reddit', + color: '#ff5700', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'redditOAuth2Api', + required: true, + displayOptions: { + show: { + resource: [ + 'postComment', + 'post', + 'profile', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Post', + value: 'post', + }, + { + name: 'Post Comment', + value: 'postComment', + }, + { + name: 'Profile', + value: 'profile', + }, + { + name: 'Subreddit', + value: 'subreddit', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'post', + description: 'Resource to consume', + }, + ...postCommentOperations, + ...postCommentFields, + ...profileOperations, + ...profileFields, + ...subredditOperations, + ...subredditFields, + ...postOperations, + ...postFields, + ...userOperations, + ...userFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + // ********************************************************************* + // post + // ********************************************************************* + + if (resource === 'post') { + + if (operation === 'create') { + // ---------------------------------- + // post: create + // ---------------------------------- + + // https://www.reddit.com/dev/api/#POST_api_submit + + const qs: IDataObject = { + title: this.getNodeParameter('title', i), + sr: this.getNodeParameter('subreddit', i), + kind: this.getNodeParameter('kind', i), + }; + + qs.kind === 'self' + ? qs.text = this.getNodeParameter('text', i) + : qs.url = this.getNodeParameter('url', i); + + if (qs.url) { + qs.resubmit = this.getNodeParameter('resubmit', i); + } + + responseData = await redditApiRequest.call(this, 'POST', 'api/submit', qs); + + responseData = responseData.json.data; + + } else if (operation === 'delete') { + // ---------------------------------- + // post: delete + // ---------------------------------- + + // https://www.reddit.com/dev/api/#POST_api_del + + const postTypePrefix = 't3_'; + + const qs: IDataObject = { + id: postTypePrefix + this.getNodeParameter('postId', i), + }; + + await redditApiRequest.call(this, 'POST', 'api/del', qs); + + responseData = { success: true }; + + } else if (operation === 'get') { + // ---------------------------------- + // post: get + // ---------------------------------- + + const subreddit = this.getNodeParameter('subreddit', i); + const postId = this.getNodeParameter('postId', i) as string; + const endpoint = `r/${subreddit}/comments/${postId}.json`; + + responseData = await redditApiRequest.call(this, 'GET', endpoint, {}); + responseData = responseData[0].data.children[0].data; + + } else if (operation === 'getAll') { + // ---------------------------------- + // post: getAll + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_hot + // https://www.reddit.com/dev/api/#GET_new + // https://www.reddit.com/dev/api/#GET_rising + // https://www.reddit.com/dev/api/#GET_{sort} + + const subreddit = this.getNodeParameter('subreddit', i); + let endpoint = `r/${subreddit}.json`; + + const { category } = this.getNodeParameter('filters', i) as { category: string }; + if (category) { + endpoint = `r/${subreddit}/${category}.json`; + } + + responseData = await handleListing.call(this, i, endpoint); + + } + + } else if (resource === 'postComment') { + + // ********************************************************************* + // postComment + // ********************************************************************* + + if (operation === 'create') { + // ---------------------------------- + // postComment: create + // ---------------------------------- + + // https://www.reddit.com/dev/api/#POST_api_comment + + const postTypePrefix = 't3_'; + + const qs: IDataObject = { + text: this.getNodeParameter('commentText', i), + thing_id: postTypePrefix + this.getNodeParameter('postId', i), + }; + + responseData = await redditApiRequest.call(this, 'POST', 'api/comment', qs); + responseData = responseData.json.data.things[0].data; + + } else if (operation === 'getAll') { + // ---------------------------------- + // postComment: getAll + // ---------------------------------- + + // https://www.reddit.com/r/{subrreddit}/comments/{postId}.json + + const subreddit = this.getNodeParameter('subreddit', i); + const postId = this.getNodeParameter('postId', i) as string; + const endpoint = `r/${subreddit}/comments/${postId}.json`; + + responseData = await handleListing.call(this, i, endpoint); + + } else if (operation === 'delete') { + // ---------------------------------- + // postComment: delete + // ---------------------------------- + + // https://www.reddit.com/dev/api/#POST_api_del + + const commentTypePrefix = 't1_'; + + const qs: IDataObject = { + id: commentTypePrefix + this.getNodeParameter('commentId', i), + }; + + await redditApiRequest.call(this, 'POST', 'api/del', qs); + + responseData = { success: true }; + + } else if (operation === 'reply') { + // ---------------------------------- + // postComment: reply + // ---------------------------------- + + // https://www.reddit.com/dev/api/#POST_api_comment + + const commentTypePrefix = 't1_'; + + const qs: IDataObject = { + text: this.getNodeParameter('replyText', i), + thing_id: commentTypePrefix + this.getNodeParameter('commentId', i), + }; + + responseData = await redditApiRequest.call(this, 'POST', 'api/comment', qs); + responseData = responseData.json.data.things[0].data; + } + + } else if (resource === 'profile') { + // ********************************************************************* + // pprofile + // ********************************************************************* + + if (operation === 'get') { + // ---------------------------------- + // profile: get + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_api_v1_me + // https://www.reddit.com/dev/api/#GET_api_v1_me_karma + // https://www.reddit.com/dev/api/#GET_api_v1_me_prefs + // https://www.reddit.com/dev/api/#GET_api_v1_me_trophies + // https://www.reddit.com/dev/api/#GET_prefs_{where} + + const endpoints: { [key: string]: string } = { + identity: 'me', + blockedUsers: 'me/blocked', + friends: 'me/friends', + karma: 'me/karma', + prefs: 'me/prefs', + trophies: 'me/trophies', + }; + + const details = this.getNodeParameter('details', i) as string; + const endpoint = `api/v1/${endpoints[details]}`; + responseData = await redditApiRequest.call(this, 'GET', endpoint, {}); + + if (details === 'identity') { + responseData = responseData.features; + } else if (details === 'friends') { + responseData = responseData.data.children; + if (!responseData.length) { + throw new Error('Reddit error response [404]: Not Found'); + } + } else if (details === 'karma') { + responseData = responseData.data; + if (!responseData.length) { + throw new Error('Reddit error response [404]: Not Found'); + } + } else if (details === 'trophies') { + responseData = responseData.data.trophies.map((trophy: IDataObject) => trophy.data); + } + } + + } else if (resource === 'subreddit') { + + // ********************************************************************* + // subreddit + // ********************************************************************* + + if (operation === 'get') { + // ---------------------------------- + // subreddit: get + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_r_{subreddit}_about + // https://www.reddit.com/dev/api/#GET_r_{subreddit}_about_rules + + const subreddit = this.getNodeParameter('subreddit', i); + const content = this.getNodeParameter('content', i) as string; + const endpoint = `r/${subreddit}/about/${content}.json`; + + responseData = await redditApiRequest.call(this, 'GET', endpoint, {}); + + if (content === 'rules') { + responseData = responseData.rules; + } else if (content === 'about') { + responseData = responseData.data; + } + + } else if (operation === 'getAll') { + // ---------------------------------- + // subreddit: getAll + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_api_trending_subreddits + // https://www.reddit.com/dev/api/#POST_api_search_subreddits + // https://www.reddit.com/r/subreddits.json + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (filters.trending) { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const endpoint = 'api/trending_subreddits.json'; + responseData = await redditApiRequest.call(this, 'GET', endpoint, {}); + responseData = responseData.subreddit_names.map((name: string) => ({ name })); + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.splice(0, limit); + } + + } else if (filters.keyword) { + const qs: IDataObject = {}; + qs.query = filters.keyword; + + const endpoint = 'api/search_subreddits.json'; + responseData = await redditApiRequest.call(this, 'POST', endpoint, qs); + + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + responseData = responseData.subreddits.splice(0, limit); + } + } else { + const endpoint = 'r/subreddits.json'; + responseData = await handleListing.call(this, i, endpoint); + } + } + + } else if (resource === 'user') { + // ********************************************************************* + // user + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // user: get + // ---------------------------------- + + // https://www.reddit.com/dev/api/#GET_user_{username}_{where} + + const username = this.getNodeParameter('username', i) as string; + const details = this.getNodeParameter('details', i) as string; + const endpoint = `user/${username}/${details}.json`; + + responseData = details === 'about' + ? await redditApiRequest.call(this, 'GET', endpoint, {}) + : await handleListing.call(this, i, endpoint); + + if (details === 'about') { + responseData = responseData.data; + } + } + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Reddit/SubredditDescription.ts b/packages/nodes-base/nodes/Reddit/SubredditDescription.ts new file mode 100644 index 0000000000..c7c2c7626f --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/SubredditDescription.ts @@ -0,0 +1,162 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const subredditOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve background information about a subreddit.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve information about subreddits from all of Reddit.', + }, + ], + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + }, + }, + }, +] as INodeProperties[]; + +export const subredditFields = [ + // ---------------------------------- + // subreddit: get + // ---------------------------------- + { + displayName: 'Content', + name: 'content', + type: 'options', + required: true, + default: 'about', + description: 'Subreddit content to retrieve.', + options: [ + { + name: 'About', + value: 'about', + }, + { + name: 'Rules', + value: 'rules', + }, + ], + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Subreddit', + name: 'subreddit', + type: 'string', + required: true, + default: '', + description: 'The name of subreddit to retrieve the content from.', + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // subreddit: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Keyword', + name: 'keyword', + type: 'string', + default: '', + description: 'The keyword for the subreddit search.', + }, + { + displayName: 'Trending', + name: 'trending', + type: 'boolean', + default: false, + description: 'Currently trending subreddits in all of Reddit.', + }, + ], + displayOptions: { + show: { + resource: [ + 'subreddit', + ], + operation: [ + 'getAll', + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/UserDescription.ts b/packages/nodes-base/nodes/Reddit/UserDescription.ts new file mode 100644 index 0000000000..e181c3c6d7 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/UserDescription.ts @@ -0,0 +1,140 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + description: 'Operation to perform', + }, +] as INodeProperties[]; + +export const userFields = [ + { + displayName: 'Username', + name: 'username', + type: 'string', + required: true, + default: '', + description: 'Reddit ID of the user to retrieve.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Details', + name: 'details', + type: 'options', + required: true, + default: 'about', + description: 'Details of the user to retrieve.', + options: [ + { + name: 'About', + value: 'about', + }, + { + name: 'Comments', + value: 'comments', + }, + { + name: 'Gilded', + value: 'gilded', + }, + { + name: 'Overview', + value: 'overview', + }, + { + name: 'Submitted', + value: 'submitted', + }, + ], + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Return all results.', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + details: [ + 'overview', + 'submitted', + 'comments', + 'gilded', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + description: 'The number of results to return.', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + details: [ + 'comments', + 'gilded', + 'overview', + 'submitted', + ], + returnAll: [ + false, + ], + }, + }, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Reddit/reddit.svg b/packages/nodes-base/nodes/Reddit/reddit.svg new file mode 100644 index 0000000000..507bbb9ed7 --- /dev/null +++ b/packages/nodes-base/nodes/Reddit/reddit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/SecurityScorecard/SecurityScorecard.node.json b/packages/nodes-base/nodes/SecurityScorecard/SecurityScorecard.node.json new file mode 100644 index 0000000000..34d0d53dd3 --- /dev/null +++ b/packages/nodes-base/nodes/SecurityScorecard/SecurityScorecard.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.securityScorecard", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/securityScorecard" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.securityScorecard/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Signl4/GenericFunctions.ts b/packages/nodes-base/nodes/Signl4/GenericFunctions.ts index 5281ae8c25..78e745e76c 100644 --- a/packages/nodes-base/nodes/Signl4/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Signl4/GenericFunctions.ts @@ -8,17 +8,26 @@ import { import { OptionsWithUri, - } from 'request'; +} from 'request'; /** * Make an API request to SIGNL4 * * @param {IHookFunctions | IExecuteFunctions} this - * @param {object} message + * @param {string} method + * @param {string} contentType + * @param {string} body + * @param {object} query + * @param {string} teamSecret + * @param {object} options * @returns {Promise} + * */ -export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, body: string, query: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('signl4Api'); + + const teamSecret = credentials?.teamSecret as string; let options: OptionsWithUri = { headers: { @@ -27,7 +36,7 @@ export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, method, body, qs: query, - uri: uri || ``, + uri: `https://connect.signl4.com/webhook/${teamSecret}`, json: true, }; diff --git a/packages/nodes-base/nodes/Signl4/Signl4.node.json b/packages/nodes-base/nodes/Signl4/Signl4.node.json index e6e06e864f..8337da9d30 100644 --- a/packages/nodes-base/nodes/Signl4/Signl4.node.json +++ b/packages/nodes-base/nodes/Signl4/Signl4.node.json @@ -1,7 +1,7 @@ { "node": "n8n-nodes-base.signl4", - "nodeVersion": "1.0", - "codexVersion": "1.0", + "nodeVersion": "1.1", + "codexVersion": "1.1", "categories": [ "Communication", "Development" diff --git a/packages/nodes-base/nodes/Signl4/Signl4.node.ts b/packages/nodes-base/nodes/Signl4/Signl4.node.ts index 2b8defc440..1a729502ab 100644 --- a/packages/nodes-base/nodes/Signl4/Signl4.node.ts +++ b/packages/nodes-base/nodes/Signl4/Signl4.node.ts @@ -264,20 +264,18 @@ export class Signl4 implements INodeType { // Send alert if (operation === 'send') { const message = this.getNodeParameter('message', i) as string; - const additionalFields = this.getNodeParameter('additionalFields',i) as IDataObject; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const data: IDataObject = { message, }; - if (additionalFields.alertingScenario) { - data['X-S4-AlertingScenario'] = additionalFields.alertingScenario as string; + if (additionalFields.title) { + data.title = additionalFields.title as string; } - if (additionalFields.externalId) { - data['X-S4-ExternalID'] = additionalFields.externalId as string; - } - if (additionalFields.filtering) { - data['X-S4-Filtering'] = (additionalFields.filtering as boolean).toString(); + + if (additionalFields.service) { + data.service = additionalFields.service as string; } if (additionalFields.locationFieldsUi) { const locationUi = (additionalFields.locationFieldsUi as IDataObject).locationFieldsValues as IDataObject; @@ -285,16 +283,25 @@ export class Signl4 implements INodeType { data['X-S4-Location'] = `${locationUi.latitude},${locationUi.longitude}`; } } - if (additionalFields.service) { - data['X-S4-Service'] = additionalFields.service as string; + + if (additionalFields.alertingScenario) { + data['X-S4-AlertingScenario'] = additionalFields.alertingScenario as string; } + + if (additionalFields.filtering) { + data['X-S4-Filtering'] = (additionalFields.filtering as boolean).toString(); + } + + if (additionalFields.externalId) { + data['X-S4-ExternalID'] = additionalFields.externalId as string; + } + data['X-S4-Status'] = 'new'; - if (additionalFields.title) { - data['title'] = additionalFields.title as string; - } + data['X-S4-SourceSystem'] = 'n8n'; + + // Attachments const attachments = additionalFields.attachmentsUi as IDataObject; - if (attachments) { if (attachments.attachmentsBinary && items[i].binary) { @@ -304,14 +311,14 @@ export class Signl4 implements INodeType { if (binaryProperty) { - const supportedFileExtension = ['png', 'jpg', 'txt']; + const supportedFileExtension = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'mp3', 'wav']; if (!supportedFileExtension.includes(binaryProperty.fileExtension as string)) { throw new Error(`Invalid extension, just ${supportedFileExtension.join(',')} are supported}`); } - data['file'] = { + data.attachment = { value: Buffer.from(binaryProperty.data, BINARY_ENCODING), options: { filename: binaryProperty.fileName, @@ -325,18 +332,14 @@ export class Signl4 implements INodeType { } } - const credentials = this.getCredentials('signl4Api'); - - const endpoint = `https://connect.signl4.com/webhook/${credentials?.teamSecret}`; - responseData = await SIGNL4ApiRequest.call( this, 'POST', '', - data, - {}, - endpoint, {}, + { + formData: data, + }, ); } // Resolve alert @@ -348,26 +351,24 @@ export class Signl4 implements INodeType { data['X-S4-Status'] = 'resolved'; - const credentials = this.getCredentials('signl4Api'); - - const endpoint = `https://connect.signl4.com/webhook/${credentials?.teamSecret}`; + data['X-S4-SourceSystem'] = 'n8n'; responseData = await SIGNL4ApiRequest.call( this, 'POST', '', - data, - {}, - endpoint, {}, + { + formData: data, + }, ); } } - } - if (Array.isArray(responseData)) { - returnData.push.apply(returnData, responseData as IDataObject[]); - } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } } return [this.helpers.returnJsonArray(returnData)]; } diff --git a/packages/nodes-base/nodes/Switch.node.ts b/packages/nodes-base/nodes/Switch.node.ts index c1c728d94f..3d18abf16a 100644 --- a/packages/nodes-base/nodes/Switch.node.ts +++ b/packages/nodes-base/nodes/Switch.node.ts @@ -487,7 +487,7 @@ export class Switch implements INodeType { smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0), startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string), regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { - const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimy]*)$')); + const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$')); let regex: RegExp; if (!regexMatch) { diff --git a/packages/nodes-base/nodes/Tapfiliate/AffiliateDescription.ts b/packages/nodes-base/nodes/Tapfiliate/AffiliateDescription.ts new file mode 100644 index 0000000000..c32d2bc876 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/AffiliateDescription.ts @@ -0,0 +1,331 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const affiliateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an affiliate', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an affiliate', + }, + { + name: 'Get', + value: 'get', + description: 'Get an affiliate by ID', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all affiliates', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const affiliateFields = [ + /* -------------------------------------------------------------------------- */ + /* affiliate:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'affiliate', + ], + }, + }, + description: `The affiliate’s email.`, + }, + { + displayName: 'First Name', + name: 'firstname', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'affiliate', + ], + }, + }, + default: '', + description: `The affiliate’s firstname.`, + }, + { + displayName: 'Last Name', + name: 'lastname', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'affiliate', + ], + }, + }, + default: '', + description: `The affiliate’s lastname.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Address', + name: 'addressUi', + placeholder: 'Address', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'addressValues', + displayName: 'Address', + values: [ + { + displayName: 'Line 1', + name: 'address', + type: 'string', + default: '', + }, + { + displayName: 'Line 2', + name: 'address_two', + type: 'string', + default: '', + }, + { + displayName: 'Postal Code', + name: 'postal_code', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Country Code', + name: 'country', + type: 'string', + default: '', + description: `The country’s ISO_3166-1 code. Codes.`, + }, + ], + }, + ], + }, + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + description: `The affiliate’s company data,`, + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* affiliate:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Affiliate ID', + name: 'affiliateId', + required: true, + type: 'string', + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + + /* -------------------------------------------------------------------------- */ + /* affiliate:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + + /* -------------------------------------------------------------------------- */ + /* affiliate:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If set to true, all the results will be returned.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'affiliate', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Affiliate Group ID', + name: 'affiliate_group_id', + type: 'string', + default: '', + description: 'Retrieves affiliates for a certain affiliate group.', + }, + { + displayName: 'Click ID', + name: 'click_id', + type: 'string', + default: '', + description: 'Click ID.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'An email address,', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: 'Retrieves children for a certain parent affiliate.', + }, + { + displayName: 'Referral Code', + name: 'referral_code', + type: 'string', + default: '', + description: 'An affiliate’s referral code. This corresponds to the value of ref= in their referral link.', + }, + { + displayName: 'Source ID', + name: 'source_id', + type: 'string', + default: '', + description: 'The Source ID.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Tapfiliate/AffiliateMetadataDescription.ts b/packages/nodes-base/nodes/Tapfiliate/AffiliateMetadataDescription.ts new file mode 100644 index 0000000000..d65d38794d --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/AffiliateMetadataDescription.ts @@ -0,0 +1,196 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const affiliateMetadataOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: `Add metadata to affiliate`, + }, + { + name: 'Remove', + value: 'remove', + description: `Remove metadata from affiliate`, + }, + { + name: 'Update', + value: 'update', + description: `Update affiliate's metadata`, + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const affiliateMetadataFields = [ + /* -------------------------------------------------------------------------- */ + /* affiliateMetadata:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'add', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + { + displayName: 'Metadata', + name: 'metadataUi', + placeholder: 'Add Metadata', + type: 'fixedCollection', + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'add', + ], + }, + }, + default: '', + typeOptions: { + multipleValues: true, + }, + description: 'Meta data', + options: [ + { + name: 'metadataValues', + displayName: 'Metadata', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the metadata key to add.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the metadata key.', + }, + ], + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* ffiliateMetadata:remove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'remove', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + { + displayName: 'Key', + name: 'key', + type: 'string', + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'remove', + ], + }, + }, + default: '', + description: 'Name of the metadata key to remove.', + }, + + /* -------------------------------------------------------------------------- */ + /* affiliateMetadata:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'update', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + { + displayName: 'Key', + name: 'key', + type: 'string', + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Name of the metadata key to update.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + displayOptions: { + show: { + resource: [ + 'affiliateMetadata', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Value to set for the metadata key.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Tapfiliate/GenericFunctions.ts b/packages/nodes-base/nodes/Tapfiliate/GenericFunctions.ts new file mode 100644 index 0000000000..2eea6eac51 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/GenericFunctions.ts @@ -0,0 +1,78 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function tapfiliateApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('tapfiliateApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'Api-Key': credentials.apiKey, + }, + method, + qs, + body, + uri: uri || `https://api.tapfiliate.com/1.6${endpoint}`, + json: true, + }; + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + try { + return await this.helpers.request!(options); + } catch (error) { + if (error.statusCode === 404) { + throw new Error( + `Tapfiliate error response [${error.statusCode}]: Not Found`, + ); + } + + if (error.response && error.response.body && error.response.body.errors) { + + let errors = error.response.body.errors; + + errors = errors.map((e: IDataObject) => e.message); + // Try to return the error prettier + throw new Error( + `Tapfiliate error response [${error.statusCode}]: ${errors.join('|')}`, + ); + } + throw error; + } +} + +export async function tapfiliateApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.page = 1; + + do { + responseData = await tapfiliateApiRequest.call(this, method, endpoint, body, query, '', { resolveWithFullResponse: true }); + returnData.push.apply(returnData, responseData.body); + query.page++; + + } while ( + responseData.headers.link.includes('next') + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Tapfiliate/ProgramAffiliateDescription.ts b/packages/nodes-base/nodes/Tapfiliate/ProgramAffiliateDescription.ts new file mode 100644 index 0000000000..b2f6b3d396 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/ProgramAffiliateDescription.ts @@ -0,0 +1,359 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const programAffiliateOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add affiliate to program', + }, + { + name: 'Approve', + value: 'approve', + description: 'Approve an affiliate for a program', + }, + { + name: 'Disapprove', + value: 'disapprove', + description: 'Disapprove an affiliate', + }, + { + name: 'Get', + value: 'get', + description: 'Get an affiliate in a program', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all affiliates in program', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const programAffiliateFields = [ + /* -------------------------------------------------------------------------- */ + /* programAffiliate:add */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Program ID', + name: 'programId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getPrograms', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'programAffiliate', + ], + }, + }, + description: `The ID of the Program to add the affiliate to. This ID can be found as part of the URL when viewing the program on the platform.`, + }, + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'add', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Approved', + name: 'approved', + type: 'boolean', + default: true, + description: `An optional approval status.`, + }, + { + displayName: 'Coupon', + name: 'coupon', + type: 'string', + default: '', + description: 'An optional coupon for this affiliate.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* programAffiliate:approve */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Program ID', + name: 'programId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPrograms', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'approve', + ], + resource: [ + 'programAffiliate', + ], + }, + }, + description: `The ID of the Program to add the affiliate to. This ID can be found as part of the URL when viewing the program on the platform.`, + }, + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'approve', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + + /* -------------------------------------------------------------------------- */ + /* programAffiliate:disapprove */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Program ID', + name: 'programId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPrograms', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'disapprove', + ], + resource: [ + 'programAffiliate', + ], + }, + }, + description: `The ID of the Program to add the affiliate to. This ID can be found as part of the URL when viewing the program on the platform.`, + }, + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'disapprove', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + + /* -------------------------------------------------------------------------- */ + /* affiliate:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Program ID', + name: 'programId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getPrograms', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'programAffiliate', + ], + }, + }, + description: `The ID of the Program to add the affiliate to. This ID can be found as part of the URL when viewing the program on the platform.`, + }, + { + displayName: 'Affiliate ID', + name: 'affiliateId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The ID of the affiliate.', + }, + + /* -------------------------------------------------------------------------- */ + /* programAffiliate:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Program ID', + name: 'programId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPrograms', + }, + required: true, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'programAffiliate', + ], + }, + }, + description: `The ID of the Program to add the affiliate to. This ID can be found as part of the URL when viewing the program on the platform.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If set to true, all the results will be returned.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'programAffiliate', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Affiliate Group ID', + name: 'affiliate_group_id', + type: 'string', + default: '', + description: 'Retrieves affiliates for a certain affiliate group.', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'An email address.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + description: 'Retrieves children for a certain parent affiliate.', + }, + { + displayName: 'Source ID', + name: 'source_id', + type: 'string', + default: '', + description: 'Source ID.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.ts b/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.ts new file mode 100644 index 0000000000..ef25358307 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/Tapfiliate.node.ts @@ -0,0 +1,280 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription +} from 'n8n-workflow'; + +import { + affiliateFields, + affiliateOperations +} from './AffiliateDescription'; + +import { + affiliateMetadataFields, + affiliateMetadataOperations, +} from './AffiliateMetadataDescription'; + +import { + programAffiliateFields, + programAffiliateOperations, +} from './ProgramAffiliateDescription'; + +import { + tapfiliateApiRequest, + tapfiliateApiRequestAllItems, +} from './GenericFunctions'; + +export class Tapfiliate implements INodeType { + description: INodeTypeDescription = { + displayName: 'Tapfiliate', + name: 'tapfiliate', + icon: 'file:tapfiliate.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + description: 'Consume Tapfiliate API', + defaults: { + name: 'Tapfiliate', + color: '#4a8de8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'tapfiliateApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Affiliate', + value: 'affiliate', + }, + { + name: 'Affiliate Metadata', + value: 'affiliateMetadata', + }, + { + name: 'Program Affiliate', + value: 'programAffiliate', + }, + ], + default: 'affiliate', + required: true, + description: 'Resource to consume', + }, + ...affiliateOperations, + ...affiliateFields, + ...affiliateMetadataOperations, + ...affiliateMetadataFields, + ...programAffiliateOperations, + ...programAffiliateFields, + ], + }; + + methods = { + loadOptions: { + // Get custom fields to display to user so that they can select them easily + async getPrograms(this: ILoadOptionsFunctions,): Promise { + const returnData: INodePropertyOptions[] = []; + const programs = await tapfiliateApiRequestAllItems.call(this, 'GET', '/programs/'); + for (const program of programs) { + returnData.push({ + name: program.title, + value: program.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const returnData: IDataObject[] = []; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + if (resource === 'affiliate') { + if (operation === 'create') { + //https://tapfiliate.com/docs/rest/#affiliates-affiliates-collection-post + for (let i = 0; i < length; i++) { + const firstname = this.getNodeParameter('firstname', i) as string; + const lastname = this.getNodeParameter('lastname', i) as string; + const email = this.getNodeParameter('email', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + firstname, + lastname, + email, + }; + Object.assign(body, additionalFields); + + if (body.addressUi) { + body.address = (body.addressUi as IDataObject).addressValues as IDataObject; + delete body.addressUi; + if ((body.address as IDataObject).country) { + (body.address as IDataObject).country = { + code: (body.address as IDataObject).country, + }; + } + } + + if (body.companyName) { + body.company = { + name: body.companyName, + }; + delete body.companyName; + } + responseData = await tapfiliateApiRequest.call(this, 'POST', '/affiliates/', body); + returnData.push(responseData); + } + } + if (operation === 'delete') { + //https://tapfiliate.com/docs/rest/#affiliates-affiliate-delete + for (let i = 0; i < length; i++) { + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'DELETE', `/affiliates/${affiliateId}/`); + returnData.push({ success: true }); + } + } + if (operation === 'get') { + //https://tapfiliate.com/docs/rest/#affiliates-affiliate-get + for (let i = 0; i < length; i++) { + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'GET', `/affiliates/${affiliateId}/`); + returnData.push(responseData); + } + } + if (operation === 'getAll') { + //https://tapfiliate.com/docs/rest/#affiliates-affiliates-collection-get + for (let i = 0; i < length; i++) { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + Object.assign(qs, filters); + if (returnAll) { + responseData = await tapfiliateApiRequestAllItems.call(this, 'GET', `/affiliates/`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = await tapfiliateApiRequest.call(this, 'GET', `/affiliates/`, {}, qs); + responseData = responseData.splice(0, limit); + } + returnData.push.apply(returnData, responseData); + } + } + } + if (resource === 'affiliateMetadata') { + if (operation === 'add') { + //https://tapfiliate.com/docs/rest/#affiliates-meta-data-key-put + for (let i = 0; i < length; i++) { + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + const metadata = (this.getNodeParameter('metadataUi', i) as IDataObject || {}).metadataValues as IDataObject[] || []; + if (metadata.length === 0) { + throw new Error('Metadata cannot be empty.'); + } + for (const { key, value } of metadata) { + await tapfiliateApiRequest.call(this, 'PUT', `/affiliates/${affiliateId}/meta-data/${key}/`, { value }); + } + returnData.push({ success: true }); + } + } + if (operation === 'remove') { + //https://tapfiliate.com/docs/rest/#affiliates-meta-data-key-delete + for (let i = 0; i < length; i++) { + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + const key = this.getNodeParameter('key', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'DELETE', `/affiliates/${affiliateId}/meta-data/${key}/`); + returnData.push({ success: true }); + } + } + if (operation === 'update') { + //https://tapfiliate.com/docs/rest/#affiliates-notes-collection-get + for (let i = 0; i < length; i++) { + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + const key = this.getNodeParameter('key', i) as string; + const value = this.getNodeParameter('value', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'PUT', `/affiliates/${affiliateId}/meta-data/`, { [key]: value }); + returnData.push(responseData); + } + } + } + if (resource === 'programAffiliate') { + if (operation === 'add') { + //https://tapfiliate.com/docs/rest/#programs-program-affiliates-collection-post + for (let i = 0; i < length; i++) { + const programId = this.getNodeParameter('programId', i) as string; + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + affiliate: { + id: affiliateId, + }, + }; + Object.assign(body, additionalFields); + + responseData = await tapfiliateApiRequest.call(this, 'POST', `/programs/${programId}/affiliates/`, body); + returnData.push(responseData); + } + } + if (operation === 'approve') { + //https://tapfiliate.com/docs/rest/#programs-approve-an-affiliate-for-a-program-put + for (let i = 0; i < length; i++) { + const programId = this.getNodeParameter('programId', i) as string; + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'PUT', `/programs/${programId}/affiliates/${affiliateId}/approved/`); + returnData.push(responseData); + } + } + if (operation === 'disapprove') { + //https://tapfiliate.com/docs/rest/#programs-approve-an-affiliate-for-a-program-delete + for (let i = 0; i < length; i++) { + const programId = this.getNodeParameter('programId', i) as string; + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'DELETE', `/programs/${programId}/affiliates/${affiliateId}/approved/`); + returnData.push(responseData); + } + } + if (operation === 'get') { + //https://tapfiliate.com/docs/rest/#programs-affiliate-in-program-get + for (let i = 0; i < length; i++) { + const programId = this.getNodeParameter('programId', i) as string; + const affiliateId = this.getNodeParameter('affiliateId', i) as string; + responseData = await tapfiliateApiRequest.call(this, 'GET', `/programs/${programId}/affiliates/${affiliateId}/`); + returnData.push(responseData); + } + } + if (operation === 'getAll') { + //https://tapfiliate.com/docs/rest/#programs-program-affiliates-collection-get + for (let i = 0; i < length; i++) { + const programId = this.getNodeParameter('programId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + Object.assign(qs, filters); + if (returnAll) { + responseData = await tapfiliateApiRequestAllItems.call(this, 'GET', `/programs/${programId}/affiliates/`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + responseData = await tapfiliateApiRequest.call(this, 'GET', `/programs/${programId}/affiliates/`, {}, qs); + responseData = responseData.splice(0, limit); + } + returnData.push.apply(returnData, responseData); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Tapfiliate/tapfiliate.svg b/packages/nodes-base/nodes/Tapfiliate/tapfiliate.svg new file mode 100644 index 0000000000..7662b67f70 --- /dev/null +++ b/packages/nodes-base/nodes/Tapfiliate/tapfiliate.svg @@ -0,0 +1 @@ + diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 6cfaa3ad7d..ea9b7afb8e 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -19,7 +19,7 @@ export class Telegram implements INodeType { description: INodeTypeDescription = { displayName: 'Telegram', name: 'telegram', - icon: 'file:telegram.png', + icon: 'file:telegram.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -200,6 +200,16 @@ export class Telegram implements INodeType { value: 'editMessageText', description: 'Edit a text message', }, + { + name: 'Pin Chat Message', + value: 'pinChatMessage', + description: 'Pin a chat message', + }, + { + name: 'Unpin Chat Message', + value: 'unpinChatMessage', + description: 'Unpin a chat message', + }, { name: 'Send Animation', value: 'sendAnimation', @@ -266,6 +276,8 @@ export class Telegram implements INodeType { 'get', 'leave', 'member', + 'pinChatMessage', + 'unpinChatMessage', 'setDescription', 'setTitle', 'sendAnimation', @@ -288,6 +300,54 @@ export class Telegram implements INodeType { description: 'Unique identifier for the target chat or username of the target
channel (in the format @channelusername).', }, + // ---------------------------------- + // message:pinChatMessage + // ---------------------------------- + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'pinChatMessage', + 'unpinChatMessage', + ], + resource: [ + 'message', + ], + }, + }, + required: true, + description: 'Unique identifier of the message to pin or unpin.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'pinChatMessage', + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Disable Notification', + name: 'disable_notification', + type: 'boolean', + default: false, + description: 'Do not send a notification to all chat members about the new pinned message.', + }, + ], + }, // ---------------------------------- // chat @@ -1583,10 +1643,10 @@ export class Telegram implements INodeType { body.title = this.getNodeParameter('title', i) as string; } - // } else if (resource === 'bot') { - // if (operation === 'info') { - // endpoint = 'getUpdates'; - // } + // } else if (resource === 'bot') { + // if (operation === 'info') { + // endpoint = 'getUpdates'; + // } } else if (resource === 'file') { if (operation === 'get') { @@ -1622,6 +1682,30 @@ export class Telegram implements INodeType { // Add additional fields and replyMarkup addAdditionalFields.call(this, body, i); + } else if (operation === 'pinChatMessage') { + // ---------------------------------- + // message:pinChatMessage + // ---------------------------------- + + endpoint = 'pinChatMessage'; + + body.chat_id = this.getNodeParameter('chatId', i) as string; + body.message_id = this.getNodeParameter('messageId', i) as string; + + const { disable_notification } = this.getNodeParameter('additionalFields', i) as IDataObject; + if (disable_notification) { + body.disable_notification = true; + } + + } else if (operation === 'unpinChatMessage') { + // ---------------------------------- + // message:unpinChatMessage + // ---------------------------------- + + endpoint = 'unpinChatMessage'; + + body.chat_id = this.getNodeParameter('chatId', i) as string; + body.message_id = this.getNodeParameter('messageId', i) as string; } else if (operation === 'sendAnimation') { // ---------------------------------- diff --git a/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts b/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts index f6449586ff..69fab1d2ac 100644 --- a/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts +++ b/packages/nodes-base/nodes/Telegram/TelegramTrigger.node.ts @@ -23,7 +23,7 @@ export class TelegramTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Telegram Trigger', name: 'telegramTrigger', - icon: 'file:telegram.png', + icon: 'file:telegram.svg', group: ['trigger'], version: 1, subtitle: '=Updates: {{$parameter["updates"].join(", ")}}', diff --git a/packages/nodes-base/nodes/Telegram/telegram.png b/packages/nodes-base/nodes/Telegram/telegram.png deleted file mode 100644 index 6eadb84e91..0000000000 Binary files a/packages/nodes-base/nodes/Telegram/telegram.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Telegram/telegram.svg b/packages/nodes-base/nodes/Telegram/telegram.svg new file mode 100644 index 0000000000..8256014e98 --- /dev/null +++ b/packages/nodes-base/nodes/Telegram/telegram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts new file mode 100644 index 0000000000..54dbc069e6 --- /dev/null +++ b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts @@ -0,0 +1,290 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getItemCopy, + pgInsert, + pgQuery, + pgUpdate, +} from '../Postgres/Postgres.node.functions'; + +import * as pgPromise from 'pg-promise'; + +export class TimescaleDb implements INodeType { + description: INodeTypeDescription = { + displayName: 'TimescaleDB', + name: 'timescaleDb', + icon: 'file:timescale.svg', + group: ['input'], + version: 1, + description: 'Add and update data in TimescaleDB', + defaults: { + name: 'TimescaleDB', + color: '#fdb515', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'timescaleDb', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + description: 'Execute an SQL query', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert rows in database', + }, + { + name: 'Update', + value: 'update', + description: 'Update rows in database', + }, + ], + default: 'insert', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // executeQuery + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + operation: [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'SELECT id, name FROM product WHERE id < 40', + required: true, + description: 'The SQL query to execute.', + }, + + // ---------------------------------- + // insert + // ---------------------------------- + { + displayName: 'Schema', + name: 'schema', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: 'public', + required: true, + description: 'Name of the schema the table belongs to', + }, + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to insert data to.', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: + 'Comma separated list of the properties which should used as columns for the new rows.', + }, + { + displayName: 'Return Fields', + name: 'returnFields', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: '*', + description: 'Comma separated list of the fields that the operation will return', + }, + + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: 'Schema', + name: 'schema', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: 'public', + required: true, + description: 'Name of the schema the table belongs to', + }, + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to update data in', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: 'id', + required: true, + description: + 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'name,description', + description: + 'Comma separated list of the properties which should used as columns for rows to update.', + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('timescaleDb'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const pgp = pgPromise(); + + const config = { + host: credentials.host as string, + port: credentials.port as number, + database: credentials.database as string, + user: credentials.user as string, + password: credentials.password as string, + ssl: !['disable', undefined].includes(credentials.ssl as string | undefined), + sslmode: (credentials.ssl as string) || 'disable', + }; + + const db = pgp(config); + + let returnItems = []; + + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'executeQuery') { + // ---------------------------------- + // executeQuery + // ---------------------------------- + + const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items); + + returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]); + } else if (operation === 'insert') { + // ---------------------------------- + // insert + // ---------------------------------- + + const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items); + + // Add the id to the data + for (let i = 0; i < insertData.length; i++) { + returnItems.push({ + json: { + ...insertData[i], + }, + }); + } + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items); + + returnItems = this.helpers.returnJsonArray(updateItems); + + } else { + await pgp.end(); + throw new Error(`The operation "${operation}" is not supported!`); + } + + // Close the connection + await pgp.end(); + + return this.prepareOutputData(returnItems); + } +} diff --git a/packages/nodes-base/nodes/TimescaleDb/timescale.svg b/packages/nodes-base/nodes/TimescaleDb/timescale.svg new file mode 100644 index 0000000000..ed6ddce7d8 --- /dev/null +++ b/packages/nodes-base/nodes/TimescaleDb/timescale.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/nodes-base/nodes/Todoist/Todoist.node.ts b/packages/nodes-base/nodes/Todoist/Todoist.node.ts index 4190f54cdc..4bb77324b7 100644 --- a/packages/nodes-base/nodes/Todoist/Todoist.node.ts +++ b/packages/nodes-base/nodes/Todoist/Todoist.node.ts @@ -18,6 +18,7 @@ import { interface IBodyCreateTask { content: string; project_id?: number; + section_id?: number; parent?: number; order?: number; label_ids?: number[]; @@ -273,6 +274,19 @@ export class Todoist implements INodeType { default: 1, description: 'Task priority from 1 (normal) to 4 (urgent).', }, + { + displayName: 'Section', + name: 'section', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSections', + loadOptionsDependsOn: [ + 'project', + ], + }, + default: {}, + description: 'The section you want to operate on.', + }, ], }, { @@ -399,6 +413,29 @@ export class Todoist implements INodeType { return returnData; }, + // Get all the available sections in the selected project, to display them + // to user so that he can select one easily + async getSections(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + + const projectId = this.getCurrentNodeParameter('project') as number; + if (projectId) { + const qs: IDataObject = {project_id: projectId}; + const sections = await todoistApiRequest.call(this, 'GET', '/sections', {}, qs); + for (const section of sections) { + const sectionName = section.name; + const sectionId = section.id; + + returnData.push({ + name: sectionName, + value: sectionId, + }); + } + } + + return returnData; + }, + // Get all the available labels to display them to user so that he can // select them easily async getLabels(this: ILoadOptionsFunctions): Promise { @@ -458,6 +495,10 @@ export class Todoist implements INodeType { body.label_ids = labels; } + if (options.section) { + body.section_id = options.section as number; + } + responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body); } if (operation === 'close') { diff --git a/packages/nodes-base/nodes/Typeform/GenericFunctions.ts b/packages/nodes-base/nodes/Typeform/GenericFunctions.ts index 83ca713afe..11110ca4f1 100644 --- a/packages/nodes-base/nodes/Typeform/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Typeform/GenericFunctions.ts @@ -8,9 +8,13 @@ import { INodePropertyOptions, } from 'n8n-workflow'; -import { OptionsWithUri } from 'request'; -import { IDataObject } from 'n8n-workflow'; +import { + OptionsWithUri, +} from 'request'; +import { + IDataObject, +} from 'n8n-workflow'; // Interface in Typeform export interface ITypeformDefinition { diff --git a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts index d45ca7fa67..206a386468 100644 --- a/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts +++ b/packages/nodes-base/nodes/Typeform/TypeformTrigger.node.ts @@ -22,7 +22,7 @@ export class TypeformTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Typeform Trigger', name: 'typeformTrigger', - icon: 'file:typeform.png', + icon: 'file:typeform.svg', group: ['trigger'], version: 1, subtitle: '=Form ID: {{$parameter["formId"]}}', @@ -133,10 +133,10 @@ export class TypeformTrigger implements INodeType { for (const item of items) { if (item.form_id === formId - && item.url === webhookUrl) { + && item.url === webhookUrl) { webhookData.webhookId = item.tag; return true; - } + } } return false; diff --git a/packages/nodes-base/nodes/Typeform/typeform.png b/packages/nodes-base/nodes/Typeform/typeform.png deleted file mode 100644 index df0b904216..0000000000 Binary files a/packages/nodes-base/nodes/Typeform/typeform.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Typeform/typeform.svg b/packages/nodes-base/nodes/Typeform/typeform.svg new file mode 100644 index 0000000000..092c063329 --- /dev/null +++ b/packages/nodes-base/nodes/Typeform/typeform.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 8c774a701e..4fe62af632 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.101.0", + "version": "0.103.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -61,6 +61,7 @@ "dist/credentials/CustomerIoApi.credentials.js", "dist/credentials/S3.credentials.js", "dist/credentials/CrateDb.credentials.js", + "dist/credentials/DiscourseApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", "dist/credentials/DriftApi.credentials.js", "dist/credentials/DriftOAuth2Api.credentials.js", @@ -176,6 +177,7 @@ "dist/credentials/QuestDb.credentials.js", "dist/credentials/QuickBaseApi.credentials.js", "dist/credentials/RabbitMQ.credentials.js", + "dist/credentials/RedditOAuth2Api.credentials.js", "dist/credentials/Redis.credentials.js", "dist/credentials/RocketchatApi.credentials.js", "dist/credentials/RundeckApi.credentials.js", @@ -210,8 +212,10 @@ "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TaigaCloudApi.credentials.js", "dist/credentials/TaigaServerApi.credentials.js", + "dist/credentials/TapfiliateApi.credentials.js", "dist/credentials/TelegramApi.credentials.js", "dist/credentials/TheHiveApi.credentials.js", + "dist/credentials/TimescaleDb.credentials.js", "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TodoistOAuth2Api.credentials.js", "dist/credentials/TravisCiApi.credentials.js", @@ -296,6 +300,7 @@ "dist/nodes/CustomerIo/CustomerIoTrigger.node.js", "dist/nodes/DateTime.node.js", "dist/nodes/Discord/Discord.node.js", + "dist/nodes/Discourse/Discourse.node.js", "dist/nodes/Disqus/Disqus.node.js", "dist/nodes/Drift/Drift.node.js", "dist/nodes/Dropbox/Dropbox.node.js", @@ -422,6 +427,7 @@ "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js", "dist/nodes/ReadPdf.node.js", + "dist/nodes/Reddit/Reddit.node.js", "dist/nodes/Redis/Redis.node.js", "dist/nodes/RenameKeys.node.js", "dist/nodes/Rocketchat/Rocketchat.node.js", @@ -457,10 +463,12 @@ "dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js", "dist/nodes/Taiga/Taiga.node.js", "dist/nodes/Taiga/TaigaTrigger.node.js", + "dist/nodes/Tapfiliate/Tapfiliate.node.js", "dist/nodes/Telegram/Telegram.node.js", "dist/nodes/Telegram/TelegramTrigger.node.js", "dist/nodes/TheHive/TheHive.node.js", "dist/nodes/TheHive/TheHiveTrigger.node.js", + "dist/nodes/TimescaleDb/TimescaleDb.node.js", "dist/nodes/Todoist/Todoist.node.js", "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/TravisCi/TravisCi.node.js", @@ -521,7 +529,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.50.0", + "n8n-workflow": "~0.51.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" @@ -555,7 +563,7 @@ "mqtt": "4.2.1", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.61.0", + "n8n-core": "~0.62.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 70a4d50d99..b43779998a 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.50.0", + "version": "0.51.0", "description": "Workflow base code of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index 232b3e9a47..0cfc80e2a2 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -7,6 +7,7 @@ import { NodeParameterValue, Workflow, WorkflowDataProxy, + WorkflowExecuteMode, } from './'; // @ts-ignore @@ -58,7 +59,7 @@ export class Expression { * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} * @memberof Workflow */ - resolveSimpleParameterValue(parameterValue: NodeParameterValue, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { + resolveSimpleParameterValue(parameterValue: NodeParameterValue, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { // Check if it is an expression if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') { // Is no expression so return value @@ -71,7 +72,7 @@ export class Expression { parameterValue = parameterValue.substr(1); // Generate a data proxy which allows to query workflow data - const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, -1, selfData); + const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, -1, selfData); const data = dataProxy.getDataProxy(); // Execute the expression @@ -101,7 +102,7 @@ export class Expression { * @returns {(string | undefined)} * @memberof Workflow */ - getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, defaultValue?: boolean | number | string): boolean | number | string | undefined { + getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, mode: WorkflowExecuteMode, defaultValue?: boolean | number | string): boolean | number | string | undefined { if (parameterValue === undefined) { // Value is not set so return the default return defaultValue; @@ -117,7 +118,7 @@ export class Expression { }, }; - return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData) as boolean | number | string | undefined; + return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode) as boolean | number | string | undefined; } @@ -131,7 +132,7 @@ export class Expression { * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} * @memberof Workflow */ - getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined { + getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], mode: WorkflowExecuteMode, defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined { if (parameterValue === undefined) { // Value is not set so return the default return defaultValue; @@ -148,10 +149,10 @@ export class Expression { }; // Resolve the "outer" main values - const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, false, selfData); + const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData, mode, false, selfData); // Resolve the "inner" values - return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData, false, selfData); + return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData, mode, false, selfData); } @@ -171,7 +172,7 @@ export class Expression { * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} * @memberof Workflow */ - getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { + getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { // Helper function which returns true when the parameter is a complex one or array const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { return typeof value === 'object'; @@ -180,15 +181,15 @@ export class Expression { // Helper function which resolves a parameter value depending on if it is simply or not const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { if (isComplexParameter(value)) { - return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString, selfData); + return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData); } else { - return this.resolveSimpleParameterValue(value as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString, selfData); + return this.resolveSimpleParameterValue(value as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData); } }; // Check if it value is a simple one that we can get it resolved directly if (!isComplexParameter(parameterValue)) { - return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString, selfData); + return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData); } // The parameter value is complex so resolve depending on type diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index d94fca3aa1..70f57bcf60 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -103,7 +103,7 @@ export abstract class ICredentialsHelper { } abstract getCredentials(name: string, type: string): ICredentials; - abstract getDecrypted(name: string, type: string, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject; + abstract getDecrypted(name: string, type: string, mode: WorkflowExecuteMode, raw?: boolean, expressionResolveValues?: ICredentialsExpressionResolveValues): ICredentialDataDecryptedObject; abstract updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise; } diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index afcfc334d7..ef87a7313f 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -752,10 +752,11 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: } const workflowId = workflow.id || '__UNSAVED__'; + const mode = 'internal'; const returnData: IWebhookData[] = []; for (const webhookDescription of nodeType.description.webhooks) { - let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); + let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode); if (nodeWebhookPath === undefined) { // TODO: Use a proper logger console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); @@ -768,10 +769,10 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: nodeWebhookPath = nodeWebhookPath.slice(1); } - const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], 'internal', false) as boolean; const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath); - const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET'); + const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode, 'GET'); if (httpMethod === undefined) { // TODO: Use a proper logger @@ -813,9 +814,11 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD const workflowId = workflow.id || '__UNSAVED__'; + const mode = 'internal'; + const returnData: IWebhookData[] = []; for (const webhookDescription of nodeType.description.webhooks) { - let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); + let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path'], mode); if (nodeWebhookPath === undefined) { // TODO: Use a proper logger console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); @@ -828,11 +831,11 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD nodeWebhookPath = nodeWebhookPath.slice(1); } - const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], mode, false) as boolean; const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath); - const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod']); + const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], mode); if (httpMethod === undefined) { // TODO: Use a proper logger diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 1b44f0342b..b81a0263f0 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -5,6 +5,7 @@ import { IWorkflowDataProxyData, NodeHelpers, Workflow, + WorkflowExecuteMode, } from './'; @@ -17,11 +18,12 @@ export class WorkflowDataProxy { private itemIndex: number; private activeNodeName: string; private connectionInputData: INodeExecutionData[]; + private mode: WorkflowExecuteMode; private selfData: IDataObject; - constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], defaultReturnRunIndex = -1, selfData = {}) { + constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, defaultReturnRunIndex = -1, selfData = {}) { this.workflow = workflow; this.runExecutionData = runExecutionData; this.defaultReturnRunIndex = defaultReturnRunIndex; @@ -29,6 +31,7 @@ export class WorkflowDataProxy { this.itemIndex = itemIndex; this.activeNodeName = activeNodeName; this.connectionInputData = connectionInputData; + this.mode = mode; this.selfData = selfData; } @@ -114,7 +117,7 @@ export class WorkflowDataProxy { if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') { // The found value is an expression so resolve it - return that.workflow.expression.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData); + return that.workflow.expression.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData, that.mode); } return returnValue; @@ -354,11 +357,11 @@ export class WorkflowDataProxy { $env: this.envGetter(), $evaluateExpression: (expression: string, itemIndex?: number) => { itemIndex = itemIndex || that.itemIndex; - return that.workflow.expression.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData); + return that.workflow.expression.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData, that.mode); }, $item: (itemIndex: number, runIndex?: number) => { const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex; - const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, defaultReturnRunIndex); + const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, that.mode, defaultReturnRunIndex); return dataProxy.getDataProxy(); }, $items: (nodeName?: string, outputIndex?: number, runIndex?: number) => { @@ -379,6 +382,7 @@ export class WorkflowDataProxy { $self: this.selfGetter(), $parameter: this.nodeParameterGetter(this.activeNodeName), $runIndex: this.runIndex, + $mode: this.mode, $workflow: this.workflowGetter(), }; diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index 7de298348c..82920f97e3 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -1097,7 +1097,7 @@ describe('Workflow', () => { for (const parameterName of Object.keys(testData.output)) { const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName]; - const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); + const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual'); // @ts-ignore expect(result).toEqual(testData.output[parameterName]); } @@ -1247,7 +1247,7 @@ describe('Workflow', () => { const parameterName = 'values'; const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName]; - const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); + const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, 'manual'); expect(result).toEqual({ string: [