Merge branch 'master' of github.com:n8n-io/n8n

This commit is contained in:
Tysmith17 2021-02-05 10:44:56 -07:00
commit e92fe252cf
106 changed files with 9152 additions and 736 deletions

View file

@ -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

View file

@ -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?

View file

@ -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",

View file

@ -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,

View file

@ -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

View file

@ -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)) {

View file

@ -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.'), {});

View file

@ -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",

View file

@ -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<any> { // 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);

View file

@ -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) {

View file

@ -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",

View file

@ -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) {

View file

@ -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) {

View file

@ -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

View file

@ -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.

View file

@ -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;
}

View file

@ -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: '',
},
];
}

View file

@ -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.<br />See the <a href="https://github.com/jovotech/learn-jovo/blob/master/tutorials/google-spreadsheet-private-cms/README.md#google-api-console">tutorial</a> on how to create one.',
description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.',
},
{

View file

@ -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',
},
];
}

View file

@ -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.',
},
];
}

View file

@ -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',
},
];
}

View file

@ -6,6 +6,7 @@ import {
export class SecurityScorecardApi implements ICredentialType {
name = 'securityScorecardApi';
displayName = 'SecurityScorecard API';
documentationUrl = 'securityScorecard';
properties = [
{
displayName: 'API Key',

View file

@ -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: '',
},
];
}

View file

@ -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,
},
];
}

View file

@ -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',

View file

@ -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<INodePropertyOptions[]> {
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') {

View file

@ -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<any> { // 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;
}

View file

@ -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[];

View file

@ -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. <br/>
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[];

View file

@ -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',

View file

@ -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',

View file

@ -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<INodeExecutionData[][] | null> {
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)];
}

View file

@ -130,7 +130,7 @@ export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunction
};
}
export async function downloadRecordAttachments(this: IExecuteFunctions, records: IRecord[], fieldNames: string[]): Promise<INodeExecutionData[]> {
export async function downloadRecordAttachments(this: IExecuteFunctions | IPollFunctions, records: IRecord[], fieldNames: string[]): Promise<INodeExecutionData[]> {
const elements: INodeExecutionData[] = [];
for (const record of records) {
const element: INodeExecutionData = { json: {}, binary: {} };

View file

@ -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') {
// ----------------------------------

View file

@ -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<any> { // tslint:disable-line:no-any
export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // 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<INodePropertyOptions[]> {
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;

View file

@ -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/"
}
]
}
}

View file

@ -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',
},
],
},
];

View file

@ -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/"
}
]
}
}

View file

@ -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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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<any> { // 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<any> { // 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;
}

View file

@ -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.',
},
];

View file

@ -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,
},
],
},
];

View file

@ -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.',
},
];

View file

@ -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.',
},
];

View file

@ -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.',
},
];

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 104 106"><defs><style>.cls-1{fill:#231f20;}.cls-2{fill:#fff9ae;}.cls-3{fill:#00aeef;}.cls-4{fill:#00a94f;}.cls-5{fill:#f15d22;}.cls-6{fill:#e31b23;}</style></defs><title>Discourse_logo</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_3" data-name="Layer 3"><path class="cls-1" d="M51.87,0C23.71,0,0,22.83,0,51c0,.91,0,52.81,0,52.81l51.86-.05c28.16,0,51-23.71,51-51.87S80,0,51.87,0Z"/><path class="cls-2" d="M52.37,19.74A31.62,31.62,0,0,0,24.58,66.41l-5.72,18.4L39.4,80.17a31.61,31.61,0,1,0,13-60.43Z"/><path class="cls-3" d="M77.45,32.12a31.6,31.6,0,0,1-38.05,48L18.86,84.82l20.91-2.47A31.6,31.6,0,0,0,77.45,32.12Z"/><path class="cls-4" d="M71.63,26.29A31.6,31.6,0,0,1,38.8,78L18.86,84.82,39.4,80.17A31.6,31.6,0,0,0,71.63,26.29Z"/><path class="cls-5" d="M26.47,67.11a31.61,31.61,0,0,1,51-35A31.61,31.61,0,0,0,24.58,66.41l-5.72,18.4Z"/><path class="cls-6" d="M24.58,66.41A31.61,31.61,0,0,1,71.63,26.29a31.61,31.61,0,0,0-49,39.63l-3.76,18.9Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1,017 B

