mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' of github.com:n8n-io/n8n
This commit is contained in:
commit
2bed4a6246
|
@ -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
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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": {}
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
17
packages/nodes-base/credentials/Sms77Api.credentials.ts
Normal file
17
packages/nodes-base/credentials/Sms77Api.credentials.ts
Normal 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: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
418
packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts
Normal file
418
packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts
Normal 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];
|
||||
}
|
||||
}
|
BIN
packages/nodes-base/nodes/Facebook/facebook.png
Normal file
BIN
packages/nodes-base/nodes/Facebook/facebook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -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',
|
||||
|
|
|
@ -81,6 +81,9 @@ export const issueFields = [
|
|||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getProjects',
|
||||
loadOptionsDependsOn: [
|
||||
'jiraVersion',
|
||||
],
|
||||
},
|
||||
description: 'Project',
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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!`);
|
||||
}
|
||||
|
|
131
packages/nodes-base/nodes/MongoDb/mongo.node.options.ts
Normal file
131
packages/nodes-base/nodes/MongoDb/mongo.node.options.ts
Normal 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.'
|
||||
}
|
||||
]
|
||||
};
|
53
packages/nodes-base/nodes/MongoDb/mongo.node.types.ts
Normal file
53
packages/nodes-base/nodes/MongoDb/mongo.node.types.ts
Normal 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;
|
||||
};
|
104
packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts
Normal file
104
packages/nodes-base/nodes/MongoDb/mongo.node.utils.ts
Normal 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;
|
||||
});
|
||||
}
|
58
packages/nodes-base/nodes/Sms77/GenericFunctions.ts
Normal file
58
packages/nodes-base/nodes/Sms77/GenericFunctions.ts
Normal 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;
|
||||
}
|
144
packages/nodes-base/nodes/Sms77/Sms77.node.ts
Normal file
144
packages/nodes-base/nodes/Sms77/Sms77.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
BIN
packages/nodes-base/nodes/Sms77/sms77.png
Normal file
BIN
packages/nodes-base/nodes/Sms77/sms77.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
82
packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts
Normal file
82
packages/nodes-base/nodes/SurveyMonkey/GenericFunctions.ts
Normal 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;
|
||||
}
|
47
packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts
Normal file
47
packages/nodes-base/nodes/SurveyMonkey/Interfaces.ts
Normal 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;
|
||||
}
|
|
@ -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 survey’s 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 survey’s 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
BIN
packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png
Normal file
BIN
packages/nodes-base/nodes/SurveyMonkey/surveyMonkey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
|
@ -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",
|
||||
|
|
|
@ -21,7 +21,7 @@ Software: n8n
|
|||
|
||||
License: Apache 2.0
|
||||
|
||||
Licensor: Jan Oberhauser
|
||||
Licensor: n8n GmbH
|
||||
|
||||
|
||||
---------------------------------------------------------------------
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[][];
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue