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

This commit is contained in:
zou wendi 2020-05-05 09:44:21 +08:00
commit 2bed4a6246
42 changed files with 2088 additions and 271 deletions

View file

@ -6,3 +6,10 @@ indent_style = tab
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.ts]
quote_type = single

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -6,6 +6,7 @@
"bootstrap": "lerna bootstrap --hoist --no-ci",
"build": "lerna exec npm run build",
"dev": "lerna exec npm run dev --parallel",
"clean:dist": "lerna exec -- rimraf ./dist",
"start": "run-script-os",
"start:default": "cd packages/cli/bin && ./n8n",
"start:windows": "cd packages/cli/bin && n8n",
@ -14,6 +15,7 @@
},
"devDependencies": {
"lerna": "^3.13.1",
"rimraf": "^3.0.2",
"run-script-os": "^1.0.7"
},
"postcss": {}

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.65.0",
"version": "0.66.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -93,12 +93,12 @@
"jwks-rsa": "^1.6.0",
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"mongodb": "^3.2.3",
"mongodb": "^3.5.5",
"mysql2": "^2.0.1",
"n8n-core": "~0.32.0",
"n8n-editor-ui": "~0.43.0",
"n8n-nodes-base": "~0.60.0",
"n8n-workflow": "~0.29.0",
"n8n-core": "~0.33.0",
"n8n-editor-ui": "~0.44.0",
"n8n-nodes-base": "~0.61.0",
"n8n-workflow": "~0.30.0",
"open": "^7.0.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View file

