Merge branch 'master' into feature/airtable-trigger

This commit is contained in:
ricardo 2020-11-14 20:11:30 -05:00
commit fd1897a288
56 changed files with 3433 additions and 235 deletions

View file

@ -2,6 +2,21 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 0.93.0
### What changed?
Change in naming of the Authentication field for the Pipedrive Trigger node.
### When is action necessary?
If you had set "Basic Auth" for the "Authentication" field in the node.
### How to upgrade:
The "Authentication" field has been renamed to "Incoming Authentication". Please set the parameter “Incoming Authentication” to “Basic Auth” to activate it again.
## 0.90.0
### What changed?

View file

@ -15,7 +15,6 @@ import {
Db,
ExternalHooks,
GenericHelpers,
IExecutionsCurrentSummary,
LoadNodesAndCredentials,
NodeTypes,
Server,

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.92.0",
"version": "0.93.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -103,10 +103,10 @@
"lodash.get": "^4.4.2",
"mongodb": "^3.5.5",
"mysql2": "~2.1.0",
"n8n-core": "~0.50.0",
"n8n-editor-ui": "~0.62.0",
"n8n-nodes-base": "~0.87.0",
"n8n-workflow": "~0.43.0",
"n8n-core": "~0.51.0",
"n8n-editor-ui": "~0.63.0",
"n8n-nodes-base": "~0.88.0",
"n8n-workflow": "~0.44.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^8.3.0",

View file

@ -1534,17 +1534,21 @@ class App {
// Loads the currently saved workflow to execute instead of the
// one saved at the time of the execution.
const workflowId = fullExecutionData.workflowData.id;
data.workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase;
const workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase;
if (data.workflowData === undefined) {
if (workflowData === undefined) {
throw new Error(`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`);
}
data.workflowData = workflowData;
const nodeTypes = NodeTypes();
const workflowInstance = new Workflow({ id: workflowData.id as string, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings });
// Replace all of the nodes in the execution stack with the ones of the new workflow
for (const stack of data!.executionData!.executionData!.nodeExecutionStack) {
// Find the data of the last executed node in the new workflow
const node = data.workflowData.nodes.find(node => node.name === stack.node.name);
if (node === undefined) {
const node = workflowInstance.getNode(stack.node.name);
if (node === null) {
throw new Error(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`);
}

View file

@ -222,7 +222,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
return;
}
// Now that we know that the workflow should run we can return the default respons
// Now that we know that the workflow should run we can return the default response
// directly if responseMode it set to "onReceived" and a respone should be sent
if (responseMode === 'onReceived' && didSendResponse === false) {
// Return response directly and do not wait for the workflow to finish
@ -302,6 +302,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
if(data.data.resultData.error || returnData?.error !== undefined) {
if (didSendResponse === false) {
responseCallback(null, {
data: {
message: 'Workflow did error.',
},
responseCode: 500,
});
}
didSendResponse = true;
return data;
}
if (returnData === undefined) {
if (didSendResponse === false) {
responseCallback(null, {
@ -313,17 +326,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] {
}
didSendResponse = true;
return data;
} else if (returnData.error !== undefined) {
if (didSendResponse === false) {
responseCallback(null, {
data: {
message: 'Workflow did error.',
},
responseCode: 500,
});
}
didSendResponse = true;
return data;
}
const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson');

View file

@ -202,6 +202,18 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
}
export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks {
const externalHooks = ExternalHooks();
return {
workflowExecuteBefore: [
async function (this: WorkflowHooks, workflow: Workflow): Promise<void> {
await externalHooks.run('workflow.preExecute', [workflow, this.mode]);
},
],
};
}
/**
* Returns hook functions to save workflow execution and call error workflow
*
@ -337,7 +349,6 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
const externalHooks = ExternalHooks();
await externalHooks.init();
await externalHooks.run('workflow.execute', [workflowData, mode]);
const nodeTypes = NodeTypes();
@ -462,6 +473,10 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara
export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks {
optionalParameters = optionalParameters || {};
const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode);
const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode);
for (const key of Object.keys(preExecuteFunctions)) {
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
}
return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters);
}
@ -474,12 +489,19 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI
* @param {string} executionId
* @returns {WorkflowHooks}
*/
export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string): WorkflowHooks {
export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string, isMainProcess = false): WorkflowHooks {
const hookFunctions = hookFunctionsSave();
const pushFunctions = hookFunctionsPush();
for (const key of Object.keys(pushFunctions)) {
hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]);
}
if (isMainProcess) {
const preExecuteFunctions = hookFunctionsPreExecute();
for (const key of Object.keys(preExecuteFunctions)) {
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
}
}
return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string});
}

View file

@ -100,9 +100,6 @@ export class WorkflowRunner {
* @memberof WorkflowRunner
*/
async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise<string> {
const externalHooks = ExternalHooks();
await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]);
const executionsProcess = config.get('executions.process') as string;
let executionId: string;
@ -112,6 +109,7 @@ export class WorkflowRunner {
executionId = await this.runSubprocess(data, loadStaticData);
}
const externalHooks = ExternalHooks();
if (externalHooks.exists('workflow.postExecute')) {
this.activeExecutions.getPostExecutePromise(executionId)
.then(async (executionData) => {
@ -148,7 +146,7 @@ export class WorkflowRunner {
// Register the active execution
const executionId = this.activeExecutions.add(data, undefined);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId);
additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true);
let workflowExecution: PCancelable<IRun>;
if (data.executionData !== undefined) {

View file

@ -2,6 +2,7 @@
import {
CredentialsOverwrites,
CredentialTypes,
ExternalHooks,
IWorkflowExecutionDataProcessWithExecution,
NodeTypes,
WorkflowExecuteAdditionalData,
@ -19,6 +20,7 @@ import {
INodeTypeData,
IRun,
ITaskData,
IWorkflowExecuteHooks,
Workflow,
WorkflowHooks,
} from 'n8n-workflow';
@ -68,6 +70,10 @@ export class WorkflowRunnerProcess {
const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init(inputData.credentialsOverwrite);
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings});
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials);
additionalData.hooks = this.getProcessForwardHooks();
@ -121,7 +127,7 @@ export class WorkflowRunnerProcess {
* @returns
*/
getProcessForwardHooks(): WorkflowHooks {
const hookFunctions = {
const hookFunctions: IWorkflowExecuteHooks = {
nodeExecuteBefore: [
async (nodeName: string): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
@ -144,6 +150,11 @@ export class WorkflowRunnerProcess {
],
};
const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute();
for (const key of Object.keys(preExecuteFunctions)) {
hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
}
return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string });
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.50.0",
"version": "0.51.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -47,7 +47,7 @@
"file-type": "^14.6.2",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.43.0",
"n8n-workflow": "~0.44.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"request": "^2.88.2",

View file

@ -127,7 +127,7 @@ export class ActiveWorkflows {
for (const item of pollTimes.item) {
cronTime = [];
if (item.mode === 'custom') {
cronTimes.push(item.cronExpression as string);
cronTimes.push((item.cronExpression as string).trim());
continue;
}
if (item.mode === 'everyMinute') {
@ -178,6 +178,11 @@ export class ActiveWorkflows {
// Start the cron-jobs
const cronJobs: CronJob[] = [];
for (const cronTime of cronTimes) {
const cronTimeParts = cronTime.split(' ');
if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) {
throw new Error('The polling interval is too short. It has to be at least a minute!');
}
cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone));
}

View file

@ -468,7 +468,6 @@ export class WorkflowExecute {
this.runExecutionData.startData = {};
}
this.executeHook('workflowExecuteBefore', []);
let currentExecutionTry = '';
let lastExecutionTry = '';
@ -482,6 +481,35 @@ export class WorkflowExecute {
});
const returnPromise = (async () => {
try {
await this.executeHook('workflowExecuteBefore', [workflow]);
} catch (error) {
// Set the error that it can be saved correctly
executionError = {
message: error.message,
stack: error.stack,
};
// Set the incoming data of the node that it can be saved correctly
executionData = this.runExecutionData.executionData!.nodeExecutionStack[0] as IExecuteData;
this.runExecutionData.resultData = {
runData: {
[executionData.node.name]: [
{
startTime,
executionTime: (new Date().getTime()) - startTime,
data: ({
'main': executionData.data.main,
} as ITaskDataConnections),
},
],
},
lastNodeExecuted: executionData.node.name,
error: executionError,
};
throw error;
}
executionLoop:
while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.62.0",
"version": "0.63.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -65,7 +65,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.43.0",
"n8n-workflow": "~0.44.0",
"node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -20,7 +20,7 @@
</g>
</g>
</svg>
<span class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span>
<span class="doc-link-text">Need help? <a class="doc-hyperlink" :href="documentationUrl" target="_blank">Open credential docs</a></span>
</div>
</div>
</div>
@ -119,7 +119,11 @@ export default mixins(
const credentialType = this.$store.getters.credentialType(credentialTypeName);
if (credentialType.documentationUrl !== undefined) {
return `${credentialType.documentationUrl}`;
if (credentialType.documentationUrl.startsWith('http')) {
return credentialType.documentationUrl;
} else {
return 'https://docs.n8n.io/credentials/' + credentialType.documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal';
}
}
return undefined;
},

View file

@ -9,7 +9,7 @@
</div>
<transition name="fade">
<div v-if="showDocumentHelp && nodeType" class="doc-help-wrapper">
<svg id="help-logo" v-if="showDocumentHelp && nodeType" :href="'https://docs.n8n.io/nodes/' + nodeType.name" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<svg id="help-logo" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Node Documentation</title>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
@ -26,7 +26,7 @@
</svg>
<div v-if="showDocumentHelp && nodeType" class="text">
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="'https://docs.n8n.io/nodes/' + nodeType.name + '?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=' + nodeType.name" target="_blank">Open {{nodeType.displayName}} documentation</a>
Need help? <a id="doc-hyperlink" v-if="showDocumentHelp && nodeType" :href="documentationUrl" target="_blank">Open {{nodeType.displayName}} documentation</a>
</div>
</div>
</transition>
@ -65,6 +65,17 @@ export default Vue.extend({
};
},
computed: {
documentationUrl (): string {
if (!this.nodeType) {
return '';
}
if (this.nodeType.documentationUrl && this.nodeType.documentationUrl.startsWith('http')) {
return this.nodeType.documentationUrl;
}
return 'https://docs.n8n.io/nodes/' + (this.nodeType.documentationUrl || this.nodeType.name) + '?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=' + this.nodeType.name;
},
node (): INodeUi {
return this.$store.getters.activeNode;
},

View file

@ -82,8 +82,8 @@
</el-select>
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
<el-color-picker :value="displayValue" :disabled="isReadOnly" @change="valueChanged" size="small" class="color-picker" @focus="setFocus" :title="displayTitle" ></el-color-picker>
<el-input v-model="tempValue" size="small" type="text" :value="displayValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" ></el-input>
<el-color-picker :value="displayValue" :disabled="isReadOnly" @change="valueChanged" size="small" class="color-picker" @focus="setFocus" :title="displayTitle" :show-alpha="getArgument('showAlpha')"></el-color-picker>
<el-input v-model="tempValue" size="small" type="text" :value="tempValue" :disabled="isReadOnly" @change="valueChanged" @keydown.stop @focus="setFocus" :title="displayTitle" ></el-input>
</div>
<div v-else-if="parameter.type === 'boolean'">
@ -213,6 +213,10 @@ export default mixins(
this.loadRemoteParameterOptions();
},
value () {
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) {
// Do not set for color with alpha else wrong value gets displayed in field
return;
}
this.tempValue = this.displayValue as string;
},
},
@ -274,6 +278,18 @@ export default mixins(
returnValue = this.expressionValueComputed;
}
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && returnValue.charAt(0) === '#') {
// Convert the value to rgba that el-color-picker can display it correctly
const bigint = parseInt(returnValue.slice(1), 16);
const h = [];
h.push((bigint >> 24) & 255);
h.push((bigint >> 16) & 255);
h.push((bigint >> 8) & 255);
h.push((255 - bigint & 255) / 255);
returnValue = 'rgba('+h.join()+')';
}
if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') {
const rows = this.getArgument('rows');
if (rows === undefined || rows === 1) {
@ -537,14 +553,35 @@ export default mixins(
// Set focus on field
setTimeout(() => {
// @ts-ignore
(this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus();
if (this.$refs.inputField.$el) {
// @ts-ignore
(this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus();
}
});
},
rgbaToHex (value: string): string | null {
// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
const valueMatch = (value as string).match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);
if (valueMatch === null) {
// TODO: Display something if value is not valid
return null;
}
const [r, g, b, a] = valueMatch.splice(1, 4).map(v => Number(v));
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) + ((1 << 8) + Math.floor((1-a)*255)).toString(16).slice(1);
},
valueChanged (value: string | number | boolean | Date | null) {
if (value instanceof Date) {
value = value.toISOString();
}
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value.toString().charAt(0) !== '#') {
const newValue = this.rgbaToHex(value as string);
if (newValue !== null) {
this.tempValue = newValue;
value = newValue;
}
}
const parameterData = {
node: this.node !== null ? this.node.name : this.nodeName,
name: this.path,
@ -570,6 +607,13 @@ export default mixins(
this.nodeName = this.node.name;
}
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && this.displayValue !== null && this.displayValue.toString().charAt(0) !== '#') {
const newValue = this.rgbaToHex(this.displayValue as string);
if (newValue !== null) {
this.tempValue = newValue;
}
}
if (this.remoteMethod !== undefined && this.node !== null) {
// Make sure to load the parameter options
// directly and whenever the credentials change

View file

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

View file

@ -0,0 +1,47 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class GetResponseOAuth2Api implements ICredentialType {
name = 'getResponseOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'GetResponse OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://app.getresponse.com/oauth2_authorize.html',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.getresponse.com/v3/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
description: 'Resource to consume.',
},
];
}

View file

@ -0,0 +1,33 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class GotifyApi implements ICredentialType {
name = 'gotifyApi';
displayName = 'Gotify API';
documentationUrl = 'gotify';
properties = [
{
displayName: 'App API Token',
name: 'appApiToken',
type: 'string' as NodePropertyTypes,
default: '',
description: '(Optional) Needed for message creation.',
},
{
displayName: 'Client API Token',
name: 'clientApiToken',
type: 'string' as NodePropertyTypes,
default: '',
description: '(Optional) Needed for everything (delete, getAll) but message creation.',
},
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
description: 'The URL of the Gotify host.',
},
];
}

View file

@ -0,0 +1,48 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class LineNotifyOAuth2Api implements ICredentialType {
name = 'lineNotifyOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Line Notify OAuth2 API';
documentationUrl = 'line';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://notify-bot.line.me/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://notify-bot.line.me/oauth/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'notify',
required: true,
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -44,5 +44,11 @@ export class MicrosoftSql implements ICredentialType {
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'TLS',
name: 'tls',
type: 'boolean' as NodePropertyTypes,
default: true,
},
];
}

View file

@ -38,5 +38,25 @@ export class Sftp implements ICredentialType {
},
default: '',
},
{
displayName: 'Private Key',
name: 'privateKey',
type: 'string' as NodePropertyTypes,
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'String that contains a private key for either key-based or hostbased user authentication (OpenSSH format).',
},
{
displayName: 'Passphrase',
name: 'passphrase',
typeOptions: {
password: true,
},
type: 'string' as NodePropertyTypes,
default: '',
description: 'For an encrypted private key, this is the passphrase used to decrypt it.',
},
];
}

View file

@ -0,0 +1,33 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class StrapiApi implements ICredentialType {
name = 'strapiApi';
displayName = 'Strapi API';
properties = [
{
displayName: 'Email',
name: 'email',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'URL',
name: 'url',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'https://api.example.com',
},
];
}

View file

@ -1,6 +1,6 @@
import { ContainerOptions, Delivery } from 'rhea';
import { IExecuteSingleFunctions } from 'n8n-core';
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
@ -69,17 +69,15 @@ export class Amqp implements INodeType {
],
};
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
const item = this.getInputData();
async execute(this: IExecuteFunctions): Promise < INodeExecutionData[][] > {
const credentials = this.getCredentials('amqp');
if (!credentials) {
throw new Error('Credentials are mandatory!');
}
const sink = this.getNodeParameter('sink', '') as string;
const applicationProperties = this.getNodeParameter('headerParametersJson', {}) as string | object;
const options = this.getNodeParameter('options', {}) as IDataObject;
const sink = this.getNodeParameter('sink', 0, '') as string;
const applicationProperties = this.getNodeParameter('headerParametersJson', 0, {}) as string | object;
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
let headerProperties = applicationProperties;
if (typeof applicationProperties === 'string' && applicationProperties !== '') {
@ -109,35 +107,43 @@ export class Amqp implements INodeType {
connectOptions.transport = credentials.transportType;
}
const allSent = new Promise(( resolve ) => {
container.on('sendable', (context: any) => { // tslint:disable-line:no-any
const conn = container.connect(connectOptions);
const sender = conn.open_sender(sink);
let body: IDataObject | string = item.json;
const sendOnlyProperty = options.sendOnlyProperty as string;
const responseData: IDataObject[] = await new Promise((resolve) => {
container.once('sendable', (context: any) => { // tslint:disable-line:no-any
const returnData = [];
if (sendOnlyProperty) {
body = body[sendOnlyProperty] as string;
const items = this.getInputData();
for (let i = 0; i < items.length; i++) {
const item = items[i];
let body: IDataObject | string = item.json;
const sendOnlyProperty = options.sendOnlyProperty as string;
if (sendOnlyProperty) {
body = body[sendOnlyProperty] as string;
}
if (options.dataAsObject !== true) {
body = JSON.stringify(body);
}
const result = context.sender.send({
application_properties: headerProperties,
body,
});
returnData.push({ id: result.id });
}
if (options.dataAsObject !== true) {
body = JSON.stringify(body);
}
const message = {
application_properties: headerProperties,
body,
};
const sendResult = context.sender.send(message);
resolve(sendResult);
resolve(returnData);
});
});
container.connect(connectOptions).open_sender(sink);
sender.close();
conn.close();
const sendResult: Delivery = await allSent as Delivery; // sendResult has a a property that causes circular reference if returned
return { json: { id: sendResult.id } } as INodeExecutionData;
return [this.helpers.returnJsonArray(responseData)];
}
}

View file

@ -82,6 +82,20 @@ export class AmqpTrigger implements INodeType {
default: false,
description: 'Returns only the body property.',
},
{
displayName: 'Messages per Cicle',
name: 'pullMessagesNumber',
type: 'number',
default: 100,
description: 'Number of messages to pull from the bus for every cicle',
},
{
displayName: 'Sleep Time',
name: 'sleepTime',
type: 'number',
default: 10,
description: 'Milliseconds to sleep after every cicle.',
},
],
},
],
@ -99,6 +113,7 @@ export class AmqpTrigger implements INodeType {
const clientname = this.getNodeParameter('clientname', '') as string;
const subscription = this.getNodeParameter('subscription', '') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
const pullMessagesNumber = options.pullMessagesNumber || 100;
if (sink === '') {
throw new Error('Queue or Topic required!');
@ -130,10 +145,13 @@ export class AmqpTrigger implements INodeType {
connectOptions.transport = credentials.transportType;
}
let lastMsgId: number | undefined = undefined;
const self = this;
container.on('receiver_open', (context: any) => { // tslint:disable-line:no-any
context.receiver.add_credit(pullMessagesNumber);
});
container.on('message', (context: any) => { // tslint:disable-line:no-any
// ignore duplicate message check, don't think it's necessary, but it was in the rhea-lib example code
if (context.message.message_id && context.message.message_id === lastMsgId) {
@ -143,6 +161,12 @@ export class AmqpTrigger implements INodeType {
let data = context.message;
if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) {
// The buffer is not ready... Stringify and parse back to load it.
const cont = JSON.stringify(data.body.content);
data.body = String.fromCharCode.apply(null, JSON.parse(cont).data);
}
if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) {
// The buffer is not ready... Stringify and parse back to load it.
const content = JSON.stringify(data.body.content);
@ -158,6 +182,12 @@ export class AmqpTrigger implements INodeType {
self.emit([self.helpers.returnJsonArray([data])]);
if (context.receiver.credit === 0) {
setTimeout(() => {
context.receiver.add_credit(pullMessagesNumber);
}, options.sleepTime as number || 10);
}
});
const connection = container.connect(connectOptions);
@ -170,14 +200,14 @@ export class AmqpTrigger implements INodeType {
durable: 2,
expiry_policy: 'never',
},
credit_window: 1, // prefetch 1
credit_window: 0, // prefetch 1
};
} else {
clientOptions = {
source: {
address: sink,
},
credit_window: 1, // prefetch 1
credit_window: 0, // prefetch 1
};
}
connection.open_receiver(clientOptions);
@ -186,6 +216,8 @@ export class AmqpTrigger implements INodeType {
// The "closeFunction" function gets called by n8n whenever
// the workflow gets deactivated and can so clean up.
async function closeFunction() {
container.removeAllListeners('receiver_open');
container.removeAllListeners('message');
connection.close();
}

View file

@ -9,6 +9,12 @@ import {
INodeTypeDescription,
} from 'n8n-workflow';
import * as gm from 'gm';
import { file } from 'tmp-promise';
import {
writeFile as fsWriteFile,
} from 'fs';
import { promisify } from 'util';
const fsWriteFileAsync = promisify(fsWriteFile);
export class EditImage implements INodeType {
@ -61,6 +67,11 @@ export class EditImage implements INodeType {
value: 'resize',
description: 'Change the size of image',
},
{
name: 'Shear',
value: 'shear',
description: 'Shear image along the X or Y axis',
},
{
name: 'Text',
value: 'text',
@ -385,6 +396,11 @@ export class EditImage implements INodeType {
value: 'onlyIfSmaller',
description: 'Resize only if image is smaller than width or height',
},
{
name: 'Percent',
value: 'percent',
description: 'Width and height are specified in percents.',
},
],
default: 'maximumArea',
displayOptions: {
@ -422,7 +438,10 @@ export class EditImage implements INodeType {
displayName: 'Background Color',
name: 'backgroundColor',
type: 'color',
default: '#ffffff',
default: '#ffffffff',
typeOptions: {
showAlpha: true,
},
displayOptions: {
show: {
operation: [
@ -433,6 +452,39 @@ export class EditImage implements INodeType {
description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..',
},
// ----------------------------------
// shear
// ----------------------------------
{
displayName: 'Degrees X',
name: 'degreesX',
type: 'number',
default: 0,
displayOptions: {
show: {
operation: [
'shear',
],
},
},
description: 'X (horizontal) shear degrees.',
},
{
displayName: 'Degrees Y',
name: 'degreesY',
type: 'number',
default: 0,
displayOptions: {
show: {
operation: [
'shear',
],
},
},
description: 'Y (vertical) shear degrees.',
},
{
displayName: 'Options',
name: 'options',
@ -503,7 +555,6 @@ export class EditImage implements INodeType {
},
description: 'Sets the jpeg|png|tiff compression level from 0 to 100 (best).',
},
],
},
],
@ -529,6 +580,8 @@ export class EditImage implements INodeType {
let gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING));
gmInstance = gmInstance.background('transparent');
if (operation === 'blur') {
const blur = this.getNodeParameter('blur') as number;
const sigma = this.getNodeParameter('sigma') as number;
@ -574,6 +627,8 @@ export class EditImage implements INodeType {
option = '<';
} else if (resizeOption === 'onlyIfLarger') {
option = '>';
} else if (resizeOption === 'percent') {
option = '%';
}
gmInstance = gmInstance.resize(width, height, option);
@ -581,6 +636,10 @@ export class EditImage implements INodeType {
const rotate = this.getNodeParameter('rotate') as number;
const backgroundColor = this.getNodeParameter('backgroundColor') as string;
gmInstance = gmInstance.rotate(backgroundColor, rotate);
} else if (operation === 'shear') {
const xDegrees = this.getNodeParameter('degreesX') as number;
const yDegress = this.getNodeParameter('degreesY') as number;
gmInstance = gmInstance.shear(xDegrees, yDegress);
} else if (operation === 'text') {
const fontColor = this.getNodeParameter('fontColor') as string;
const fontSize = this.getNodeParameter('fontSize') as number;
@ -624,6 +683,8 @@ export class EditImage implements INodeType {
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, item.binary);
// Make a deep copy of the binary data we change
newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string]));
}
if (options.quality !== undefined) {

View file

@ -288,6 +288,8 @@ export class Ftp implements INodeType {
port: credentials.port as number,
username: credentials.username as string,
password: credentials.password as string,
privateKey: credentials.privateKey as string | undefined,
passphrase: credentials.passphrase as string | undefined,
});
} else {

View file

@ -0,0 +1,646 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const contactOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'contact',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new contact',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a contact',
},
{
name: 'Get',
value: 'get',
description: 'Get a contact',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all contacts',
},
{
name: 'Update',
value: 'update',
description: 'Update contact properties',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const contactFields = [
/* -------------------------------------------------------------------------- */
/* contact:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
default: '',
description: '',
},
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
default: '',
description: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'customFieldValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field ID',
name: 'customFieldId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
description: 'The end user specified key of the user defined data.',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
description: 'The end user specified value of the user defined data.',
default: '',
},
],
},
],
},
{
displayName: 'Day Of Cycle',
name: 'dayOfCycle',
type: 'string',
description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`,
default: '',
},
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`,
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
},
{
displayName: 'Scoring',
name: 'scoring',
type: 'number',
default: '',
description: 'Contact scoring, pass null to remove the score from a contact',
typeOptions: {
minValue: 0,
},
},
{
displayName: 'Tag IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'delete',
],
},
},
default: '',
description: 'Id of contact to delete.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'delete',
],
},
},
options: [
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
description: `This makes it possible to pass the IP from which the contact unsubscribed. Used only if the messageId was send.`,
default: '',
},
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
description: `The ID of a message (such as a newsletter, an autoresponder, or an RSS-newsletter). When passed, this method will simulate the unsubscribe process, as if the contact clicked the unsubscribe link in a given message.`,
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
default: '',
description: 'Unique identifier for a particular contact',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`,
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 20,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'string',
description: `Search contacts by campaign ID`,
default: '',
},
{
displayName: 'Change On From',
name: 'changeOnFrom',
type: 'dateTime',
default: '',
description: `Search contacts edited from this date`,
},
{
displayName: 'Change On To',
name: 'changeOnTo',
type: 'dateTime',
default: '',
description: `Search contacts edited to this date`,
},
{
displayName: 'Created On From',
name: 'createdOnFrom',
type: 'dateTime',
default: '',
description: `Count data from this date`,
},
{
displayName: 'Created On To',
name: 'createdOnTo',
type: 'dateTime',
default: '',
description: `Count data from this date`,
},
{
displayName: 'Exact Match',
name: 'exactMatch',
type: 'boolean',
default: false,
description: `When set to true it will search for contacts with the exact value<br>
of the email and name provided in the query string. Without this flag, matching is done via a standard 'like' comparison,<br>
which may sometimes be slow.`,
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`,
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
description: `Search contacts by name`,
default: '',
},
{
displayName: 'Origin',
name: 'origin',
type: 'options',
options: [
{
name: 'API',
value: 'api',
},
{
name: 'Copy',
value: 'copy',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Forward',
value: 'forward',
},
{
name: 'import',
value: 'import',
},
{
name: 'Iphone',
value: 'iphone',
},
{
name: 'Landing Page',
value: 'landing_page',
},
{
name: 'Leads',
value: 'leads',
},
{
name: 'Panel',
value: 'panel',
},
{
name: 'Sale',
value: 'sale',
},
{
name: 'Survey',
value: 'survey',
},
{
name: 'Webinar',
value: 'webinar',
},
{
name: 'WWW',
value: 'www',
},
],
description: `Search contacts by origin`,
default: '',
},
{
displayName: 'Sort By',
name: 'sortBy',
type: 'options',
options: [
{
name: 'Campaign ID',
value: 'campaignId',
},
{
name: 'Changed On',
value: 'changedOn',
},
{
name: 'Created On',
value: 'createdOn',
},
{
name: 'Email',
value: 'email',
},
],
default: '',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'ASC',
value: 'ASC',
},
{
name: 'DESC',
value: 'DESC',
},
],
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
default: '',
description: 'Unique identifier for a particular contact',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
default: '',
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'customFieldValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field ID',
name: 'customFieldId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
description: 'The end user specified key of the user defined data.',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
description: 'The end user specified value of the user defined data.',
default: '',
},
],
},
],
},
{
displayName: 'Day Of Cycle',
name: 'dayOfCycle',
type: 'string',
description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`,
default: '',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
},
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`,
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
},
{
displayName: 'Scoring',
name: 'scoring',
type: 'number',
default: '',
description: 'Contact scoring, pass null to remove the score from a contact',
typeOptions: {
minValue: 0,
},
},
{
displayName: 'Tag IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: '',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,70 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject
} from 'n8n-workflow';
export async function getresponseApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const authentication = this.getNodeParameter('authentication', 0, 'apiKey') as string;
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://api.getresponse.com/v3${resource}`,
json: true,
};
try {
options = Object.assign({}, options, option);
if (Object.keys(body).length === 0) {
delete options.body;
}
if (authentication === 'apiKey') {
const credentials = this.getCredentials('getResponseApi') as IDataObject;
options!.headers!['X-Auth-Token'] = `api-key ${credentials.apiKey}`;
//@ts-ignore
return await this.helpers.request.call(this, options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'getResponseOAuth2Api', options);
}
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier
throw new Error(`GetResponse error response [${error.statusCode}]: ${error.response.body.message}`);
}
throw error;
}
}
export async function getResponseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
do {
responseData = await getresponseApiRequest.call(this, method, endpoint, body, query, undefined, { resolveWithFullResponse: true });
query.page++;
returnData.push.apply(returnData, responseData.body);
} while (
responseData.headers.TotalPages !== responseData.headers.CurrentPage
);
return returnData;
}

View file

@ -0,0 +1,320 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
getresponseApiRequest,
getResponseApiRequestAllItems,
} from './GenericFunctions';
import {
contactFields,
contactOperations,
} from './ContactDescription';
import * as moment from 'moment-timezone';
export class GetResponse implements INodeType {
description: INodeTypeDescription = {
displayName: 'GetResponse',
name: 'getResponse',
icon: 'file:getResponse.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume GetResponse API.',
defaults: {
name: 'GetResponse',
color: '#00afec',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'getResponseApi',
required: true,
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'getResponseOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Key',
value: 'apiKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiKey',
description: 'The resource to operate on.',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Contact',
value: 'contact',
},
],
default: 'contact',
description: 'The resource to operate on.',
},
...contactOperations,
...contactFields,
],
};
methods = {
loadOptions: {
// Get all the campaigns to display them to user so that he can
// select them easily
async getCampaigns(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const campaigns = await getresponseApiRequest.call(
this,
'GET',
`/campaigns`,
);
for (const campaign of campaigns) {
returnData.push({
name: campaign.name as string,
value: campaign.campaignId,
});
}
return returnData;
},
// Get all the tagd to display them to user so that he can
// select them easily
async getTags(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tags = await getresponseApiRequest.call(
this,
'GET',
`/tags`,
);
for (const tag of tags) {
returnData.push({
name: tag.name as string,
value: tag.tagId,
});
}
return returnData;
},
// Get all the custom fields to display them to user so that he can
// select them easily
async getCustomFields(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const customFields = await getresponseApiRequest.call(
this,
'GET',
`/custom-fields`,
);
for (const customField of customFields) {
returnData.push({
name: customField.name as string,
value: customField.customFieldId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'contact') {
//https://apireference.getresponse.com/#operation/createContact
if (operation === 'create') {
const email = this.getNodeParameter('email', i) as string;
const campaignId = this.getNodeParameter('campaignId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
email,
campaign: {
campaignId,
},
};
Object.assign(body, additionalFields);
if (additionalFields.customFieldsUi) {
const customFieldValues = (additionalFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[];
if (customFieldValues) {
body.customFieldValues = customFieldValues;
for (let i = 0; i < customFieldValues.length; i++) {
if (!Array.isArray(customFieldValues[i].value)) {
customFieldValues[i].value = [customFieldValues[i].value];
}
}
delete body.customFieldsUi;
}
}
responseData = await getresponseApiRequest.call(this, 'POST', '/contacts', body);
responseData = { success: true };
}
//https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/deleteContact
if (operation === 'delete') {
const contactId = this.getNodeParameter('contactId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
Object.assign(qs, options);
responseData = await getresponseApiRequest.call(this, 'DELETE', `/contacts/${contactId}`, {}, qs);
responseData = { success: true };
}
//https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactById
if (operation === 'get') {
const contactId = this.getNodeParameter('contactId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
Object.assign(qs, options);
responseData = await getresponseApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs);
}
//https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactList
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const timezone = this.getTimezone();
Object.assign(qs, options);
const isNotQuery = [
'sortBy',
'sortOrder',
'additionalFlags',
'fields',
'exactMatch',
];
const isDate = [
'createdOnFrom',
'createdOnTo',
'changeOnFrom',
'changeOnTo',
];
const dateMapToKey: { [key: string]: string; } = {
'createdOnFrom': '[createdOn][from]',
'createdOnTo': '[createdOn][to]',
'changeOnFrom': '[changeOn][from]',
'changeOnTo': '[changeOn][to]',
};
for (const key of Object.keys(qs)) {
if (!isNotQuery.includes(key)) {
if (isDate.includes(key)) {
qs[`query${dateMapToKey[key]}`] = moment.tz(qs[key], timezone).format('YYYY-MM-DDTHH:mm:ssZZ');
} else {
qs[`query[${key}]`] = qs[key];
}
delete qs[key];
}
}
if (qs.sortBy) {
qs[`sort[${qs.sortBy}]`] = qs.sortOrder || 'ASC';
}
if (qs.exactMatch === true) {
qs['additionalFlags'] = 'exactMatch';
delete qs.exactMatch;
}
if (returnAll) {
responseData = await getResponseApiRequestAllItems.call(this, 'GET', `/contacts`, {}, qs);
} else {
qs.perPage = this.getNodeParameter('limit', i) as number;
responseData = await getresponseApiRequest.call(this, 'GET', `/contacts`, {}, qs);
}
}
//https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/updateContact
if (operation === 'update') {
const contactId = this.getNodeParameter('contactId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {};
Object.assign(body, updateFields);
if (updateFields.customFieldsUi) {
const customFieldValues = (updateFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[];
if (customFieldValues) {
body.customFieldValues = customFieldValues;
delete body.customFieldsUi;
}
}
responseData = await getresponseApiRequest.call(this, 'POST', `/contacts/${contactId}`, body);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

View file

@ -0,0 +1,68 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function gotifyApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('gotifyApi') as IDataObject;
const options: OptionsWithUri = {
method,
headers: {
'X-Gotify-Key': (method === 'POST') ? credentials.appApiToken : credentials.clientApiToken,
accept: 'application/json',
},
body,
qs,
uri: uri || `${credentials.url}${path}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.request.call(this, options);
} catch (error) {
if (error.response && error.response.body && error.response.body.errorDescription) {
const message = error.response.body.errorDescription;
// Try to return the error prettier
throw new Error(
`Gotify error response [${error.statusCode}]: ${message}`,
);
}
throw error;
}
}
export async function gotifyApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
query.limit = 100;
do {
responseData = await gotifyApiRequest.call(this, method, endpoint, body, query, uri);
if (responseData.paging.next) {
uri = responseData.paging.next;
}
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData.paging.next
);
return returnData;
}

View file

@ -0,0 +1,262 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
gotifyApiRequest,
gotifyApiRequestAllItems,
} from './GenericFunctions';
export class Gotify implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gotify',
name: 'gotify',
icon: 'file:gotify.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Gotify API.',
defaults: {
name: 'Gotify',
color: '#71c8ec',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'gotifyApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Message',
value: 'message',
},
],
default: 'message',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'message',
],
},
},
options: [
{
name: 'Create',
value: 'create',
},
{
name: 'Delete',
value: 'delete',
},
{
name: 'Get All',
value: 'getAll',
},
],
default: 'create',
description: 'The resource to operate on.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'create',
],
},
},
default: '',
description: `The message. Markdown (excluding html) is allowed.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'create',
],
},
},
default: {},
options: [
{
displayName: 'Priority',
name: 'priority',
type: 'number',
default: 1,
description: 'The priority of the message.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: `The title of the message.`,
},
],
},
{
displayName: 'Message ID',
name: 'messageId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'delete',
],
},
},
default: '',
description: `The message id.`,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 20,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'message') {
if (operation === 'create') {
const message = this.getNodeParameter('message', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
message,
};
Object.assign(body, additionalFields);
responseData = await gotifyApiRequest.call(
this,
'POST',
`/message`,
body,
);
}
if (operation === 'delete') {
const messageId = this.getNodeParameter('messageId', i) as string;
responseData = await gotifyApiRequest.call(
this,
'DELETE',
`/message/${messageId}`,
);
responseData = { success: true };
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await gotifyApiRequestAllItems.call(
this,
'messages',
'GET',
'/message',
{},
qs,
);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await gotifyApiRequest.call(
this,
'GET',
`/message`,
{},
qs,
);
responseData = responseData.messages;
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -100,39 +100,12 @@ export const dealFields = [
},
},
options: [
{
displayName: 'Deal Name',
name: 'dealName',
type: 'string',
default: '',
},
{
displayName: 'Pipeline',
name: 'pipeline',
type: 'string',
default: '',
},
{
displayName: 'Close Date',
name: 'closeDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Amount',
name: 'amount',
type: 'string',
default: '',
},
{
displayName: 'Deal Type',
name: 'dealType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDealTypes',
},
default: '',
},
{
displayName: 'Associated Company',
name: 'associatedCompany',
@ -151,6 +124,68 @@ export const dealFields = [
},
default: [],
},
{
displayName: 'Close Date',
name: 'closeDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Custom Properties',
name: 'customPropertiesUi',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'customPropertiesValues',
displayName: 'Custom Property',
values: [
{
displayName: 'Property',
name: 'property',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDealCustomProperties',
},
default: '',
description: 'Name of the property.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the property',
},
],
},
],
},
{
displayName: 'Deal Name',
name: 'dealName',
type: 'string',
default: '',
},
{
displayName: 'Deal Type',
name: 'dealType',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDealTypes',
},
default: '',
},
{
displayName: 'Pipeline',
name: 'pipeline',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
@ -191,6 +226,53 @@ export const dealFields = [
},
},
options: [
{
displayName: 'Amount',
name: 'amount',
type: 'string',
default: '',
},
{
displayName: 'Close Date',
name: 'closeDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Custom Properties',
name: 'customPropertiesUi',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'customPropertiesValues',
displayName: 'Custom Property',
values: [
{
displayName: 'Property',
name: 'property',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDealCustomProperties',
},
default: '',
description: 'Name of the property.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the property',
},
],
},
],
},
{
displayName: 'Deal Name',
name: 'dealName',
@ -208,24 +290,6 @@ export const dealFields = [
default: '',
description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.',
},
{
displayName: 'Pipeline',
name: 'pipeline',
type: 'string',
default: '',
},
{
displayName: 'Close Date',
name: 'closeDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Amount',
name: 'amount',
type: 'string',
default: '',
},
{
displayName: 'Deal Type',
name: 'dealType',
@ -235,6 +299,12 @@ export const dealFields = [
},
default: '',
},
{
displayName: 'Pipeline',
name: 'pipeline',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */

View file

@ -552,6 +552,25 @@ export class Hubspot implements INodeType {
}
return returnData;
},
// Get all the deal properties to display them to user so that he can
// select them easily
async getDealCustomProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/properties/v2/deals/properties';
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
for (const property of properties) {
if (property.hubspotDefined === null) {
const propertyName = property.label;
const propertyId = property.name;
returnData.push({
name: propertyName,
value: propertyId,
});
}
}
return returnData;
},
/* -------------------------------------------------------------------------- */
/* FORM */
@ -1801,6 +1820,17 @@ export class Hubspot implements INodeType {
value: additionalFields.pipeline as string,
});
}
if (additionalFields.customPropertiesUi) {
const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[];
if (customProperties) {
for (const customProperty of customProperties) {
body.properties.push({
name: customProperty.property,
value: customProperty.value,
});
}
}
}
body.associations = association;
const endpoint = '/deals/v1/deal';
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body);
@ -1846,6 +1876,17 @@ export class Hubspot implements INodeType {
value: updateFields.pipeline as string,
});
}
if (updateFields.customPropertiesUi) {
const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[];
if (customProperties) {
for (const customProperty of customProperties) {
body.properties.push({
name: customProperty.property,
value: customProperty.value,
});
}
}
}
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body);
}