View file

@ -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',

View file

@ -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',

View file

@ -120,6 +120,7 @@ export const contactListFields = [
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* contactList:remove */
/* -------------------------------------------------------------------------- */

View file

@ -1,6 +1,6 @@
import {
IDataObject,
} from 'n8n-workflow';
} from 'n8n-workflow';
export interface IAssociation {
associatedCompanyIds?: number[];

View file

@ -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',

View file

@ -1,6 +1,6 @@
import {
IDataObject,
} from 'n8n-workflow';
} from 'n8n-workflow';
export interface IContext {
goToWebinarWebinarKey?: string;

File diff suppressed because it is too large Load diff

View file

@ -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<INodePropertyOptions[]> {
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 });
}

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<boolean> {
// 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<boolean> {
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<boolean> {
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 {};

View file

@ -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.<br/>
},
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.<br/>
By default, the results will only include ticket ID and will not include the values for any properties for your tickets.<br/>
Including this parameter will include the data for the specified property in the results.<br/>
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,<br/>
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.<br/>
{
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,<br/>
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.<br/>
By default, the results will only include ticket ID and will not include the values for any properties for your tickets.<br/>
Including this parameter will include the data for the specified property in the results.<br/>
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,<br/>
},
{
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,<br/>
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[];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,022 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 62.883 69.883" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.442" y="2.442"/><symbol id="A" overflow="visible"><path d="M55.504 30.401a16.26 16.26 0 0 0-5.904-5.864c-1.865-1.084-3.794-1.773-5.972-2.07v-7.798c2.161-.895 3.558-3.018 3.525-5.357a5.86 5.86 0 0 0-5.859-5.889 5.91 5.91 0 0 0-5.908 5.889c0 2.393 1.27 4.434 3.452 5.357v7.754c-1.808.262-3.562.812-5.195 1.631L12.769 8.247c.146-.552.273-1.123.273-1.724C13.042 2.92 10.122 0 6.519 0A6.52 6.52 0 0 0 0 6.524c0 3.604 2.92 6.524 6.524 6.524a6.47 6.47 0 0 0 3.35-.952l1.367 1.035 18.726 13.501c-.991.908-1.914 1.943-2.651 3.105-1.494 2.368-2.407 4.971-2.407 7.813v.586c.007 1.927.354 3.838 1.025 5.645.566 1.543 1.396 2.949 2.427 4.219l-6.221 6.235c-1.841-.684-3.906-.23-5.298 1.162-.947.942-1.48 2.227-1.475 3.565s.527 2.612 1.479 3.564 2.227 1.48 3.565 1.48a5.01 5.01 0 0 0 3.565-1.48c.942-.952 1.479-2.227 1.475-3.564a5.03 5.03 0 0 0-.234-1.514l6.426-6.426a16.09 16.09 0 0 0 2.856 1.563 16.7 16.7 0 0 0 6.685 1.406h.439a15.76 15.76 0 0 0 7.627-1.929 15.77 15.77 0 0 0 5.977-5.63c1.499-2.393 2.319-5.044 2.319-7.959v-.146c0-2.866-.664-5.508-2.051-7.93zm-7.847 13.487c-1.743 1.938-3.75 3.135-6.016 3.135h-.43c-1.294 0-2.564-.356-3.799-1.011a8.79 8.79 0 0 1-3.33-3.032c-.898-1.27-1.387-2.656-1.387-4.126v-.439c0-1.445.278-2.817.977-4.111.747-1.465 1.758-2.515 3.101-3.389a7.6 7.6 0 0 1 4.297-1.294h.147c1.416 0 2.769.278 4.038.928 1.286.677 2.378 1.67 3.174 2.886a9.18 9.18 0 0 1 1.421 4.053l.034.913c0 1.987-.762 3.828-2.28 5.498z" stroke="none" fill="#f8761f" fill-rule="nonzero"/></symbol></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -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) {

View file

@ -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<any> { // 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<any> { // 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();
}

View file

@ -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[];

View file

@ -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<br/>
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.<br/>
This parameter accepts a comma-separated list. Expand options include:<br/>
@ -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.<br/>
This parameter accepts a comma-separated list.<br/>
@ -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.<br/>
This parameter is useful where fields have been added by a connect app and a field's key<br/>
@ -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.<br/>
This parameter accepts a comma-separated list. Allowed values:<br/>
@ -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.<br/>
This parameter is useful where fields have been added by a connect app and a field's key<br/>
@ -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.<br/>
This parameter accepts transitions.fields, which returns information about the fields in the<br/>
@ -1054,7 +1102,6 @@ export const issueFields = [
displayName: 'Transition ID',
name: 'transitionId',
type: 'string',
required: false,
default: '',
description: 'The ID of the transition.',
},
@ -1062,7 +1109,6 @@ export const issueFields = [
displayName: 'Skip Remote Only Condition',
name: 'skipRemoteOnlyCondition',
type: 'boolean',
required: false,
default: false,
description: `Indicates whether transitions with the condition Hide<br/>
From User Condition are included in the response.`,

View file

@ -1,6 +1,6 @@
import {
IDataObject,
} from 'n8n-workflow';
} from 'n8n-workflow';
export interface IFields {
assignee?: IDataObject;

View file

@ -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<INodePropertyOptions[]> {
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)];
}
}
}