@ -512,11 +512,8 @@ class App {
const sessionId = GenericHelpers.getSessionId(req);
// Check if workflow is saved as webhooks can only be tested with saved workflows.
// If that is the case check if any webhooks calls are present we have to wait for and
// if that is the case wait till we receive it.
if (WorkflowHelpers.isWorkflowIdValid(workflowData.id) === true && (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined)) {
// Webhooks can only be tested with saved workflows
// If webhooks nodes exist and are active we have to wait for till we receive a call
if (runData === undefined || startNodes === undefined || startNodes.length === 0 || destinationNode === undefined) {
const credentials = await WorkflowCredentials(workflowData.nodes);
const additionalData = await WorkflowExecuteAdditionalData.getBase(credentials);
const nodeTypes = NodeTypes();
@ -1083,7 +1080,6 @@ class App {
return returnData;
}));
// Forces the execution to stop
this.app.post('/rest/executions-current/:id/stop', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<IExecutionsStopData> => {
const executionId = req.params.id;
@ -1151,6 +1147,26 @@ class App {
// Webhooks
// ----------------------------------------
// HEAD webhook requests
this.app.head(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhook.length + 2);
let response;
try {
response = await this.activeWorkflowRunner.executeWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// GET webhook requests
this.app.get(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
@ -1173,7 +1189,6 @@ class App {
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// POST webhook requests
this.app.post(`/${this.endpointWebhook}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook/" to get the registred part of the url
@ -1195,6 +1210,26 @@ class App {
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// HEAD webhook requests (test for UI)
this.app.head(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url
const requestUrl = (req as ICustomRequest).parsedUrl!.pathname!.slice(this.endpointWebhookTest.length + 2);
let response;
try {
response = await this.testWebhooks.callTestWebhook('HEAD', requestUrl, req, res);
} catch (error) {
ResponseHelper.sendErrorResponse(res, error);
return;
}
if (response.noWebhookResponse === true) {
// Nothing else to do as the response got already sent
return;
}
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// GET webhook requests (test for UI)
this.app.get(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => {
@ -1217,7 +1252,6 @@ class App {
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
});
// POST webhook requests (test for UI)
this.app.post(`/${this.endpointWebhookTest}/*`, async (req: express.Request, res: express.Response) => {
// Cut away the "/webhook-test/" to get the registred part of the url

View file

@ -129,6 +129,10 @@ export class TestWebhooks {
return false;
}
if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
}
// Remove test-webhooks automatically if they do not get called (after 120 seconds)
const timeout = setTimeout(() => {
this.cancelTestWebhook(workflowData.id.toString());

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.32.0",
"version": "0.33.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -44,7 +44,7 @@
"crypto-js": "3.1.9-1",
"lodash.get": "^4.4.2",
"mmmagic": "^0.5.2",
"n8n-workflow": "~0.29.0",
"n8n-workflow": "~0.30.0",
"p-cancelable": "^2.0.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.7"

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.43.0",
"version": "0.44.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -64,7 +64,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.29.0",
"n8n-workflow": "~0.30.0",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",

View file

@ -21,18 +21,20 @@
<font-awesome-icon icon="question-circle" />
</el-tooltip>
</div>
<el-row v-for="parameter in credentialTypeData.properties" :key="parameter.name" class="parameter-wrapper">
<el-col :span="6" class="parameter-name">
{{parameter.displayName}}:
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
<div slot="content" v-html="parameter.description"></div>
<font-awesome-icon icon="question-circle"/>
</el-tooltip>
</el-col>
<el-col :span="18">
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
</el-col>
</el-row>
<span v-for="parameter in credentialTypeData.properties" :key="parameter.name">
<el-row v-if="displayCredentialParameter(parameter)" class="parameter-wrapper">
<el-col :span="6" class="parameter-name">
{{parameter.displayName}}:
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
<div slot="content" v-html="parameter.description"></div>
<font-awesome-icon icon="question-circle"/>
</el-tooltip>
</el-col>
<el-col :span="18">
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
</el-col>
</el-row>
</span>
<el-row class="nodes-access-wrapper">
<el-col :span="6" class="headline">
@ -85,6 +87,7 @@ import {
ICredentialType,
ICredentialNodeAccess,
INodeCredentialDescription,
INodeProperties,
INodeTypeDescription,
} from 'n8n-workflow';
@ -162,6 +165,14 @@ export default mixins(
tempValue[name] = parameterData.value;
Vue.set(this, 'propertyValue', tempValue);
},
displayCredentialParameter (parameter: INodeProperties): boolean {
if (parameter.displayOptions === undefined) {
// If it is not defined no need to do a proper check
return true;
}
return this.displayParameter(this.propertyValue, parameter, '');
},
async createCredentials (): Promise<void> {
const nodesAccess = this.nodesAccess.map((nodeType) => {
return {

View file

@ -504,11 +504,11 @@ export default mixins(
} else if (entry.finished === true) {
return 'The worklow execution was successful.';
} else if (entry.retryOf !== undefined) {
return `The workflow execution was a retry of "${entry.retryOf}" and did fail.<br />New retries have to be started from the original execution.`;
return `The workflow execution was a retry of "${entry.retryOf}" and failed.<br />New retries have to be started from the original execution.`;
} else if (entry.retrySuccessId !== undefined) {
return `The workflow execution did fail but the retry "${entry.retrySuccessId}" was successful.`;
return `The workflow execution failed but the retry "${entry.retrySuccessId}" was successful.`;
} else {
return 'The workflow execution did fail.';
return 'The workflow execution failed.';
}
},
async stopExecution (activeExecutionId: string) {

View file

@ -22,6 +22,8 @@ export const pushConnection = mixins(
return {
eventSource: null as EventSource | null,
reconnectTimeout: null as NodeJS.Timeout | null,
retryTimeout: null as NodeJS.Timeout | null,
pushMessageQueue: [] as Array<{ event: Event, retriesLeft: number }>,
};
},
computed: {
@ -96,47 +98,84 @@ export const pushConnection = mixins(
* @param {number} retryAttempts
* @returns
*/
retryPushMessage (event: Event, retryAttempts: number) {
retryAttempts = retryAttempts - 1;
queuePushMessage (event: Event, retryAttempts: number) {
this.pushMessageQueue.push({ event, retriesLeft: retryAttempts });
if (retryAttempts <= 0) {
return;
if (this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 20);
}
},
/**
* Process the push messages which are waiting in the queue
*/
processWaitingPushMessages () {
if (this.retryTimeout !== null) {
clearTimeout(this.retryTimeout);
this.retryTimeout = null;
}
setTimeout(() => {
this.pushMessageReceived(event, retryAttempts);
}, 200);
const queueLength = this.pushMessageQueue.length;
for (let i = 0; i < queueLength; i++) {
const messageData = this.pushMessageQueue.shift();
if (this.pushMessageReceived(messageData!.event, true) === false) {
// Was not successful
messageData!.retriesLeft -= 1;
if (messageData!.retriesLeft > 0) {
// If still retries are left add it back and stop execution
this.pushMessageQueue.unshift(messageData!);
}
break;
}
}
if (this.pushMessageQueue.length !== 0 && this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 25);
}
},
/**
* Process a newly received message
*
* @param {Event} event The event data with the message data
* @returns {void}
* @param {boolean} [isRetry] If it is a retry
* @returns {boolean} If message could be processed
*/
pushMessageReceived (event: Event, retryAttempts?: number): void {
retryAttempts = retryAttempts || 5;
pushMessageReceived (event: Event, isRetry?: boolean): boolean {
const retryAttempts = 5;
let receivedData: IPushData;
try {
// @ts-ignore
receivedData = JSON.parse(event.data);
} catch (error) {
console.error('The received push data is not valid JSON.'); // eslint-disable-line no-console
return;
return false;
}
if (!['testWebhookReceived'].includes(receivedData.type) && isRetry !== true && this.pushMessageQueue.length) {
// If there are already messages in the queue add the new one that all of them
// get executed in order
this.queuePushMessage(event, retryAttempts);
return false;
}
if (['nodeExecuteAfter', 'nodeExecuteBefore'].includes(receivedData.type)) {
if (this.$store.getters.isActionActive('workflowRunning') === false) {
// No workflow is running so ignore the messages
return;
return false;
}
const pushData = receivedData.data as IPushDataNodeExecuteBefore;
if (this.$store.getters.activeExecutionId !== pushData.executionId) {
// The data is not for the currently active execution or
// we do not have the execution id yet.
this.retryPushMessage(event, retryAttempts);
return;
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
}
@ -148,14 +187,16 @@ export const pushConnection = mixins(
if (this.$store.getters.isActionActive('workflowRunning') === false) {
// No workflow is running so ignore the messages
return;
return false;
}
if (this.$store.getters.activeExecutionId !== pushData.executionIdActive) {
// The workflow which did finish execution did either not get started
// by this session or we do not have the execution id yet.
this.retryPushMessage(event, retryAttempts);
return;
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
const runDataExecuted = pushData.data;
@ -231,7 +272,10 @@ export const pushConnection = mixins(
this.$store.commit('setExecutionWaitingForWebhook', false);
this.$store.commit('setActiveExecutionId', pushData.executionId);
}
this.processWaitingPushMessages();
}
return true;
},
},
});

View file

@ -179,7 +179,7 @@ if (process.env.NODE_ENV !== 'production') {
// not do anything about it anyway
return;
}
console.error('error cought in main.ts'); // eslint-disable-line no-console
console.error('error caught in main.ts'); // eslint-disable-line no-console
console.error(message); // eslint-disable-line no-console
console.error(error); // eslint-disable-line no-console
};

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class FacebookGraphApi implements ICredentialType {
name = 'facebookGraphApi';
displayName = 'Facebook Graph API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -1,45 +1,106 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
export class MongoDb implements ICredentialType {
name = 'mongoDb';
displayName = 'MongoDB';
properties = [
{
displayName: 'Configuration Type',
name: 'configurationType',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'Connection String',
value: 'connectionString',
description: 'Provide connection data via string',
},
{
name: 'Values',
value: 'values',
description: 'Provide connection data via values',
},
],
default: 'values',
description: 'The operation to perform.',
},
{
displayName: 'Connection String',
name: 'connectionString',
type: 'string' as NodePropertyTypes,
displayOptions: {
show: {
configurationType: [
'connectionString',
],
},
},
default: '',
placeholder: 'mongodb://<USERNAME>:<PASSWORD>@localhost:27017/?authSource=admin&readPreference=primary&appname=n8n&ssl=false',
required: false,
description: `If provided, the value here will be used as a MongoDB connection string,<br />
and the MongoDB credentials will be ignored`
},
{
displayName: 'Host',
name: 'host',
type: 'string' as NodePropertyTypes,
default: 'localhost',
displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: 'localhost'
},
{
displayName: 'Database',
name: 'database',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Note: the database should still be provided even if using an override connection string'
},
{
displayName: 'User',
name: 'user',
type: 'string' as NodePropertyTypes,
default: '',
displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: ''
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
password: true
},
default: '',
displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: ''
},
{
displayName: 'Port',
name: 'port',
type: 'number' as NodePropertyTypes,
default: 27017,
displayOptions: {
show: {
configurationType: [
'values',
],
},
},
default: 27017
},
];
}

View file

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class Sms77Api implements ICredentialType {
name = 'sms77Api';
displayName = 'Sms77 API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,36 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SurveyMonkeyApi implements ICredentialType {
name = 'surveyMonkeyApi';
displayName = 'SurveyMonkey API';
properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: `The access token must have the following scopes:</br />
- Create/modify webhooks</br />
- View webhooks</br />
- View surveys</br />
- View collectors</br />
- View responses<br />
- View response details`,
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,418 @@
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
export class FacebookGraphApi implements INodeType {
description: INodeTypeDescription = {
displayName: 'Facebook Graph API',
name: 'facebookGraphApi',
icon: 'file:facebook.png',
group: ['transform'],
version: 1,
description: 'Interacts with Facebook using the Graph API',
defaults: {
name: 'Facebook Graph API',
color: '#772244',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'facebookGraphApi',
required: true,
},
],
properties: [
{
displayName: 'Host URL',
name: 'hostUrl',
type: 'options',
options: [
{
name: 'Default',
value: 'graph.facebook.com',
},
{
name: 'Video Uploads',
value: 'graph-video.facebook.com',
}
],
default: 'graph.facebook.com',
description: 'The Host URL of the request. Almost all requests are passed to the graph.facebook.com host URL. The single exception is video uploads, which use graph-video.facebook.com.',
required: true,
},
{
displayName: 'HTTP Request Method',
name: 'httpRequestMethod',
type: 'options',
options: [
{
name: 'GET',
value: 'GET',
},
{
name: 'POST',
value: 'POST',
},
{
name: 'DELETE',
value: 'DELETE',
},
],
default: 'GET',
description: 'The HTTP Method to be used for the request.',
required: true,
},
{
displayName: 'Graph API Version',
name: 'graphApiVersion',
type: 'options',
options: [
{
name: 'Latest',
value: '',
},
{
name: 'v6.0',
value: 'v6.0',
},
{
name: 'v5.0',
value: 'v5.0',
},
{
name: 'v4.0',
value: 'v4.0',
},
{
name: 'v3.3',
value: 'v3.3',
},
{
name: 'v3.2',
value: 'v3.2',
},
{
name: 'v3.1',
value: 'v3.1',
},
{
name: 'v3.0',
value: 'v3.0',
},
{
name: 'v2.12',
value: 'v2.12',
},
],
default: '',
description: 'The version of the Graph API to be used in the request.',
required: true,
},
{
displayName: 'Node',
name: 'node',
type: 'string',
default: '',
description: 'The node on which to operate. A node is an individual object with a unique ID. For example, there are many User node objects, each with a unique ID representing a person on Facebook.',
placeholder: 'me',
required: true,
},
{
displayName: 'Edge',
name: 'edge',
type: 'string',
default: '',
description: 'Edge of the node on which to operate. Edges represent collections of objects wich are attached to the node.',
placeholder: 'videos',
required: false,
},
{
displayName: 'Send Binary Data',
name: 'sendBinaryData',
type: 'boolean',
displayOptions: {
show: {
httpRequestMethod: [
'POST',
'PUT',
],
},
},
default: false,
required: true,
description: 'If binary data should be send as body.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
required: false,
default: '',
placeholder: 'file:data',
displayOptions: {
hide: {
sendBinaryData: [
false,
],
},
show: {
httpRequestMethod: [
'POST',
'PUT',
],
},
},
description: `Name of the binary property which contains the data for the file to be uploaded.<br />
For Form-Data Multipart, multiple can be provided in the format:<br />
"sendKey1:binaryProperty1,sendKey2:binaryProperty2`,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/httpRequestMethod': [
'GET',
],
},
},
description: 'The list of fields to request in the GET request.',
default: {},
options: [
{
name: 'field',
displayName: 'Field',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the field.',
},
],
},
],
},
{
displayName: 'Query Parameters',
name: 'queryParameters',
placeholder: 'Add Parameter',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'The query parameters to send',
default: {},
options: [
{
name: 'parameter',
displayName: 'Parameter',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the parameter.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the parameter.',
},
],
},
],
},
{
displayName: 'Query Parameters JSON',
name: 'queryParametersJson',
type: 'json',
default: '{}',
placeholder: '{\"field_name\": \"field_value\"}',
description: 'The query parameters to send, defined as a JSON object',
required: false,
}
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
let response: any; // tslint:disable-line:no-any
const returnItems: INodeExecutionData[] = [];
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
const graphApiCredentials = this.getCredentials('facebookGraphApi');
const hostUrl = this.getNodeParameter('hostUrl', itemIndex) as string;
const httpRequestMethod = this.getNodeParameter('httpRequestMethod', itemIndex) as string;
let graphApiVersion = this.getNodeParameter('graphApiVersion', itemIndex) as string;
const node = this.getNodeParameter('node', itemIndex) as string;
const edge = this.getNodeParameter('edge', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject;
if (graphApiVersion !== '') {
graphApiVersion += '/';
}
let uri = `https://${hostUrl}/${graphApiVersion}${node}`;
if (edge) {
uri = `${uri}/${edge}`;
}
const requestOptions : OptionsWithUri = {
headers: {
accept: 'application/json,text/*;q=0.99',
},
method: httpRequestMethod,
uri,
json: true,
gzip: true,
qs: {
access_token: graphApiCredentials!.accessToken,
},
};
if (options !== undefined) {
// Build fields query parameter as a comma separated list
if (options.fields !== undefined) {
const fields = options.fields as IDataObject;
if (fields.field !== undefined) {
const fieldsCsv = (fields.field as IDataObject[]).map(field => field.name).join(',');
requestOptions.qs.fields = fieldsCsv;
}
}
// Add the query parameters defined in the UI
if (options.queryParameters !== undefined) {
const queryParameters = options.queryParameters as IDataObject;
if (queryParameters.parameter !== undefined) {
for (const queryParameter of queryParameters.parameter as IDataObject[]) {
requestOptions.qs[queryParameter.name as string] = queryParameter.value;
}
}
}
// Add the query parameters defined as a JSON object
if (options.queryParametersJson) {
let queryParametersJsonObj = {};
try
{
queryParametersJsonObj = JSON.parse(options.queryParametersJson as string);
} catch { /* Do nothing, at least for now */}
const qs = requestOptions.qs;
requestOptions.qs = {
...qs,
...queryParametersJsonObj,
};
}
}
const sendBinaryData = this.getNodeParameter('sendBinaryData', itemIndex, false) as boolean;
if (sendBinaryData) {
const item = items[itemIndex];
if (item.binary === undefined) {
throw new Error('No binary data exists on item!');
}
const binaryPropertyNameFull = this.getNodeParameter('binaryPropertyName', itemIndex) as string;
let propertyName = 'file';
let binaryPropertyName = binaryPropertyNameFull;
if (binaryPropertyNameFull.includes(':')) {
const binaryPropertyNameParts = binaryPropertyNameFull.split(':');
propertyName = binaryPropertyNameParts[0];
binaryPropertyName = binaryPropertyNameParts[1];
}
if (item.binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryProperty = item.binary[binaryPropertyName] as IBinaryData;
requestOptions.formData = {
[propertyName]: {
value: Buffer.from(binaryProperty.data, BINARY_ENCODING),
options: {
filename: binaryProperty.fileName,
contentType: binaryProperty.mimeType,
},
},
};
}
try {
// Now that the options are all set make the actual http request
response = await this.helpers.request(requestOptions);
} catch (error) {
if (this.continueOnFail() === false) {
throw error;
}
let errorItem;
if (error.response !== undefined) {
// Since this is a Graph API node and we already know the request was
// not successful, we'll go straight to the error details.
const graphApiErrors = error.response.body?.error ?? {};
errorItem = {
statusCode: error.statusCode,
...graphApiErrors,
headers: error.response.headers,
};
} else {
// Unknown Graph API response, we'll dump everything in the response item
errorItem = error;
}
returnItems.push({ json: { ...errorItem } });
continue;
}
if (typeof response === 'string') {
if (this.continueOnFail() === false) {
throw new Error('Response body is not valid JSON.');
}
returnItems.push({ json: { message: response } });
continue;
}
returnItems.push({json: response});
}
return [returnItems];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -212,7 +212,7 @@ export class Github implements INodeType {
{
name: 'Get Emails',
value: 'getEmails',
description: 'Returns the repositories of a user',
description: 'Returns the email addresses of a user',
},
{
name: 'Get Repositories',

View file

@ -81,6 +81,9 @@ export const issueFields = [
},
typeOptions: {
loadOptionsMethod: 'getProjects',
loadOptionsDependsOn: [
'jiraVersion',
],
},
description: 'Project',
},

View file

@ -111,9 +111,10 @@ export class JiraSoftwareCloud implements INodeType {
// select them easily
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const jiraCloudCredentials = this.getCredentials('jiraSoftwareCloudApi');
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
let endpoint = '/project/search';
if (jiraCloudCredentials === undefined) {
if (jiraVersion === 'server') {
endpoint = '/project';
}
let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');

View file

@ -3,192 +3,29 @@ import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
INodeTypeDescription
} from 'n8n-workflow';
import { nodeDescription } from './mongo.node.options';
import { MongoClient } from 'mongodb';
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map((item) => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}
import {
getItemCopy,
validateAndResolveMongoCredentials
} from './mongo.node.utils';
export class MongoDb implements INodeType {
description: INodeTypeDescription = {
displayName: 'MongoDB',
name: 'mongoDb',
icon: 'file:mongoDb.png',
group: ['input'],
version: 1,
description: 'Find, insert and update documents in MongoDB.',
defaults: {
name: 'MongoDB',
color: '#13AA52',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mongoDb',
required: true,
}
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Find',
value: 'find',
description: 'Find documents.',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert documents.',
},
{
name: 'Update',
value: 'update',
description: 'Updates documents.',
},
],
default: 'find',
description: 'The operation to perform.',
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
required: true,
default: '',
description: 'MongoDB Collection'
},
// ----------------------------------
// find
// ----------------------------------
{
displayName: 'Query (JSON format)',
name: 'query',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: [
'find'
],
},
},
default: '{}',
placeholder: `{ "birth": { "$gt": "1950-01-01" } }`,
required: true,
description: 'MongoDB Find query.',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Fields',
name: 'fields',
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
},
},
default: '',
placeholder: 'name,description',
description: 'Comma separated list of the fields to be included into the new document.',
},
// ----------------------------------
// update
// ----------------------------------
{
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: 'Fields',
name: 'fields',
type: 'string',
displayOptions: {
show: {
operation: [
'update'
],
},
},
default: '',
placeholder: 'name,description',
description: 'Comma separated list of the fields to be included into the new document.',
},
]
};
description: INodeTypeDescription = nodeDescription;
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const { database, connectionString } = validateAndResolveMongoCredentials(
this.getCredentials('mongoDb')
);
const credentials = this.getCredentials('mongoDb');
const client: MongoClient = await MongoClient.connect(connectionString, {
useNewUrlParser: true,
useUnifiedTopology: true
});
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let connectionUri = '';
if (credentials.port) {
connectionUri = `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`;
} else {
connectionUri = `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`;
}
const client = await MongoClient.connect(connectionUri, { useNewUrlParser: true, useUnifiedTopology: true });
const mdb = client.db(credentials.database as string);
const mdb = client.db(database as string);
let returnItems = [];
@ -206,7 +43,6 @@ export class MongoDb implements INodeType {
.toArray();
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
@ -229,7 +65,7 @@ export class MongoDb implements INodeType {
returnItems.push({
json: {
...insertItems[parseInt(i, 10)],
id: insertedIds[parseInt(i, 10)] as string,
id: insertedIds[parseInt(i, 10)] as string
}
});
}
@ -258,7 +94,7 @@ export class MongoDb implements INodeType {
continue;
}
const filter: { [key: string] :string } = {};
const filter: { [key: string]: string } = {};
filter[updateKey] = item[updateKey] as string;
await mdb
@ -267,7 +103,6 @@ export class MongoDb implements INodeType {
}
returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]);
} else {
throw new Error(`The operation "${operation}" is not supported!`);
}