View file

@ -73,7 +73,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut
}
}
export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];

View file

@ -112,12 +112,16 @@ export class Jira implements INodeType {
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
let endpoint = '';
let projects;
let endpoint = '/api/2/project/search';
if (jiraVersion === 'server') {
endpoint = '/api/2/project';
projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');
} else {
endpoint = '/api/2/project/search';
projects = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values', endpoint, 'GET');
}
let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');
if (projects.values && Array.isArray(projects.values)) {
projects = projects.values;
@ -130,6 +134,13 @@ export class Jira implements INodeType {
value: projectId,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -165,6 +176,12 @@ export class Jira implements INodeType {
}
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -184,6 +201,13 @@ export class Jira implements INodeType {
value: labelId,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -203,6 +227,13 @@ export class Jira implements INodeType {
value: priorityId,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -241,6 +272,12 @@ export class Jira implements INodeType {
}
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -260,6 +297,13 @@ export class Jira implements INodeType {
value: groupId,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
@ -277,6 +321,13 @@ export class Jira implements INodeType {
value: transition.id,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
},

View file

@ -0,0 +1,50 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function lineApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || ``,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'lineNotifyOAuth2Api', options, { tokenType: 'Bearer' });
} catch (error) {
let errorMessage;
if (error.response && error.response.body && error.response.body.message) {
errorMessage = error.response.body.message;
throw new Error(`Line error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}

View file

@ -0,0 +1,144 @@
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
lineApiRequest,
} from './GenericFunctions';
import {
notificationFields,
notificationOperations,
} from './NotificationDescription';
export class Line implements INodeType {
description: INodeTypeDescription = {
displayName: 'Line',
name: 'line',
icon: 'file:line.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Line API.',
defaults: {
name: 'Line',
color: '#00b900',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'lineNotifyOAuth2Api',
required: true,
displayOptions: {
show: {
resource: [
'notification',
],
},
},
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Notification',
value: 'notification',
},
],
default: 'notification',
description: 'The resource to operate on.',
},
...notificationOperations,
...notificationFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'notification') {
//https://notify-bot.line.me/doc/en/
if (operation === 'send') {
const message = this.getNodeParameter('message', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
message,
};
Object.assign(body, additionalFields);
if (body.hasOwnProperty('notificationDisabled')) {
body.notificationDisabled = (body.notificationDisabled) ? 'true' : 'false';
}
if (body.stickerUi) {
const sticker = (body.stickerUi as IDataObject).stickerValue as IDataObject;
if (sticker) {
body.stickerId = sticker.stickerId;
body.stickerPackageId = sticker.stickerPackageId;
}
delete body.stickerUi;
}
if (body.imageUi) {
const image = (body.imageUi as IDataObject).imageValue as IDataObject;
if (image && image.binaryData === true) {
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[image.binaryProperty] === undefined) {
throw new Error(`No binary data property "${image.binaryProperty}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[image.binaryProperty as string];
body.imageFile = {
value: Buffer.from(binaryData.data, BINARY_ENCODING),
options: {
filename: binaryData.fileName,
},
};
} else {
body.imageFullsize = image.imageFullsize;
body.imageThumbnail = image.imageThumbnail;
}
delete body.imageUi;
}
responseData = await lineApiRequest.call(this, 'POST', '', {}, {}, 'https://notify-api.line.me/api/notify', { formData: body });
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,176 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const notificationOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'notification',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Sends notifications to users or groups',
},
],
default: 'send',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const notificationFields = [
/* -------------------------------------------------------------------------- */
/* notification:send */
/* -------------------------------------------------------------------------- */
{
displayName: 'Message',
name: 'message',
required: true,
type: 'string',
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'notification',
],
},
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'notification',
],
},
},
options: [
{
displayName: 'Image',
name: 'imageUi',
placeholder: 'Add Image',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'imageValue',
displayName: 'image',
values: [
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
default: false,
},
{
displayName: 'Image Full Size',
name: 'imageFullsize',
type: 'string',
default: '',
displayOptions: {
show: {
binaryData: [
false,
],
},
},
description: 'HTTP/HTTPS URL. Maximum size of 2048×2048px JPEG',
},
{
displayName: 'Image Thumbnail',
name: 'imageThumbnail',
type: 'string',
displayOptions: {
show: {
binaryData: [
false,
],
},
},
default: '',
description: 'HTTP/HTTPS URL. Maximum size of 240×240px JPEG',
},
{
displayName: 'Binary Property',
name: 'binaryProperty',
type: 'string',
displayOptions: {
show: {
binaryData: [
true,
],
},
},
default: 'data',
description: `Name of the property that holds the binary data.<br>`,
},
],
},
],
},
{
displayName: 'Notification Disabled',
name: 'notificationDisabled',
type: 'boolean',
default: false,
description: `true: The user doesn't receive a push notification when the message is sent.<br>
false: The user receives a push notification when the message is sent`,
},
{
displayName: 'Sticker',
name: 'stickerUi',
placeholder: 'Add Sticker',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'stickerValue',
displayName: 'Sticker',
values: [
{
displayName: 'Sticker ID',
name: 'stickerId',
type: 'number',
default: '',
description: 'Sticker ID',
},
{
displayName: 'Sticker Package ID',
name: 'stickerPackageId',
type: 'number',
default: '',
description: 'Package ID',
},
],
},
],
},
],
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -217,6 +217,9 @@ export class MicrosoftSql implements INodeType {
user: credentials.user as string,
password: credentials.password as string,
domain: credentials.domain ? (credentials.domain as string) : undefined,
options: {
encrypt: credentials.tls as boolean,
},
};
const pool = new mssql.ConnectionPool(config);