View file

@ -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.',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 68.25 71.25" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#C" x="3.125" y="3.125"/><defs><linearGradient id="A" x1="91.90%" y1="40.22%" x2="28.49%" y2="81.63%"><stop offset="18%" stop-color="#0052cc"/><stop offset="100%" stop-color="#2684ff"/></linearGradient><linearGradient id="B" x1="8.70%" y1="59.17%" x2="72.26%" y2="17.99%"><stop offset="18%" stop-color="#0052cc"/><stop offset="100%" stop-color="#2684ff"/></linearGradient></defs><symbol id="C" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M61.161 30.211L30.95 0 .74 30.211a2.54 2.54 0 0 0 0 3.581l30.211 30.21 30.211-30.21a2.54 2.54 0 0 0 0-3.581zM30.95 41.46l-9.462-9.462 9.462-9.462 9.462 9.462z" fill="#2684ff"/><path d="M30.95 22.599C24.755 16.405 24.724 6.37 30.881.138L10.114 20.774l11.268 11.268z" fill="url(#A)"/><path d="M40.437 31.973L30.95 41.46a15.93 15.93 0 0 1 0 22.536l20.749-20.749z" fill="url(#B)"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -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<any> { // 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<any> { // 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<any> { // 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;
}

View file

@ -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:<br><code>/r/[subreddit_name]/comments/[post_id]/[post_title]</code>',
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:<br><code>/r/[subreddit_name]/comments/[post_id]/[post_title]</code>',
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:<br><code>/r/[subreddit_name]/comments/[post_id]/[post_title]/[comment_id]</code>',
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:<br><code>www.reddit.com/r/[subreddit_name]/comments/[post_id]/[post_title]/[comment_id]</code>',
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[];

View file

@ -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<br>it was already posted to the subreddit before.<br>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:<br><code>/r/[subreddit_name]/comments/[post_id]/[post_title]</code>',
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:<br><code>/r/[subreddit_name]/comments/[post_id]/[post_title]</code>',
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[];

View file

@ -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[];

View file

@ -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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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[];

View file