View file

@ -0,0 +1,131 @@
import { INodeTypeDescription } from 'n8n-workflow';
/**
* Options to be displayed
*/
export const nodeDescription: INodeTypeDescription = {
displayName: 'MongoDB',
name: 'mongoDb',
icon: 'file:mongoDb.png',
group: ['input'],
version: 1,
description: 'Find, insert and update documents in MongoDB.',
defaults: {
name: 'MongoDB',
color: '#13AA52'
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mongoDb',
required: true
}
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Find',
value: 'find',
description: 'Find documents.'
},
{
name: 'Insert',
value: 'insert',
description: 'Insert documents.'
},
{
name: 'Update',
value: 'update',
description: 'Updates documents.'
}
],
default: 'find',
description: 'The operation to perform.'
},
{
displayName: 'Collection',
name: 'collection',
type: 'string',
required: true,
default: '',
description: 'MongoDB Collection'
},
// ----------------------------------
// find
// ----------------------------------
{
displayName: 'Query (JSON format)',
name: 'query',
type: 'string',
typeOptions: {
rows: 5
},
displayOptions: {
show: {
operation: ['find']
}
},
default: '{}',
placeholder: `{ "birth": { "$gt": "1950-01-01" } }`,
required: true,
description: 'MongoDB Find query.'
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Fields',
name: 'fields',
type: 'string',
displayOptions: {
show: {
operation: ['insert']
}
},
default: '',
placeholder: 'name,description',
description:
'Comma separated list of the fields to be included into the new document.'
},
// ----------------------------------
// update
// ----------------------------------
{
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: 'Fields',
name: 'fields',
type: 'string',
displayOptions: {
show: {
operation: ['update']
}
},
default: '',
placeholder: 'name,description',
description:
'Comma separated list of the fields to be included into the new document.'
}
]
};