View file

@ -54,13 +54,31 @@ export class PipedriveTrigger implements INodeType {
{
name: 'pipedriveApi',
required: true,
displayOptions: {
show: {
authentication: [
'apiToken',
],
},
},
},
{
name: 'pipedriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
authentication: [
incomingAuthentication: [
'basicAuth',
],
},
@ -80,6 +98,23 @@ export class PipedriveTrigger implements INodeType {
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Token',
value: 'apiToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiToken',
description: 'Method of authentication.',
},
{
displayName: 'Incoming Authentication',
name: 'incomingAuthentication',
type: 'options',
options: [
{
name: 'Basic Auth',
@ -91,7 +126,7 @@ export class PipedriveTrigger implements INodeType {
},
],
default: 'none',
description: 'If authentication should be activated for the webhook (makes it more scure).',
description: 'If authentication should be activated for the webhook (makes it more secure).',
},
{
displayName: 'Action',
@ -218,7 +253,7 @@ export class PipedriveTrigger implements INodeType {
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const authentication = this.getNodeParameter('authentication', 0) as string;
const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string;
const eventAction = this.getNodeParameter('action') as string;
const eventObject = this.getNodeParameter('object') as string;
@ -232,7 +267,7 @@ export class PipedriveTrigger implements INodeType {
http_auth_password: undefined as string | undefined,
};
if (authentication === 'basicAuth') {
if (incomingAuthentication === 'basicAuth') {
const httpBasicAuth = this.getCredentials('httpBasicAuth');
if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) {
@ -285,9 +320,9 @@ export class PipedriveTrigger implements INodeType {
const resp = this.getResponseObject();
const realm = 'Webhook';
const authentication = this.getNodeParameter('authentication', 0) as string;
const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string;
if (authentication === 'basicAuth') {
if (incomingAuthentication === 'basicAuth') {
// Basic authorization is needed to call webhook
const httpBasicAuth = this.getCredentials('httpBasicAuth');

View file

@ -262,6 +262,38 @@ export const identifyFields = [
},
],
},
{
displayName: 'Custom Traits',
name: 'customTraitsUi',
placeholder: 'Add Custom Trait',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'customTraitValues',
displayName: 'Custom Traits',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: '',
},
],
},
],
},
],
},
],

View file

@ -38,6 +38,7 @@ import {
} from './TrackInterface';
import * as uuid from 'uuid/v4';
import { customerFields } from '../CustomerIo/CustomerDescription';
export class Segment implements INodeType {
description: INodeTypeDescription = {
@ -170,6 +171,7 @@ export class Segment implements INodeType {
if (traits.id) {
body.traits!.id = traits.id as string;
}
if (traits.company) {
const company = (traits.company as IDataObject).companyUi as IDataObject;
if (company) {
@ -384,6 +386,14 @@ export class Segment implements INodeType {
if (traits.id) {
body.traits!.id = traits.id as string;
}
if (traits.customTraitsUi) {
const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[];
if (customTraits && customTraits.length !== 0) {
for (const customTrait of customTraits) {
body.traits![customTrait.key as string] = customTrait.value;
}
}
}
if (traits.company) {
const company = (traits.company as IDataObject).companyUi as IDataObject;
if (company) {
@ -531,6 +541,17 @@ export class Segment implements INodeType {
body.integrations!.salesforce = integrations.salesforce as boolean;
}
}
if (Object.keys(traits.company as IDataObject).length === 0) {
//@ts-ignore
delete body.traits.company;
}
if (Object.keys(traits.address as IDataObject).length === 0) {
//@ts-ignore
delete body.traits.address;
}
responseData = await segmentApiRequest.call(this, 'POST', '/identify', body);
}
}
@ -602,6 +623,14 @@ export class Segment implements INodeType {
if (traits.id) {
body.traits!.id = traits.id as string;
}
if (traits.customTraitsUi) {
const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[];
if (customTraits && customTraits.length !== 0) {
for (const customTrait of customTraits) {
body.traits![customTrait.key as string] = customTrait.value;
}
}
}
if (traits.company) {
const company = (traits.company as IDataObject).companyUi as IDataObject;
if (company) {
@ -760,6 +789,17 @@ export class Segment implements INodeType {
body.properties!.value = properties.value as string;
}
}
if (Object.keys(traits.company as IDataObject).length === 0) {
//@ts-ignore
delete body.traits.company;
}
if (Object.keys(traits.address as IDataObject).length === 0) {
//@ts-ignore
delete body.traits.address;
}
responseData = await segmentApiRequest.call(this, 'POST', '/track', body);
}
//https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#page

View file

@ -285,6 +285,38 @@ export const trackFields = [
},
],
},
{
displayName: 'Custom Traits',
name: 'customTraitsUi',
placeholder: 'Add Custom Trait',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'customTraitValues',
displayName: 'Custom Traits',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: '',
},
],
},
],
},
],
},
],