@ -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[];

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 513 514" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M0 76.8C0 34.253 34.253 0 76.8 0h358.4C477.747 0 512 34.253 512 76.8v358.4c0 42.547-34.253 76.8-76.8 76.8H76.8C34.253 512 0 477.747 0 435.2z" fill="#ff4500"/><path d="M79 305c0-68.142 78.942-123 177-123s177 54.858 177 123-78.942 123-177 123S79 373.142 79 305z"/><g fill="#ff4500"><path d="M199 347c35 29 79 29 114 0l12 11c-42 35-96 35-138 0z"/><use xlink:href="#C"/><use xlink:href="#C" x="-118"/></g></g><g stroke="#fff" fill="none"><use xlink:href="#C" x="75" y="-160" stroke-width="25"/><path d="M87 282c-45-22-5-92 40-50m298 50c45-22 5-92-40-50m-127-45l24-83 80 16" stroke-width="22"/></g></symbol><defs ><path id="C" d="M287 285a27.94 27.94 0 1 1 56 0 27.94 27.94 0 1 1-56 0z"/></defs></svg>

After

Width:  |  Height:  |  Size: 1,010 B

View file

@ -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/"
}
]
}
}

View file

@ -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<any>}
*
*/
export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, body: string, query: IDataObject = {}, option: IDataObject = {}): Promise<any> { // 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,
};

View file

@ -1,7 +1,7 @@
{
"node": "n8n-nodes-base.signl4",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"nodeVersion": "1.1",
"codexVersion": "1.1",
"categories": [
"Communication",
"Development"

View file

@ -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)];
}

View file

@ -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) {

View file

@ -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 affiliates email.`,
},
{
displayName: 'First Name',
name: 'firstname',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'affiliate',
],
},
},
default: '',
description: `The affiliates firstname.`,
},
{
displayName: 'Last Name',
name: 'lastname',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'affiliate',
],
},
},
default: '',
description: `The affiliates 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 countrys ISO_3166-1 code. <a target="_blank" href="https://en.wikipedia.org/wiki/ISO_3166-1">Codes</a>.`,
},
],
},
],
},
{
displayName: 'Company Name',
name: 'companyName',
type: 'string',
default: '',
description: `The affiliates 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 affiliates 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[];

View file

@ -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[];

View file

@ -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<any> { // 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<any> { // 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;
}

View file

@ -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[];

View file

@ -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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -0,0 +1 @@
<svg width="900" height="900" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="2.037%" y1="113.141%" x2="106.275%" y2="-7.854%" id="a"><stop stop-color="#2FDFBF" offset="0%"/><stop stop-color="#2FDFBF" offset="0%"/><stop stop-color="#2F74FF" offset="100%"/></linearGradient></defs><path d="M900 289.19c0-113.514-67.164-221.622-178.657-267.568C685.075 6.757 647.463 0 609.851 0 552.09 0 495.67 17.568 448.657 48.649 402.985 17.568 347.91 0 288.806 0c-20.15 0-41.642 2.703-63.134 6.757C118.209 29.73 32.239 113.514 6.716 220.27v2.703C2.686 241.892 0 262.163 0 281.081 0 322.973 9.403 363.514 24.18 400l145.074 363.514c21.492 55.405 64.477 101.35 123.582 122.972 21.492 8.109 42.985 12.163 65.82 13.514h502.389l-90-239.19L878.507 400C891.94 363.514 900 325.676 900 289.19zM95.373 281.08c0-12.162 1.343-25.676 4.03-37.838v-2.702c16.12-68.92 75.224-127.027 145.075-140.541 14.776-2.703 29.552-4.054 42.985-4.054 32.238 0 61.79 8.108 88.656 21.622-13.432 18.918-25.522 37.837-34.925 60.81L188.06 556.757l-76.567-191.892c-9.403-28.379-16.12-56.757-16.12-83.784zm693.134 82.433L666.27 659.459l55.074 145.946H362.687c-12.09 0-24.18-2.702-36.27-8.108-48.357-17.567-76.566-63.513-76.566-110.81 0-13.514 2.686-28.379 8.06-43.244l173.283-428.378c30.896-74.324 103.433-118.92 180-118.92 24.18 0 49.702 4.055 73.88 14.866 75.225 31.08 119.553 102.703 119.553 178.378-1.343 24.325-5.373 50-16.12 74.325z" fill-rule="nonzero" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -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<br />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') {
// ----------------------------------

View file

@ -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(", ")}}',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 66 66" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><g stroke="none" fill-rule="nonzero"><path d="M0 32c0 17.673 14.327 32 32 32s32-14.327 32-32S49.673 0 32 0 0 14.327 0 32" fill="#37aee2"/><path d="M21.661 34.338l3.797 10.508s.475.983.983.983 8.068-7.864 8.068-7.864l8.407-16.237-21.119 9.898z" fill="#c8daea"/><path d="M26.695 37.034l-.729 7.746s-.305 2.373 2.068 0l4.644-4.203" fill="#a9c6d8"/><path d="M21.73 34.712l-7.809-2.545s-.932-.378-.633-1.237c.062-.177.186-.328.559-.588 1.731-1.206 32.028-12.096 32.028-12.096s.856-.288 1.361-.097c.231.088.378.187.503.548.045.132.071.411.068.689-.003.201-.027.386-.045.678-.184 2.978-5.706 25.198-5.706 25.198s-.33 1.3-1.514 1.345c-.432.016-.956-.071-1.582-.61-2.323-1.998-10.352-7.394-12.126-8.58-.1-.067-.129-.154-.146-.239-.025-.125.108-.28.108-.28s13.98-12.427 14.352-13.731c.029-.101-.079-.151-.226-.107-.929.342-17.025 10.506-18.801 11.629-.104.066-.395.023-.395.023"/></g></symbol></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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<INodeExecutionData[][]> {
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);
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
@ -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') {