View file

@ -0,0 +1,53 @@
import { CredentialInformation } from 'n8n-workflow';
/**
* Credentials object for Mongo, if using individual parameters
*/
export interface IMongoParametricCredentials {
/**
* Whether to allow overriding the parametric credentials with a connection string
*/
configurationType: 'values';
host: string;
database: string;
user: string;
password: string;
port?: number;
}
/**
* Credentials object for Mongo, if using override connection string
*/
export interface IMongoOverrideCredentials {
/**
* Whether to allow overriding the parametric credentials with a connection string
*/
configurationType: 'connectionString';
/**
* If using an override connection string, this is where it will be.
*/
connectionString: string;
database: string;
}
/**
* Unified credential object type (whether params are overridden with a connection string or not)
*/
export type IMongoCredentialsType =
| IMongoParametricCredentials
| IMongoOverrideCredentials;
/**
* Resolve the database and connection string from input credentials
*/
export type IMongoCredentials = {
/**
* Database name (used to create the Mongo client)
*/
database: string;
/**
* Generated connection string (after validating and figuring out overrides)
*/
connectionString: string;
};

View file

@ -0,0 +1,104 @@
import {
IDataObject,
INodeExecutionData,
ICredentialDataDecryptedObject
} from 'n8n-workflow';
import {
IMongoCredentialsType,
IMongoParametricCredentials,
IMongoCredentials
} from './mongo.node.types';
/**
* Standard way of building the MongoDB connection string, unless overridden with a provided string
*
* @param {ICredentialDataDecryptedObject} credentials MongoDB credentials to use, unless conn string is overridden
*/
function buildParameterizedConnString(
credentials: IMongoParametricCredentials
): string {
if (credentials.port) {
return `mongodb://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}`;
} else {
return `mongodb+srv://${credentials.user}:${credentials.password}@${credentials.host}`;
}
}
/**
* Build mongoDb connection string and resolve database name.
* If a connection string override value is provided, that will be used in place of individual args
*
* @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use
*/
function buildMongoConnectionParams(
credentials: IMongoCredentialsType
): IMongoCredentials {
const sanitizedDbName =
credentials.database && credentials.database.trim().length > 0
? credentials.database.trim()
: '';
if (credentials.configurationType === 'connectionString') {
if (
credentials.connectionString &&
credentials.connectionString.trim().length > 0
) {
return {
connectionString: credentials.connectionString.trim(),
database: sanitizedDbName
};
} else {
throw new Error(
'Cannot override credentials: valid MongoDB connection string not provided '
);
}
} else {
return {
connectionString: buildParameterizedConnString(credentials),
database: sanitizedDbName
};
}
}
/**
* Verify credentials. If ok, build mongoDb connection string and resolve database name.
*
* @param {ICredentialDataDecryptedObject} credentials raw/input MongoDB credentials to use
*/
export function validateAndResolveMongoCredentials(
credentials?: ICredentialDataDecryptedObject
): IMongoCredentials {
if (credentials === undefined) {
throw new Error('No credentials got returned!');
} else {
return buildMongoConnectionParams(
credentials as unknown as IMongoCredentialsType,
);
}
}
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
export function getItemCopy(
items: INodeExecutionData[],
properties: string[]
): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map(item => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}