View file

@ -24,7 +24,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions
throw new Error('No credentials got returned!');
}
const headerWithAuthentication = Object.assign({},
{ Authorization: ` Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` });
{ Authorization: `Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` });
const options: OptionsWithUri = {
headers: headerWithAuthentication,
@ -47,6 +47,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions
try {
return await this.helpers.request!(options);
} catch (error) {
console.log(error.response.body);
if (error.response.body && error.response.body.errors) {
let message = '';
if (typeof error.response.body.errors === 'object') {

View file

@ -25,6 +25,7 @@ export class ShopifyTrigger implements INodeType {
icon: 'file:shopify.png',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["event"]}}',
description: 'Handle Shopify events via webhooks',
defaults: {
name: 'Shopify Trigger',
@ -55,271 +56,268 @@ export class ShopifyTrigger implements INodeType {
options:
[
{
name: 'App uninstalled',
name: 'App Uninstalled',
value: 'app/uninstalled',
},
{
name: 'Carts create',
name: 'Cart Created',
value: 'carts/create',
},
{
name: 'Carts update',
name: 'Cart Updated',
value: 'carts/update',
},
{
name: 'Checkouts create',
name: 'Checkout Created',
value: 'checkouts/create',
},
{
name: 'Checkouts delete',
name: 'Checkout Delete',
value: 'checkouts/delete',
},
{
name: 'Checkouts update',
name: 'Checkout Update',
value: 'checkouts/update',
},
{
name: 'Collection listings add',
name: 'Collection Listings Added',
value: 'collection_listings/add',
},
{
name: 'Collection listings remove',
name: 'Collection Listings Removed',
value: 'collection_listings/remove',
},
{
name: 'Collection listings update',
name: 'Collection Listings Updated',
value: 'collection_listings/update',
},
{
name: 'Collections create',
name: 'Collection Created',
value: 'collections/create',
},
{
name: 'Collections delete',
name: 'Collection Deleted',
value: 'collections/delete',
},
{
name: 'Collections update',
name: 'Collection Updated',
value: 'collections/update',
},
{
name: 'Customer groups create',
name: 'Customer Groups Created',
value: 'customer_groups/create',
},
{
name: 'Customer groups delete',
name: 'Customer Groups Deleted',
value: 'customer_groups/delete',
},
{
name: 'Customer groups update',
name: 'Customer Groups Updated',
value: 'customer_groups/update',
},
{
name: 'Customers create',
name: 'Customer Created',
value: 'customers/create',
},
{
name: 'Customers delete',
name: 'Customer Deleted',
value: 'customers/delete',
},
{
name: 'Customers disable',
name: 'Customer disabled',
value: 'customers/disable',
},
{
name: 'Customers enable',
name: 'Customer Enabled',
value: 'customers/enable',
},
{
name: 'Customers update',
name: 'Customer Updated',
value: 'customers/update',
},
{
name: 'Draft orders create',
name: 'Draft Orders Created',
value: 'draft_orders/create',
},
{
name: 'Draft orders delete',
name: 'Draft Orders Deleted',
value: 'draft_orders/delete',
},
{
name: 'Draft orders update',
name: 'Draft orders Updated',
value: 'draft_orders/update',
},
{
name: 'Fulfillment events create',
name: 'Fulfillment Events Created',
value: 'fulfillment_events/create',
},
{
name: 'Fulfillment events delete',
name: 'Fulfillment Events Deleted',
value: 'fulfillment_events/delete',
},
{
name: 'Fulfillments create',
name: 'Fulfillment created',
value: 'fulfillments/create',
},
{
name: 'Fulfillments update',
name: 'Fulfillment Updated',
value: 'fulfillments/update',
},
{
name: 'Inventory_items create',
name: 'Inventory Items Created',
value: 'inventory_items/create',
},
{
name: 'Inventory_items delete',
name: 'Inventory Items Deleted',
value: 'inventory_items/delete',
},
{
name: 'Inventory_items update',
name: 'Inventory Items Updated',
value: 'inventory_items/update',
},
{
name: 'Inventory_levels connect',
name: 'Inventory Levels Connected',
value: 'inventory_levels/connect',
},
{
name: 'Inventory_levels disconnect',
name: 'Inventory Levels Disconnected',
value: 'inventory_levels/disconnect',
},
{
name: 'Inventory_levels update',
name: 'Inventory Levels Updated',
value: 'inventory_levels/update',
},
{
name: 'Locales create',
name: 'Locale Created',
value: 'locales/create',
},
{
name: 'Locales update',
name: 'Locale Updated',
value: 'locales/update',
},
{
name: 'Locations create',
name: 'Location Created',
value: 'locations/create',
},
{
name: 'Locations delete',
name: 'Location Deleted',
value: 'locations/delete',
},
{
name: 'Locations update',
name: 'Location Updated',
value: 'locations/update',
},
{
name: 'Order transactions create',
name: 'Order transactions Created',
value: 'order_transactions/create',
},
{
name: 'Orders cancelled',
name: 'Order cancelled',
value: 'orders/cancelled',
},
{
name: 'Orders create',
name: 'Order Created',
value: 'orders/create',
},
{
name: 'Orders delete',
name: 'Orders Deleted',
value: 'orders/delete',
},
{
name: 'Orders fulfilled',
name: 'Order Fulfilled',
value: 'orders/fulfilled',
},
{
name: 'Orders paid',
name: 'Order Paid',
value: 'orders/paid',
},
{
name: 'Orders partially fulfilled',
name: 'Order Partially Fulfilled',
value: 'orders/partially_fulfilled',
},
{
name: 'Orders updated',
name: 'Order Updated',
value: 'orders/updated',
},
{
name: 'Product listings add',
name: 'Product Listings Added',
value: 'product_listings/add',
},
{
name: 'Product listings remove',
name: 'Product Listings Removed',
value: 'product_listings/remove',
},
{
name: 'Product listings update',
name: 'Product Listings Updated',
value: 'product_listings/update',
},
{
name: 'Products create',
name: 'Product Created',
value: 'products/create',
},
{
name: 'Products delete',
name: 'Product Deleted',
value: 'products/delete',
},
{
name: 'Products update',
name: 'Product Updated',
value: 'products/update',
},
{
name: 'Refunds create',
name: 'Refund Created',
value: 'refunds/create',
},
{
name: 'Shop update',
name: 'Shop Updated',
value: 'shop/update',
},
{
name: 'Tender transactions create',
name: 'Tender Transactions Created',
value: 'tender_transactions/create',
},
{
name: 'Themes create',
name: 'Theme Created',
value: 'themes/create',
},
{
name: 'Themes delete',
name: 'Theme Deleted',
value: 'themes/delete',
},
{
name: 'Themes publish',
name: 'Theme Published',
value: 'themes/publish',
},
{
name: 'Themes update',
name: 'Theme Updated',
value: 'themes/update',
},
],
description: 'Event that triggers the webhook',
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const topic = this.getNodeParameter('topic') as string;
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
return false;
}
const endpoint = `/webhooks/${webhookData.webhookId}.json`;
try {
await shopifyApiRequest.call(this, 'GET', endpoint, {});
} catch (e) {
if (e.statusCode === 404) {
delete webhookData.webhookId;
return false;
const webhookUrl = this.getNodeWebhookUrl('default');
const endpoint = `/webhooks`;
const { webhooks } = await shopifyApiRequest.call(this, 'GET', endpoint, {}, { topic });
for (const webhook of webhooks) {
if (webhook.address === webhookUrl) {
webhookData.webhookId = webhook.id;
return true;
}
throw e;
}
return true;
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const credentials = this.getCredentials('shopifyApi');
const webhookUrl = this.getNodeWebhookUrl('default');
const topic = this.getNodeParameter('topic') as string;
const webhookData = this.getWorkflowStaticData('node');
const endpoint = `/webhooks.json`;
const body = {
webhook: {
@ -330,21 +328,15 @@ export class ShopifyTrigger implements INodeType {
};
let responseData;
try {
responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body);
} catch (error) {
return false;
}
responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body);
if (responseData.webhook === undefined || responseData.webhook.id === undefined) {
// Required data is missing so was not successful
return false;
}
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = responseData.webhook.id as string;
webhookData.sharedSecret = credentials!.sharedSecret as string;
webhookData.topic = topic as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
@ -357,8 +349,6 @@ export class ShopifyTrigger implements INodeType {
return false;
}
delete webhookData.webhookId;
delete webhookData.sharedSecret;
delete webhookData.topic;
}
return true;
},
@ -368,17 +358,18 @@ export class ShopifyTrigger implements INodeType {
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const headerData = this.getHeaderData() as IDataObject;
const req = this.getRequestObject();
const webhookData = this.getWorkflowStaticData('node') as IDataObject;
const credentials = this.getCredentials('shopifyApi') as IDataObject;
const topic = this.getNodeParameter('topic') as string;
if (headerData['x-shopify-topic'] !== undefined
&& headerData['x-shopify-hmac-sha256'] !== undefined
&& headerData['x-shopify-shop-domain'] !== undefined
&& headerData['x-shopify-api-version'] !== undefined) {
// @ts-ignore
const computedSignature = createHmac('sha256', webhookData.sharedSecret as string).update(req.rawBody).digest('base64');
const computedSignature = createHmac('sha256', credentials.sharedSecret as string).update(req.rawBody).digest('base64');
if (headerData['x-shopify-hmac-sha256'] !== computedSignature) {
return {};
}
if (webhookData.topic !== headerData['x-shopify-topic']) {
if (topic !== headerData['x-shopify-topic']) {
return {};
}
} else {

View file

@ -0,0 +1,350 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const entryOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'entry',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create an entry',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an entry',
},
{
name: 'Get',
value: 'get',
description: 'Get an entry',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all entries',
},
{
name: 'Update',
value: 'update',
description: 'Update an entry',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const entryFields = [
/* -------------------------------------------------------------------------- */
/* entry:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Content Type',
name: 'contentType',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'create',
],
},
},
description: 'Name of the content type.',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'create',
],
},
},
default: '',
placeholder: 'id,name,description',
description: 'Comma separated list of the properties which should used as columns for the new rows.',
},
/* -------------------------------------------------------------------------- */
/* entry:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Content Type',
name: 'contentType',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'delete',
],
},
},
description: 'Name of the content type.',
},
{
displayName: 'Entry ID',
name: 'entryId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'delete',
],
},
},
description: 'The ID of the entry to delete.',
},
/* -------------------------------------------------------------------------- */
/* entry:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Content Type',
name: 'contentType',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'get',
],
},
},
description: 'Name of the content type.',
},
{
displayName: 'Entry ID',
name: 'entryId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'get',
],
},
},
description: 'The ID of the entry to get.',
},
/* -------------------------------------------------------------------------- */
/* entry:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Content Type',
name: 'contentType',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'getAll',
],
},
},
description: 'Name of the content type',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'Returns a list of your user contacts.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Publication State',
name: 'publicationState',
type: 'options',
options: [
{
name: 'Live',
value: 'live',
},
{
name: 'Preview',
value: 'preview',
},
],
default: '',
description: 'Only select entries matching the publication state provided.',
},
{
displayName: 'Sort Fields',
name: 'sort',
type: 'string',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Sort Field',
},
default: '',
placeholder: 'name:asc',
description: `Name of the fields to sort the data by. By default will be sorted ascendingly.<br>
To modify that behavior, you have to add the sort direction after the name of sort field preceded by a colon.
For example: name:asc`,
},
{
displayName: 'Where (JSON)',
name: 'where',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'JSON query to filter the data.<a href="https://strapi.io/documentation/v3.x/content-api/parameters.html#filters" target="_blank"> Info</a>',
},
],
},
/* -------------------------------------------------------------------------- */
/* entry:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Content Type',
name: 'contentType',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'update',
],
},
},
description: 'Name of the content type.',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'update',
],
},
},
default: 'id',
required: true,
description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
resource: [
'entry',
],
operation: [
'update',
],
},
},
default: '',
placeholder: 'id,name,description',
description: 'Comma separated list of the properties which should used as columns for the new rows.',
},
] as INodeProperties[];

View file

@ -0,0 +1,103 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('strapiApi') as IDataObject;
try {
const options: OptionsWithUri = {
headers: {},
method,
body,
qs,
uri: uri || `${credentials.url}${resource}`,
json: true,
};
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers?.request(options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
let messages = error.response.body.message;
if (Array.isArray(error.response.body.message)) {
messages = messages[0].messages.map((e: IDataObject) => e.message).join('|');
}
// Try to return the error prettier
throw new Error(
`Strapi error response [${error.statusCode}]: ${messages}`,
);
}
throw error;
}
}
export async function getToken(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('strapiApi') as IDataObject;
const options: OptionsWithUri = {
headers: {
'content-type': `application/json`,
},
method: 'POST',
uri: `${credentials.url}/auth/local`,
body: {
identifier: credentials.email,
password: credentials.password,
},
json: true,
};
return this.helpers.request!(options);
}
export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query._limit = 20;
query._start = 0;
do {
responseData = await strapiApiRequest.call(this, method, resource, body, query, undefined, headers);
query._start += query._limit;
returnData.push.apply(returnData, responseData);
} while (
responseData.length !== 0
);
return returnData;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}

View file

@ -0,0 +1,192 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
getToken,
strapiApiRequest,
strapiApiRequestAllItems,
validateJSON,
} from './GenericFunctions';
import {
entryFields,
entryOperations,
} from './EntryDescription';
export class Strapi implements INodeType {
description: INodeTypeDescription = {
displayName: 'Strapi',
name: 'strapi',
icon: 'file:strapi.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Strapi API.',
defaults: {
name: 'Strapi',
color: '#725ed8',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'strapiApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Entry',
value: 'entry',
},
],
default: 'entry',
description: 'The resource to operate on.',
},
...entryOperations,
...entryFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
const headers: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
const { jwt } = await getToken.call(this);
headers.Authorization = `Bearer ${jwt}`;
if (resource === 'entry') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const body: IDataObject = {};
const contentType = this.getNodeParameter('contentType', i) as string;
const columns = this.getNodeParameter('columns', i) as string;
const columnList = columns.split(',').map(column => column.trim());
for (const key of Object.keys(items[i].json)) {
if (columnList.includes(key)) {
body[key] = items[i].json[key];
}
}
responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs, undefined, headers);
returnData.push(responseData);
}
}
if (operation === 'delete') {
for (let i = 0; i < length; i++) {
const contentType = this.getNodeParameter('contentType', i) as string;
const entryId = this.getNodeParameter('entryId', i) as string;
responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs, undefined, headers);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const contentType = this.getNodeParameter('contentType', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.sort && (options.sort as string[]).length !== 0) {
const sortFields = options.sort as string[];
qs._sort = sortFields.join(',');
}
if (options.where) {
const query = validateJSON(options.where as string);
if (query !== undefined) {
qs._where = query;
} else {
throw new Error('Query must be a valid JSON');
}
}
if (options.publicationState) {
qs._publicationState = options.publicationState as string;
}
if (returnAll) {
responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs, headers);
} else {
qs._limit = this.getNodeParameter('limit', i) as number;
responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs, undefined, headers);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const contentType = this.getNodeParameter('contentType', i) as string;
const entryId = this.getNodeParameter('entryId', i) as string;
responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs, undefined, headers);
returnData.push(responseData);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const body: IDataObject = {};
const contentType = this.getNodeParameter('contentType', i) as string;
const columns = this.getNodeParameter('columns', i) as string;
const updateKey = this.getNodeParameter('updateKey', i) as string;
const columnList = columns.split(',').map(column => column.trim());
const entryId = items[i].json[updateKey];
for (const key of Object.keys(items[i].json)) {
if (columnList.includes(key)) {
body[key] = items[i].json[key];
}
}
responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs, undefined, headers);
returnData.push(responseData);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,72 @@
<svg width="1025" height="1032" viewBox="0 0 1025 1032" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M341.328 0V344.061H682.661V688.121H1023.99V0H341.328Z" fill="url(#paint0_linear)"/>
<path d="M683 343.725H343V688.457H683V343.725Z" fill="url(#paint1_linear)"/>
<path d="M341.333 344.061H0L341.333 0V344.061Z" fill="url(#paint2_linear)"/>
<path d="M682.367 1031.18V687.457H1024.37L682.367 1031.18Z" fill="url(#paint3_linear)"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 365.162 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 374.502 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 383.843 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 393.184 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 402.525 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 411.866 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 421.206 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 430.546 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 439.887 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 449.228 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 458.569 314.663)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 365.162 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 374.502 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 383.843 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 393.184 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 402.525 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 411.866 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 421.206 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 430.546 306.041)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 365.162 297.419)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 374.502 297.419)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 383.843 297.419)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 393.184 297.419)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 402.525 297.419)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 365.162 288.796)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 374.502 288.797)" fill="#956FFF"/>
<circle r="1.7963" transform="matrix(1 8.74228e-08 8.74228e-08 -1 383.843 288.797)" fill="#956FFF"/>
<circle cx="716.162" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="725.502" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="734.843" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="744.184" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="753.525" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="762.866" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="772.206" cy="638.796" r="1.7963" fill="#956FFF"/>
<circle cx="716.162" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="725.502" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="734.843" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="744.184" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="753.525" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="762.866" cy="647.419" r="1.7963" fill="#956FFF"/>
<circle cx="716.162" cy="656.041" r="1.7963" fill="#956FFF"/>
<circle cx="725.502" cy="656.04" r="1.7963" fill="#956FFF"/>
<circle cx="734.843" cy="656.04" r="1.7963" fill="#956FFF"/>
<circle cx="744.184" cy="656.04" r="1.7963" fill="#956FFF"/>
<circle cx="753.525" cy="656.04" r="1.7963" fill="#956FFF"/>
<circle cx="716.162" cy="664.664" r="1.7963" fill="#956FFF"/>
<circle cx="725.502" cy="664.663" r="1.7963" fill="#956FFF"/>
<circle cx="734.843" cy="664.663" r="1.7963" fill="#956FFF"/>
<defs>
<linearGradient id="paint0_linear" x1="1072.36" y1="-51.4075" x2="553.031" y2="446.945" gradientUnits="userSpaceOnUse">
<stop offset="0.0208333" stop-color="#956FFF"/>
<stop offset="1" stop-color="#1C1B7E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="634.365" y1="396.5" x2="277.177" y2="712.901" gradientUnits="userSpaceOnUse">
<stop stop-color="#956FFF"/>
<stop offset="1" stop-color="#1A2670"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="170.667" y1="0" x2="170.667" y2="344.061" gradientUnits="userSpaceOnUse">
<stop offset="0.0208333" stop-color="#956FFF"/>
<stop offset="1" stop-color="#1C1B7E"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="1085.18" y1="556.22" x2="695.654" y2="967.042" gradientUnits="userSpaceOnUse">
<stop stop-color="#956FFF"/>
<stop offset="0.838542" stop-color="#1C1B7E"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.87.0",
"version": "0.88.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -71,6 +71,8 @@
"dist/credentials/FileMaker.credentials.js",
"dist/credentials/FlowApi.credentials.js",
"dist/credentials/Ftp.credentials.js",
"dist/credentials/GetResponseApi.credentials.js",
"dist/credentials/GetResponseOAuth2Api.credentials.js",
"dist/credentials/GithubApi.credentials.js",
"dist/credentials/GithubOAuth2Api.credentials.js",
"dist/credentials/GitlabApi.credentials.js",
@ -86,6 +88,7 @@
"dist/credentials/GSuiteAdminOAuth2Api.credentials.js",
"dist/credentials/GoogleTasksOAuth2Api.credentials.js",
"dist/credentials/GoogleTranslateOAuth2Api.credentials.js",
"dist/credentials/GotifyApi.credentials.js",
"dist/credentials/YouTubeOAuth2Api.credentials.js",
"dist/credentials/GumroadApi.credentials.js",
"dist/credentials/HarvestApi.credentials.js",
@ -105,6 +108,7 @@
"dist/credentials/JotFormApi.credentials.js",
"dist/credentials/Kafka.credentials.js",
"dist/credentials/KeapOAuth2Api.credentials.js",
"dist/credentials/LineNotifyOAuth2Api.credentials.js",
"dist/credentials/LinkedInOAuth2Api.credentials.js",
"dist/credentials/MailerLiteApi.credentials.js",
"dist/credentials/MailchimpApi.credentials.js",
@ -176,6 +180,7 @@
"dist/credentials/SpotifyOAuth2Api.credentials.js",
"dist/credentials/StoryblokContentApi.credentials.js",
"dist/credentials/StoryblokManagementApi.credentials.js",
"dist/credentials/StrapiApi.credentials.js",
"dist/credentials/SurveyMonkeyApi.credentials.js",
"dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
"dist/credentials/TaigaCloudApi.credentials.js",
@ -277,6 +282,7 @@
"dist/nodes/Flow/FlowTrigger.node.js",
"dist/nodes/Function.node.js",
"dist/nodes/FunctionItem.node.js",
"dist/nodes/GetResponse/GetResponse.node.js",
"dist/nodes/Github/Github.node.js",
"dist/nodes/Github/GithubTrigger.node.js",
"dist/nodes/Gitlab/Gitlab.node.js",
@ -291,6 +297,7 @@
"dist/nodes/Google/Task/GoogleTasks.node.js",
"dist/nodes/Google/Translate/GoogleTranslate.node.js",
"dist/nodes/Google/YouTube/YouTube.node.js",
"dist/nodes/Gotify/Gotify.node.js",
"dist/nodes/GraphQL/GraphQL.node.js",
"dist/nodes/Gumroad/GumroadTrigger.node.js",
"dist/nodes/HackerNews/HackerNews.node.js",
@ -313,6 +320,7 @@
"dist/nodes/Kafka/Kafka.node.js",
"dist/nodes/Keap/Keap.node.js",
"dist/nodes/Keap/KeapTrigger.node.js",
"dist/nodes/Line/Line.node.js",
"dist/nodes/LinkedIn/LinkedIn.node.js",
"dist/nodes/MailerLite/MailerLite.node.js",
"dist/nodes/MailerLite/MailerLiteTrigger.node.js",
@ -381,6 +389,7 @@
"dist/nodes/SseTrigger.node.js",
"dist/nodes/Start.node.js",
"dist/nodes/Storyblok/Storyblok.node.js",
"dist/nodes/Strapi/Strapi.node.js",
"dist/nodes/Strava/Strava.node.js",
"dist/nodes/Strava/StravaTrigger.node.js",
"dist/nodes/Stripe/StripeTrigger.node.js",
@ -449,7 +458,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^26.4.2",
"n8n-workflow": "~0.43.0",
"n8n-workflow": "~0.44.0",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.9.7"
@ -479,7 +488,7 @@
"mqtt": "^4.2.0",
"mssql": "^6.2.0",
"mysql2": "~2.1.0",
"n8n-core": "~0.50.0",
"n8n-core": "~0.51.0",
"nodemailer": "^6.4.6",
"pdf-parse": "^1.1.1",
"pg": "^8.3.0",

View file

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

View file

@ -408,6 +408,7 @@ export interface INodePropertyTypeOptions {
numberStepSize?: number; // Supported by: number
password?: boolean; // Supported by: string
rows?: number; // Supported by: string
showAlpha?: boolean; // Supported by: color
[key: string]: boolean | number | string | EditorTypes | undefined | string[];
}
@ -535,6 +536,7 @@ export interface INodeTypeDescription {
version: number;
description: string;
defaults: INodeParameters;
documentationUrl?: string;
inputs: string[];
inputNames?: string[];
outputs: string[];
@ -714,7 +716,7 @@ export interface IWorkflowExecuteHooks {
nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData) => Promise<void>)>;
nodeExecuteBefore?: Array<((nodeName: string) => Promise<void>)>;
workflowExecuteAfter?: Array<((data: IRun, newStaticData: IDataObject) => Promise<void>)>;
workflowExecuteBefore?: Array<(() => Promise<void>)>;
workflowExecuteBefore?: Array<((workflow: Workflow, data: IRunExecutionData) => Promise<void>)>;
}
export interface IWorkflowExecuteAdditionalData {

View file

@ -28,19 +28,9 @@ export class WorkflowHooks {
async executeHookFunctions(hookName: string, parameters: any[]) { // tslint:disable-line:no-any
if (this.hookFunctions[hookName] !== undefined && Array.isArray(this.hookFunctions[hookName])) {
for (const hookFunction of this.hookFunctions[hookName]!) {
await hookFunction.apply(this, parameters)
.catch((error: Error) => {
// Catch all errors here because when "executeHook" gets called
// we have the most time no "await" and so the errors would so
// not be uncaught by anything.
// TODO: Add proper logging
console.error(`There was a problem executing hook: "${hookName}"`);
console.error('Parameters:');
console.error(parameters);
console.error('Error:');
console.error(error);
});
// TODO: As catch got removed we should make sure that we catch errors
// where hooks get called
await hookFunction.apply(this, parameters);
}
}
}