View file

@ -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 {

View file

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 988 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="257px" viewBox="0 0 256 257" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M121.239531,0.47435412 C154.258847,-2.24419986 177.973534,6.51949224 203.490026,32.0209539 C231.03887,59.5533708 249.354214,95.2328648 254.290235,133.239251 C259.781839,175.505244 252.228399,207.045826 228.963591,230.137302 C206.717472,252.217693 174.224091,259.500956 130.817584,255.872056 L130.817584,255.872056 L129.020628,255.714401 C92.0580678,252.31591 65.9142731,240.197353 40.5830264,214.542641 C13.525519,187.139558 -0.732370213,156.026391 0.0289819699,123.37039 C0.401368488,107.383336 4.93009017,93.7578927 13.6639513,80.3889177 C19.3897229,71.6244438 25.2046788,64.7874057 37.3681164,52.2487147 L37.3681164,52.2487147 L41.4275324,48.0907013 L45.9022443,43.5551431 C74.7290193,14.5793771 93.7072016,2.74118195 121.239531,0.47435412 Z M201.527922,33.9842216 C176.58625,9.05724142 153.671595,0.589198793 121.467288,3.24065127 C94.3271196,5.47519117 75.6884654,17.3319772 46.5188613,46.8760997 L46.5188613,46.8760997 L44.4086816,49.0171264 C30.1966379,63.4648858 23.5365117,70.8518409 17.66965,79.3947543 L17.66965,79.3947543 L16.8159631,80.6541424 L15.9876804,81.9069942 C7.52700788,94.8577977 3.16375473,107.9854 2.80388584,123.435056 C2.06141566,155.281167 15.9979199,185.693018 42.5581255,212.592447 C67.82505,238.182016 93.7827344,249.991613 131.048788,253.106044 L131.048788,253.106044 L132.897415,253.254431 C174.642425,256.464747 205.76515,249.252143 227.008257,228.167291 C249.578581,205.765126 256.927442,175.078792 251.537704,133.596809 C246.682834,96.2152658 228.654224,61.0943485 201.527922,33.9842216 Z M166.984321,93.3903087 L166.984321,106.949394 L138.186877,106.949394 L138.186877,184.662246 L123.842281,184.662246 L123.842281,106.949394 L95.044837,106.949394 L95.044837,93.3903087 L166.984321,93.3903087 Z" fill="#262627" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -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",

View file

@ -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",

Some files were not shown because too many files have changed in this diff Show more