View file

@ -0,0 +1,58 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
IDataObject,
} from 'n8n-workflow';
/**
* Make an API request to MSG91
*
* @param {IHookFunctions | IExecuteFunctions} this
* @param {string} method
* @param {string} endpoint
* @param {object} form
* @param {object | undefined} qs
* @returns {Promise<any>}
*/
export async function sms77ApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, form: IDataObject, qs?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('sms77Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
if ('GET' === method) {
qs = setPayload(credentials, qs);
} else {
form = setPayload(credentials, form);
}
const response = await this.helpers.request({
form,
json: true,
method,
qs,
uri: `https://gateway.sms77.io/api/${endpoint}`,
});
if ('100' !== response.success) {
throw new Error('Invalid sms77 credentials or API error!');
}
return response;
}
function setPayload(credentials: ICredentialDataDecryptedObject, o?: IDataObject) {
if (!o) {
o = {};
}
o.p = credentials!.apiKey as string;
o.json = 1;
o.sendwith = 'n8n';
return o;
}

View file

@ -0,0 +1,144 @@
import {IExecuteFunctions,} from 'n8n-core';
import {IDataObject, INodeExecutionData, INodeType, INodeTypeDescription,} from 'n8n-workflow';
import {sms77ApiRequest} from './GenericFunctions';
export class Sms77 implements INodeType {
description: INodeTypeDescription = {
displayName: 'Sms77',
name: 'sms77',
icon: 'file:sms77.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Send SMS',
defaults: {
name: 'Sms77',
color: '#18D46A',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'sms77Api',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'SMS',
value: 'sms',
},
],
default: 'sms',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'sms',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send SMS',
},
],
default: 'send',
description: 'The operation to perform.',
},
{
displayName: 'From',
name: 'from',
type: 'string',
default: '',
placeholder: '+4901234567890',
required: false,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'The number from which to send the message.',
},
{
displayName: 'To',
name: 'to',
type: 'string',
default: '',
placeholder: '+49876543210',
required: true,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'The number, with coutry code, to which to send the message.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'The message to send',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: IDataObject[] = [];
for (let i = 0; i < this.getInputData().length; i++) {
const resource = this.getNodeParameter('resource', i);
if ('sms' !== resource) {
throw new Error(`The resource "${resource}" is not known!`);
}
const operation = this.getNodeParameter('operation', i);
if ('send' !== operation) {
throw new Error(`The operation "${operation}" is not known!`);
}
const responseData = await sms77ApiRequest.call(this, 'POST', 'sms', {}, {
from: this.getNodeParameter('from', i),
to: this.getNodeParameter('to', i),
text: this.getNodeParameter('message', i),
});
returnData.push(responseData);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,82 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IHookFunctions,
IWebhookFunctions
} from 'n8n-workflow';
export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('surveyMonkeyApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const endpoint = 'https://api.surveymonkey.com/v3';
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': `bearer ${credentials.accessToken}`,
},
method,
body,
qs: query,
uri: uri || `${endpoint}${resource}`,
json: true
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(query).length) {
delete options.qs;
}
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.error.message;
if (errorMessage !== undefined) {
throw new Error(`SurveyMonkey error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function surveyMonkeyRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
query.per_page = 100;
let uri: string | undefined;
do {
responseData = await surveyMonkeyApiRequest.call(this, method, endpoint, body, query, uri);
uri = responseData.links.next;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData.links.next
);
return returnData;
}
export function idsExist(ids: string[], surveyIds: string[]) {
for (const surveyId of surveyIds) {
if (!ids.includes(surveyId)) {
return false;
}
}
return true;
}

View file

@ -0,0 +1,47 @@
import {
IDataObject,
} from 'n8n-workflow';
export interface IImage {
url: string;
}
export interface IChoice {
position: number;
visible: boolean;
text: string;
id: string;
weight: number;
description: string;
image?: IImage;
}
export interface IRow {
position: number;
visible: boolean;
text: string;
id: string;
}
export interface IOther {
text: string;
visible: boolean;
is_answer_choice: boolean;
id: string;
}
export interface IQuestion {
id: string;
family?: string;
subtype?: string;
headings?: IDataObject[];
answers: IDataObject;
rows?: IDataObject;
}
export interface IAnswer {
choice_id: string;
row_id?: string;
text?: string;
other_id?: string;
}

View file

@ -0,0 +1,703 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import {
idsExist,
surveyMonkeyApiRequest,
surveyMonkeyRequestAllItems,
} from './GenericFunctions';
import {
IAnswer,
IChoice,
IQuestion,
IRow,
IOther,
} from './Interfaces';
import {
createHmac,
} from 'crypto';
export class SurveyMonkeyTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'SurveyMonkey Trigger',
name: 'surveyMonkeyTrigger',
icon: 'file:surveyMonkey.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Survey Monkey events occure.',
defaults: {
name: 'SurveyMonkey Trigger',
color: '#53b675',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'surveyMonkeyApi',
required: true,
},
],
webhooks: [
{
name: 'setup',
httpMethod: 'HEAD',
responseMode: 'onReceived',
path: 'webhook',
},
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Type',
name: 'objectType',
type: 'options',
options: [
{
name: 'Collector',
value: 'collector',
},
{
name: 'Survey',
value: 'survey',
},
],
default: '',
required: true,
},
{
displayName: 'Event',
name: 'event',
displayOptions: {
show: {
objectType: [
'survey'
],
},
},
type: 'options',
options: [
{
name: 'Collector Created',
value: 'collector_created',
description: 'A collector is created',
},
{
name: 'Collector Updated',
value: 'collector_updated',
description: 'A collector is updated',
},
{
name: 'Collector Deleted',
value: 'collector_deleted',
description: 'A collector is deleted',
},
{
name: 'Response Completed',
value: 'response_completed',
description: 'A survey response is completed',
},
{
name: 'Response Created',
value: 'response_created',
description: 'A respondent begins a survey',
},
{
name: 'Response Deleted',
value: 'response_deleted',
description: 'A response is deleted',
},
{
name: 'Response Disqualified',
value: 'response_disqualified',
description: 'A survey response is disqualified ',
},
{
name: 'Response Overquota',
value: 'response_overquota',
description: `A response is over a surveys quota`,
},
{
name: 'Response Updated',
value: 'response_updated',
description: 'A survey response is updated',
},
{
name: 'Survey Created',
value: 'survey_created',
description: 'A survey is created',
},
{
name: 'Survey Deleted',
value: 'survey_deleted',
description: 'A survey is deleted',
},
{
name: 'Survey Updated',
value: 'survey_updated',
description: 'A survey is updated',
},
],
default: '',
required: true,
},
{
displayName: 'Event',
name: 'event',
type: 'options',
displayOptions: {
show: {
objectType: [
'collector',
],
},
},
options: [
{
name: 'Collector Updated',
value: 'collector_updated',
description: 'A collector is updated',
},
{
name: 'Collector Deleted',
value: 'collector_deleted',
description: 'A collector is deleted',
},
{
name: 'Response Completed',
value: 'response_completed',
description: 'A survey response is completed',
},
{
name: 'Response Created',
value: 'response_created',
description: 'A respondent begins a survey',
},
{
name: 'Response Deleted',
value: 'response_deleted',
description: 'A response is deleted',
},
{
name: 'Response Disqualified',
value: 'response_disqualified',
description: 'A survey response is disqualified ',
},
{
name: 'Response Overquota',
value: 'response_overquota',
description: `A response is over a surveys quota`,
},
{
name: 'Response Updated',
value: 'response_updated',
description: 'A survey response is updated',
},
],
default: '',
required: true,
},
{
displayName: 'Survey IDs',
name: 'surveyIds',
type: 'multiOptions',
displayOptions: {
show: {
objectType: [
'survey',
],
},
hide: {
event: [
'survey_created',
],
},
},
typeOptions: {
loadOptionsMethod: 'getSurveys',
},
options: [],
default: [],
required: true,
},
{
displayName: 'Survey ID',
name: 'surveyId',
type: 'options',
displayOptions: {
show: {
objectType: [
'collector',
],
},
},
typeOptions: {
loadOptionsMethod: 'getSurveys',
},
default: [],
required: true,
},
{
displayName: 'Collector IDs',
name: 'collectorIds',
type: 'multiOptions',
displayOptions: {
show: {
objectType: [
'collector',
],
},
},
typeOptions: {
loadOptionsMethod: 'getCollectors',
loadOptionsDependsOn: [
'surveyId',
],
},
options: [],
default: [],
required: true,
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
displayOptions: {
show: {
event: [
'response_completed',
],
},
},
default: true,
description: 'By default the webhook-data only contain the IDs. If this option gets activated it<br />will resolve the data automatically.',
},
{
displayName: 'Only Answers',
name: 'onlyAnswers',
displayOptions: {
show: {
resolveData: [
true,
],
event: [
'response_completed',
],
},
},
type: 'boolean',
default: true,
description: 'Returns only the answers of the form and not any of the other data.',
},
],
};
methods = {
loadOptions: {
// Get all the survey's collectors to display them to user so that he can
// select them easily
async getCollectors(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const surveyId = this.getCurrentNodeParameter('surveyId');
const returnData: INodePropertyOptions[] = [];
const collectors = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', `/surveys/${surveyId}/collectors`);
for (const collector of collectors) {
const collectorName = collector.name;
const collectorId = collector.id;
returnData.push({
name: collectorName,
value: collectorId,
});
}
return returnData;
},
// Get all the surveys to display them to user so that he can
// select them easily
async getSurveys(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const surveys = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', '/surveys');
for (const survey of surveys) {
const surveyName = survey.title;
const surveyId = survey.id;
returnData.push({
name: surveyName,
value: surveyId,
});
}
return returnData;
},
},
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const objectType = this.getNodeParameter('objectType') as string;
const event = this.getNodeParameter('event') as string;
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
const endpoint = '/webhooks';
const responseData = await surveyMonkeyRequestAllItems.call(this, 'data', 'GET', endpoint, {});
const webhookUrl = this.getNodeWebhookUrl('default');
const ids: string[] = [];
if (objectType === 'survey' && event !== 'survey_created') {
const surveyIds = this.getNodeParameter('surveyIds') as string[];
ids.push.apply(ids, surveyIds);
} else if (objectType === 'collector') {
const collectorIds = this.getNodeParameter('collectorIds') as string[];
ids.push.apply(ids, collectorIds);
}
for (const webhook of responseData) {
const webhookDetails = await surveyMonkeyApiRequest.call(this, 'GET', `/webhooks/${webhook.id}`);
if (webhookDetails.subscription_url === webhookUrl
&& idsExist(webhookDetails.object_ids as string[], ids as string[])
&& webhookDetails.event_type === event) {
// Set webhook-id to be sure that it can be deleted
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = webhook.id as string;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const event = this.getNodeParameter('event') as string;
const objectType = this.getNodeParameter('objectType') as string;
const endpoint = '/webhooks';
const ids: string[] = [];
if (objectType === 'survey' && event !== 'survey_created') {
const surveyIds = this.getNodeParameter('surveyIds') as string[];
ids.push.apply(ids, surveyIds);
} else if (objectType === 'collector') {
const collectorIds = this.getNodeParameter('collectorIds') as string[];
ids.push.apply(ids, collectorIds);
}
const body: IDataObject = {
name: `n8n - Webhook [${event}]`,
object_type: objectType,
object_ids: ids,
subscription_url: webhookUrl,
event_type: event,
};
if (objectType === 'survey' && event === 'survey_created') {
delete body.object_type;
delete body.object_ids;
}
let responseData: IDataObject = {};
responseData = await surveyMonkeyApiRequest.call(this, 'POST', endpoint, body);
if (responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/${webhookData.webhookId}`;
try {
await surveyMonkeyApiRequest.call(this, 'DELETE', endpoint);
} catch (e) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const event = this.getNodeParameter('event') as string;
const objectType = this.getNodeParameter('objectType') as string;
const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject;
const headerData = this.getHeaderData() as IDataObject;
const req = this.getRequestObject();
const webhookName = this.getWebhookName();
if (webhookName === 'setup') {
// It is a create webhook confirmation request
return {};
}
if (headerData['sm-signature'] === undefined) {
return {};
}
return new Promise((resolve, reject) => {
const data: Buffer[] = [];
req.on('data', (chunk) => {
data.push(chunk);
});
req.on('end', async () => {
const computedSignature = createHmac('sha1', `${credentials.clientId}&${credentials.clientSecret}`).update(data.join('')).digest('base64');
if (headerData['sm-signature'] !== computedSignature) {
// Signature is not valid so ignore call
return {};
}
let responseData = JSON.parse(data.join(''));
let endpoint = '';
let returnItem: INodeExecutionData[] = [
{
json: responseData,
}
];
if (event === 'response_completed') {
const resolveData = this.getNodeParameter('resolveData') as boolean;
if (resolveData) {
if (objectType === 'survey') {
endpoint = `/surveys/${responseData.resources.survey_id}/responses/${responseData.object_id}/details`;
} else {
endpoint = `/collectors/${responseData.resources.collector_id}/responses/${responseData.object_id}/details`;
}
responseData = await surveyMonkeyApiRequest.call(this, 'GET', endpoint);
const surveyId = responseData.survey_id;
const questions: IQuestion[] = [];
const answers = new Map<string, IAnswer[]>();
const { pages } = await surveyMonkeyApiRequest.call(this, 'GET', `/surveys/${surveyId}/details`);
for (const page of pages) {
questions.push.apply(questions, page.questions);
}
for (const page of responseData.pages as IDataObject[]) {
for (const question of page.questions as IDataObject[]) {
answers.set(question.id as string, question.answers as IAnswer[]);
}
}
const responseQuestions = new Map<string, number | string | string[] | IDataObject>();
for (const question of questions) {
/*
TODO: add support for premium components
- File Upload
- Matrix of dropdowm menus
*/
// if question does not have an answer ignore it
if (!answers.get(question.id)) {
continue;
}
const heading = question.headings![0].heading as string;
if (question.family === 'open_ended' || question.family === 'datetime') {
if (question.subtype !== 'multi') {
responseQuestions.set(heading, answers.get(question.id)![0].text as string);
} else {
const results: IDataObject = {};
const keys = (question.answers.rows as IRow[]).map(e => e.text) as string[];
const values = answers.get(question.id)?.map(e => e.text) as string[];
for (let i = 0; i < keys.length; i++) {
// if for some reason there are questions texts repeted add the index to the key
if (results[keys[i]] !== undefined) {
results[`${keys[i]}(${i})`] = values[i] || '';
} else {
results[keys[i]] = values[i] || '';
}
}
responseQuestions.set(heading, results);
}
}
if (question.family === 'single_choice') {
const other = question.answers.other as IOther;
if (other && other.visible && other.is_answer_choice && answers.get(question.id)![0].other_id) {
responseQuestions.set(heading, answers.get(question.id)![0].text as string);
} else if (other && other.visible && !other.is_answer_choice){
const choiceId = answers.get(question.id)![0].choice_id;
const choice = (question.answers.choices as IChoice[])
.filter(e => e.id === choiceId)[0];
const comment = answers.get(question.id)
?.find(e => e.other_id === other.id)?.text as string;
responseQuestions.set(heading, { value: choice.text, comment });
} else {
const choiceId = answers.get(question.id)![0].choice_id;
const choice = (question.answers.choices as IChoice[])
.filter(e => e.id === choiceId)[0];
responseQuestions.set(heading, choice.text);
}
}
if (question.family === 'multiple_choice') {
const other = question.answers.other as IOther;
const choiceIds = answers.get(question.id)?.map((e) => e.choice_id);
const value = (question.answers.choices as IChoice[])
.filter(e => choiceIds?.includes(e.id))
.map(e => e.text) as string[];
// if "Add an "Other" Answer Option for Comments" is active and was selected
if (other && other.is_answer_choice && other.visible) {
const text = answers.get(question.id)
?.find(e => e.other_id === other.id)?.text as string;
value.push(text);
}
responseQuestions.set(heading, value);
}
if (question.family === 'matrix') {
// if more than one row it's a matrix/rating-scale
const rows = question.answers.rows as IRow[];
if (rows.length > 1) {
const results: IDataObject = {};
const choiceIds = answers.get(question.id)?.map(e => e.choice_id) as string[];
const rowIds = answers.get(question.id)?.map(e => e.row_id) as string[];
const rowsValues = (question.answers.rows as IRow[])
.filter(e => rowIds!.includes(e.id as string))
.map(e => e.text);
const choicesValues = (question.answers.choices as IChoice[])
.filter(e => choiceIds!.includes(e.id as string))
.map(e => e.text);
for (let i = 0; i < rowsValues.length; i++) {
results[rowsValues[i]] = choicesValues[i] || '';
}
// add the rows that were not answered
for (const row of question.answers.rows as IDataObject[]) {
if (!rowIds.includes(row.id as string)) {
results[row.text as string] = '';
}
}
// the comment then add the comment
const other = question.answers.other as IOther;
if (other !== undefined && other.visible) {
results.comment = answers.get(question.id)?.filter((e) => e.other_id)[0].text;
}
responseQuestions.set(heading, results);
} else {
const choiceIds = answers.get(question.id)?.map((e) => e.choice_id);
const value = (question.answers.choices as IChoice[])
.filter(e => choiceIds!.includes(e.id as string))
.map(e => (e.text === '') ? e.weight : e.text)[0];
responseQuestions.set(heading, value);
// if "Add an Other Answer Option for Comments" is active then add comment to the answer
const other = question.answers.other as IOther;
if (other !== undefined && other.visible) {
const response: IDataObject = {};
//const questionName = (question.answers.other as IOther).text as string;
const text = answers.get(question.id)?.filter((e) => e.other_id)[0].text;
response.value = value;
response.comment = text;
responseQuestions.set(heading, response);
}
}
}
if (question.family === 'demographic') {
const rows: IDataObject = {};
for (const row of answers.get(question.id) as IAnswer[]) {
rows[row.row_id as string] = row.text;
}
const addressInfo: IDataObject = {};
for (const answer of question.answers.rows as IDataObject[]) {
addressInfo[answer.type as string] = rows[answer.id as string] || '';
}
responseQuestions.set(heading, addressInfo);
}
if (question.family === 'presentation') {
if (question.subtype === 'image') {
const { url } = question.headings![0].image as IDataObject;
responseQuestions.set(heading, url as string);
}
}
}
delete responseData.pages;
responseData.questions = {};
// Map the "Map" to JSON
const tuples = JSON.parse(JSON.stringify([...responseQuestions]));
for (const [key, value] of tuples) {
responseData.questions[key] = value;
}
const onlyAnswers = this.getNodeParameter('onlyAnswers') as boolean;
if (onlyAnswers) {
responseData = responseData.questions;
}
returnItem = [
{
json: responseData,
}
];
}
}
return resolve({
workflowData: [
returnItem,
],
});
});
req.on('error', (err) => {
throw new Error(err.message);
});
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.60.0",
"version": "0.61.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -48,6 +48,7 @@
"dist/credentials/DriftApi.credentials.js",
"dist/credentials/DropboxApi.credentials.js",
"dist/credentials/EventbriteApi.credentials.js",
"dist/credentials/FacebookGraphApi.credentials.js",
"dist/credentials/FreshdeskApi.credentials.js",
"dist/credentials/FileMaker.credentials.js",
"dist/credentials/FlowApi.credentials.js",
@ -92,10 +93,12 @@
"dist/credentials/RundeckApi.credentials.js",
"dist/credentials/ShopifyApi.credentials.js",
"dist/credentials/SlackApi.credentials.js",
"dist/credentials/Sms77Api.credentials.js",
"dist/credentials/Smtp.credentials.js",
"dist/credentials/StripeApi.credentials.js",
"dist/credentials/SalesmateApi.credentials.js",
"dist/credentials/SegmentApi.credentials.js",
"dist/credentials/SurveyMonkeyApi.credentials.js",
"dist/credentials/TelegramApi.credentials.js",
"dist/credentials/TodoistApi.credentials.js",
"dist/credentials/TrelloApi.credentials.js",
@ -153,6 +156,7 @@
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
"dist/nodes/ExecuteCommand.node.js",
"dist/nodes/ExecuteWorkflow.node.js",
"dist/nodes/Facebook/FacebookGraphApi.node.js",
"dist/nodes/FileMaker/FileMaker.node.js",
"dist/nodes/Freshdesk/Freshdesk.node.js",
"dist/nodes/Flow/Flow.node.js",
@ -218,6 +222,7 @@
"dist/nodes/Shopify/Shopify.node.js",
"dist/nodes/Shopify/ShopifyTrigger.node.js",
"dist/nodes/Slack/Slack.node.js",
"dist/nodes/Sms77/Sms77.node.js",
"dist/nodes/SplitInBatches.node.js",
"dist/nodes/SpreadsheetFile.node.js",
"dist/nodes/SseTrigger.node.js",
@ -226,6 +231,7 @@
"dist/nodes/Switch.node.js",
"dist/nodes/Salesmate/Salesmate.node.js",
"dist/nodes/Segment/Segment.node.js",
"dist/nodes/SurveyMonkey/SurveyMonkeyTrigger.node.js",
"dist/nodes/Telegram/Telegram.node.js",
"dist/nodes/Telegram/TelegramTrigger.node.js",
"dist/nodes/Todoist/Todoist.node.js",
@ -261,7 +267,7 @@
"@types/jest": "^24.0.18",
"@types/lodash.set": "^4.3.6",
"@types/moment-timezone": "^0.5.12",
"@types/mongodb": "^3.3.6",
"@types/mongodb": "^3.5.4",
"@types/node": "^10.10.1",
"@types/nodemailer": "^4.6.5",
"@types/redis": "^2.8.11",
@ -270,7 +276,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^24.9.0",
"n8n-workflow": "~0.29.0",
"n8n-workflow": "~0.30.0",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
"typescript": "~3.7.4"
@ -290,9 +296,11 @@
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"lodash.unset": "^4.5.2",
"mongodb": "^3.3.2",
"moment": "2.24.0",
"moment-timezone": "^0.5.28",
"mongodb": "^3.5.5",
"mysql2": "^2.0.1",
"n8n-core": "~0.32.0",
"n8n-core": "~0.33.0",
"nodemailer": "^5.1.1",
"pdf-parse": "^1.1.1",
"pg-promise": "^9.0.3",

View file

@ -21,7 +21,7 @@ Software: n8n
License: Apache 2.0
Licensor: Jan Oberhauser
Licensor: n8n GmbH
---------------------------------------------------------------------

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.29.0",
"version": "0.30.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -548,7 +548,7 @@ export interface IWorkflowMetadata {
active: boolean;
}
export type WebhookHttpMethod = 'GET' | 'POST';
export type WebhookHttpMethod = 'GET' | 'POST' | 'HEAD';
export interface IWebhookResponseData {
workflowData?: INodeExecutionData[][];

View file

@ -728,12 +728,6 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
return [];
}
if (workflow.id === undefined) {
// Workflow has no id which means it is not saved and so webhooks
// will not be enabled
return [];
}
const nodeType = workflow.nodeTypes.getByName(node.type) as INodeType;
if (nodeType.description.webhooks === undefined) {
@ -741,12 +735,14 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
return [];
}
const workflowId = workflow.id || '__UNSAVED__';
const returnData: IWebhookData[] = [];
for (const webhookDescription of nodeType.description.webhooks) {
let nodeWebhookPath = workflow.getSimpleParameterValue(node, webhookDescription['path'], 'GET');
if (nodeWebhookPath === undefined) {
// TODO: Use a proper logger
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflow.id}".`);
console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`);
continue;
}
@ -756,13 +752,13 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
nodeWebhookPath = nodeWebhookPath.slice(1);
}
const path = getNodeWebhookPath(workflow.id, node, nodeWebhookPath);
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath);
const httpMethod = workflow.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET');
if (httpMethod === undefined) {
// TODO: Use a proper logger
console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflow.id}" could not be added because the httpMethod is not defined.`);
console.error(`The webhook "${path}" for node "${node.name}" in workflow "${workflowId}" could not be added because the httpMethod is not defined.`);
continue;
}
@ -771,7 +767,7 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData:
node: node.name,
path,
webhookDescription,
workflowId: workflow.id,
workflowId,
workflowExecuteAdditionalData: additionalData,
});
}