Merge 'master' into 'ConvertKit'

This commit is contained in:
ricardo
2020-08-05 20:17:00 -04:00
285 changed files with 17369 additions and 2488 deletions

View File

@@ -32,7 +32,25 @@ export class AcuitySchedulingTrigger implements INodeType {
{
name: 'acuitySchedulingApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'acuitySchedulingOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -43,6 +61,23 @@ export class AcuitySchedulingTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Key',
value: 'apiKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiKey',
description: 'Method of authentication.',
},
{
displayName: 'Event',
name: 'event',

View File

@@ -9,34 +9,39 @@ import {
import { IDataObject } from 'n8n-workflow';
export async function acuitySchedulingApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('acuitySchedulingApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
auth: {
user: credentials.userId as string,
password: credentials.apiKey as string,
},
auth: {},
method,
qs,
body,
uri: uri ||`https://acuityscheduling.com/api/v1${resource}`,
json: true
};
try {
return await this.helpers.request!(options);
} catch (error) {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('acuitySchedulingApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let errorMessage = error.message;
if (error.response.body && error.response.body.message) {
errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`;
options.auth = {
user: credentials.userId as string,
password: credentials.apiKey as string,
};
return await this.helpers.request!(options);
} else {
delete options.auth;
//@ts-ignore
return await this.helpers.requestOAuth2!.call(this, 'acuitySchedulingOAuth2Api', options, true);
}
throw new Error('Acuity Scheduling Error: ' + errorMessage);
} catch (error) {
throw new Error('Acuity Scheduling Error: ' + error.message);
}
}

View File

@@ -52,10 +52,10 @@ export class AffinityTrigger implements INodeType {
options: [
{
name: 'file.created',
value: 'file.deleted',
value: 'file.created',
},
{
name: 'file.created',
name: 'file.deleted',
value: 'file.deleted',
},
{
@@ -136,7 +136,7 @@ export class AffinityTrigger implements INodeType {
},
{
name: 'opportunity.deleted',
value: 'organization.deleted',
value: 'opportunity.deleted',
},
{
name: 'person.created',

View File

@@ -28,12 +28,12 @@ import { IDeal } from './DealInterface';
export class AgileCrm implements INodeType {
description: INodeTypeDescription = {
displayName: 'AgileCRM',
displayName: 'Agile CRM',
name: 'agileCrm',
icon: 'file:agilecrm.png',
group: ['transform'],
version: 1,
description: 'Consume AgileCRM API',
description: 'Consume Agile CRM API',
defaults: {
name: 'AgileCRM',
color: '#772244',

View File

@@ -44,12 +44,12 @@ export class Airtable implements INodeType {
{
name: 'Append',
value: 'append',
description: 'Appends the data to a table',
description: 'Append the data to a table',
},
{
name: 'Delete',
value: 'delete',
description: 'Deletes data from a table'
description: 'Delete data from a table'
},
{
name: 'List',
@@ -59,12 +59,12 @@ export class Airtable implements INodeType {
{
name: 'Read',
value: 'read',
description: 'Reads data from a table'
description: 'Read data from a table'
},
{
name: 'Update',
value: 'update',
description: 'Updates data in a table'
description: 'Update data in a table'
},
],
default: 'read',

View File

@@ -84,7 +84,7 @@ export class Asana implements INodeType {
{
name: 'Get',
value: 'get',
description: 'Get data of task',
description: 'Get data of a task',
},
{
name: 'Update',
@@ -369,12 +369,12 @@ export class Asana implements INodeType {
{
name: 'Get All',
value: 'getAll',
description: 'Data of all users',
description: 'Get data of all users',
},
{
name: 'Get',
value: 'get',
description: 'Get data of user',
description: 'Get data of a user',
},
],
default: 'get',

View File

@@ -19,8 +19,8 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
const endpoint = `${service}.${credentials.region}.amazonaws.com`;
// Sign AWS API request with the user credentials
const signOpts = {headers: headers || {}, host: endpoint, method, path, body};
sign(signOpts, {accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}`});
const signOpts = { headers: headers || {}, host: endpoint, method, path, body };
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}` });
const options: OptionsWithUri = {
headers: signOpts.headers,

View File

@@ -18,7 +18,7 @@ export const bucketOperations = [
{
name: 'Create',
value: 'create',
description: 'Create an bucket',
description: 'Create a bucket',
},
{
name: 'Get All',
@@ -28,7 +28,7 @@ export const bucketOperations = [
{
name: 'Search',
value: 'search',
description: 'Search withim a bucket',
description: 'Search within a bucket',
},
],
default: 'create',

View File

@@ -0,0 +1,361 @@
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
IBinaryKeyData,
} from 'n8n-workflow';
import {
boxApiRequest,
boxApiRequestAllItems,
} from './GenericFunctions';
import {
fileFields,
fileOperations,
} from './FileDescription';
import {
folderFields,
folderOperations,
} from './FolderDescription';
import * as moment from 'moment-timezone';
export class Box implements INodeType {
description: INodeTypeDescription = {
displayName: 'Box',
name: 'box',
icon: 'file:box.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Box API',
defaults: {
name: 'Box',
color: '#00aeef',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'boxOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'File',
value: 'file',
},
{
name: 'Folder',
value: 'folder',
},
],
default: 'file',
description: 'The resource to operate on.',
},
...fileOperations,
...fileFields,
...folderOperations,
...folderFields,
],
};
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 === 'file') {
// https://developer.box.com/reference/post-files-id-copy
if (operation === 'copy') {
const fileId = this.getNodeParameter('fileId', i) as string;
const parentId = this.getNodeParameter('parentId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {};
if (additionalFields.name) {
body.name = additionalFields.name as string;
}
if (parentId) {
body.parent = { id: parentId };
} else {
body.parent = { id: 0 };
}
if (additionalFields.fields) {
qs.fields = additionalFields.fields as string;
}
if (additionalFields.version) {
body.version = additionalFields.version as string;
}
responseData = await boxApiRequest.call(this, 'POST', `/files/${fileId}/copy`, body, qs);
returnData.push(responseData as IDataObject);
}
// https://developer.box.com/reference/delete-files-id
if (operation === 'delete') {
const fileId = this.getNodeParameter('fileId', i) as string;
responseData = await boxApiRequest.call(this, 'DELETE', `/files/${fileId}`);
responseData = { success: true };
returnData.push(responseData as IDataObject);
}
// https://developer.box.com/reference/get-files-id-content
if (operation === 'download') {
const fileId = this.getNodeParameter('fileId', i) as string;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}`);
const fileName = responseData.name;
let mimeType: string | undefined;
responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}/content`, {}, {}, undefined, { resolveWithFullResponse: true });
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (mimeType === undefined && responseData.headers['content-type']) {
mimeType = responseData.headers['content-type'];
}
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const data = Buffer.from(responseData.body);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType);
}
// https://developer.box.com/reference/get-files-id
if (operation === 'get') {
const fileId = this.getNodeParameter('fileId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs.fields = additionalFields.fields as string;
}
responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}`, {}, qs);
returnData.push(responseData as IDataObject);
}
// https://developer.box.com/reference/get-search/
if (operation === 'search') {
const query = this.getNodeParameter('query', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const timezone = this.getTimezone();
qs.type = 'file';
qs.query = query;
Object.assign(qs, additionalFields);
if (qs.content_types) {
qs.content_types = (qs.content_types as string).split(',');
}
if (additionalFields.createdRangeUi) {
const createdRangeValues = (additionalFields.createdRangeUi as IDataObject).createdRangeValuesUi as IDataObject;
if (createdRangeValues) {
qs.created_at_range = `${moment.tz(createdRangeValues.from, timezone).format()},${moment.tz(createdRangeValues.to, timezone).format()}`;
}
delete qs.createdRangeUi;
}
if (additionalFields.updatedRangeUi) {
const updateRangeValues = (additionalFields.updatedRangeUi as IDataObject).updatedRangeValuesUi as IDataObject;
if (updateRangeValues) {
qs.updated_at_range = `${moment.tz(updateRangeValues.from, timezone).format()},${moment.tz(updateRangeValues.to, timezone).format()}`;
}
delete qs.updatedRangeUi;
}
if (returnAll) {
responseData = await boxApiRequestAllItems.call(this, 'entries', 'GET', `/search`, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await boxApiRequest.call(this, 'GET', `/search`, {}, qs);
responseData = responseData.entries;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
// https://developer.box.com/reference/post-files-content
if (operation === 'upload') {
const parentId = this.getNodeParameter('parentId', i) as string;
const isBinaryData = this.getNodeParameter('binaryData', i) as boolean;
const fileName = this.getNodeParameter('fileName', i) as string;
const attributes: IDataObject = {};
if (parentId !== '') {
attributes['parent'] = { id: parentId };
} else {
// if not parent defined save it on the root directory
attributes['parent'] = { id: 0 };
}
if (isBinaryData) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
const body: IDataObject = {};
attributes['name'] = fileName || binaryData.fileName;
body['attributes'] = JSON.stringify(attributes);
body['file'] = {
value: Buffer.from(binaryData.data, BINARY_ENCODING),
options: {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
},
};
responseData = await boxApiRequest.call(this, 'POST', '', {}, {}, 'https://upload.box.com/api/2.0/files/content', { formData: body });
returnData.push.apply(returnData, responseData.entries as IDataObject[]);
} else {
const content = this.getNodeParameter('fileContent', i) as string;
if (fileName === '') {
throw new Error('File name must be set!');
}
attributes['name'] = fileName;
const body: IDataObject = {};
body['attributes'] = JSON.stringify(attributes);
body['file'] = {
value: Buffer.from(content),
options: {
filename: fileName,
contentType: 'text/plain',
},
};
responseData = await boxApiRequest.call(this, 'POST', '', {}, {}, 'https://upload.box.com/api/2.0/files/content', { formData: body });
returnData.push.apply(returnData, responseData.entries as IDataObject[]);
}
}
}
if (resource === 'folder') {
// https://developer.box.com/reference/post-folders
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const parentId = this.getNodeParameter('parentId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {
name,
};
if (parentId) {
body.parent = { id: parentId };
} else {
body.parent = { id: 0 };
}
if (options.access) {
body.folder_upload_email = {
access: options.access as string,
};
}
if (options.fields) {
qs.fields = options.fields as string;
}
responseData = await boxApiRequest.call(this, 'POST', '/folders', body, qs);
returnData.push(responseData);
}
// https://developer.box.com/reference/delete-folders-id
if (operation === 'delete') {
const folderId = this.getNodeParameter('folderId', i) as string;
const recursive = this.getNodeParameter('recursive', i) as boolean;
qs.recursive = recursive;
responseData = await boxApiRequest.call(this, 'DELETE', `/folders/${folderId}`, qs);
responseData = { success: true };
returnData.push(responseData as IDataObject);
}
// https://developer.box.com/reference/get-search/
if (operation === 'search') {
const query = this.getNodeParameter('query', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const timezone = this.getTimezone();
qs.type = 'folder';
qs.query = query;
Object.assign(qs, additionalFields);
if (qs.content_types) {
qs.content_types = (qs.content_types as string).split(',');
}
if (additionalFields.createdRangeUi) {
const createdRangeValues = (additionalFields.createdRangeUi as IDataObject).createdRangeValuesUi as IDataObject;
if (createdRangeValues) {
qs.created_at_range = `${moment.tz(createdRangeValues.from, timezone).format()},${moment.tz(createdRangeValues.to, timezone).format()}`;
}
delete qs.createdRangeUi;
}
if (additionalFields.updatedRangeUi) {
const updateRangeValues = (additionalFields.updatedRangeUi as IDataObject).updatedRangeValuesUi as IDataObject;
if (updateRangeValues) {
qs.updated_at_range = `${moment.tz(updateRangeValues.from, timezone).format()},${moment.tz(updateRangeValues.to, timezone).format()}`;
}
delete qs.updatedRangeUi;
}
if (returnAll) {
responseData = await boxApiRequestAllItems.call(this, 'entries', 'GET', `/search`, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await boxApiRequest.call(this, 'GET', `/search`, {}, qs);
responseData = responseData.entries;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
}
}
if (resource === 'file' && operation === 'download') {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(returnData)];
}
}
}

View File

@@ -0,0 +1,354 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
INodeTypeDescription,
INodeType,
IWebhookResponseData,
} from 'n8n-workflow';
import {
boxApiRequest,
boxApiRequestAllItems,
} from './GenericFunctions';
export class BoxTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Box Trigger',
name: 'boxTrigger',
icon: 'file:box.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when a Github events occurs.',
defaults: {
name: 'Box Trigger',
color: '#00aeef',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'boxOAuth2Api',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: 'Collaboration Accepted',
value: 'COLLABORATION.ACCEPTED',
description: 'A collaboration has been accepted',
},
{
name: 'Collaboration Created',
value: 'COLLABORATION.CREATED',
description: 'A collaboration is created',
},
{
name: 'Collaboration Rejected',
value: 'COLLABORATION.REJECTED',
description: 'A collaboration has been rejected',
},
{
name: 'Collaboration Removed',
value: 'COLLABORATION.REMOVED',
description: 'A collaboration has been removed',
},
{
name: 'Collaboration Updated',
value: 'COLLABORATION.UPDATED',
description: 'A collaboration has been updated.',
},
{
name: 'Comment Created',
value: 'COMMENT.CREATED',
description: 'A comment object is created',
},
{
name: 'Comment Deleted',
value: 'COMMENT.DELETED',
description: 'A comment object is removed',
},
{
name: 'Comment Updated',
value: 'COMMENT.UPDATED',
description: 'A comment object is edited',
},
{
name: 'File Copied',
value: 'FILE.COPIED',
description: 'A file is copied',
},
{
name: 'File Deleted',
value: 'FILE.DELETED',
description: 'A file is moved to the trash',
},
{
name: 'File Downloaded',
value: 'FILE.DOWNLOADED',
description: 'A file is downloaded',
},
{
name: 'File Locked',
value: 'FILE.LOCKED',
description: 'A file is locked',
},
{
name: 'File Moved',
value: 'FILE.MOVED',
description: 'A file is moved from one folder to another',
},
{
name: 'File Previewed',
value: 'FILE.PREVIEWED',
description: 'A file is previewed',
},
{
name: 'File Renamed',
value: 'FILE.RENAMED',
description: 'A file was renamed.',
},
{
name: 'File Restored',
value: 'FILE.RESTORED',
description: 'A file is restored from the trash',
},
{
name: 'File Trashed',
value: 'FILE.TRASHED',
description: 'A file is moved to the trash',
},
{
name: 'File Unlocked',
value: 'FILE.UNLOCKED',
description: 'A file is unlocked',
},
{
name: 'File Uploaded',
value: 'FILE.UPLOADED',
description: 'A file is uploaded to or moved to this folder',
},
{
name: 'Folder Copied',
value: 'FOLDER.COPIED',
description: 'A copy of a folder is made',
},
{
name: 'Folder Created',
value: 'FOLDER.CREATED',
description: 'A folder is created',
},
{
name: 'Folder Deleted',
value: 'FOLDER.DELETED',
description: 'A folder is permanently removed',
},
{
name: 'Folder Downloaded',
value: 'FOLDER.DOWNLOADED',
description: 'A folder is downloaded',
},
{
name: 'Folder Moved',
value: 'FOLDER.MOVED',
description: 'A folder is moved to a different folder',
},
{
name: 'Folder Renamed',
value: 'FOLDER.RENAMED',
description: 'A folder was renamed.',
},
{
name: 'Folder Restored',
value: 'FOLDER.RESTORED',
description: 'A folder is restored from the trash',
},
{
name: 'Folder Trashed',
value: 'FOLDER.TRASHED',
description: 'A folder is moved to the trash',
},
{
name: 'Metadata Instance Created',
value: 'METADATA_INSTANCE.CREATED',
description: 'A new metadata template instance is associated with a file or folder',
},
{
name: 'Metadata Instance Deleted',
value: 'METADATA_INSTANCE.DELETED',
description: 'An existing metadata template instance associated with a file or folder is deleted',
},
{
name: 'Metadata Instance Updated',
value: 'METADATA_INSTANCE.UPDATED',
description: 'An attribute (value) is updated/deleted for an existing metadata template instance associated with a file or folder',
},
{
name: 'Sharedlink Created',
value: 'SHARED_LINK.CREATED',
description: 'A shared link was created',
},
{
name: 'Sharedlink Deleted',
value: 'SHARED_LINK.DELETED',
description: 'A shared link was deleted',
},
{
name: 'Sharedlink Updated',
value: 'SHARED_LINK.UPDATED',
description: 'A shared link was updated',
},
{
name: 'Task Assignment Created',
value: 'TASK_ASSIGNMENT.CREATED',
description: 'A task is created',
},
{
name: 'Task Assignment Updated',
value: 'TASK_ASSIGNMENT.UPDATED',
description: 'A task is updated',
},
{
name: 'Webhook Deleted',
value: 'WEBHOOK.DELETED',
description: 'When a webhook is deleted',
},
],
required: true,
default: [],
description: 'The events to listen to.',
},
{
displayName: 'Target Type',
name: 'targetType',
type: 'options',
options: [
{
name: 'File',
value: 'file',
},
{
name: 'Folder',
value: 'folder',
},
],
default: '',
description: 'The type of item to trigger a webhook',
},
{
displayName: 'Target ID',
name: 'targetId',
type: 'string',
required: false,
default: '',
description: 'The ID of the item to trigger a webhook',
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const events = this.getNodeParameter('events') as string;
const targetId = this.getNodeParameter('targetId') as string;
const targetType = this.getNodeParameter('targetType') as string;
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
const endpoint = '/webhooks';
const webhooks = await boxApiRequestAllItems.call(this, 'entries', 'GET', endpoint, {});
console.log(webhooks);
for (const webhook of webhooks) {
if (webhook.address === webhookUrl &&
webhook.target.id === targetId &&
webhook.target.type === targetType) {
for (const event of events) {
if (!webhook.triggers.includes(event)) {
return false;
}
}
}
// Set webhook-id to be sure that it can be deleted
webhookData.webhookId = webhook.id as string;
return true;
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const events = this.getNodeParameter('events') as string;
const targetId = this.getNodeParameter('targetId') as string;
const targetType = this.getNodeParameter('targetType') as string;
const endpoint = '/webhooks';
const body = {
address: webhookUrl,
triggers: events,
target: {
id: targetId,
type: targetType,
}
};
const responseData = await boxApiRequest.call(this, 'POST', endpoint, body);
if (responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/${webhookData.webhookId}`;
try {
await boxApiRequest.call(this, 'DELETE', endpoint);
} catch (e) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
return {
workflowData: [
this.helpers.returnJsonArray(bodyData)
],
};
}
}

View File

@@ -0,0 +1,599 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const fileOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'file',
],
},
},
options: [
{
name: 'Copy',
value: 'copy',
description: 'Copy a file',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a file',
},
{
name: 'Download',
value: 'download',
description: 'Download a file',
},
{
name: 'Get',
value: 'get',
description: 'Get a file',
},
{
name: 'Search',
value: 'search',
description: 'Search files',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file',
},
],
default: 'upload',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const fileFields = [
/* -------------------------------------------------------------------------- */
/* file:copy */
/* -------------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'copy',
],
resource: [
'file',
],
},
},
default: '',
description: 'File ID',
},
{
displayName: 'Parent ID',
name: 'parentId',
type: 'string',
default: '',
displayOptions: {
show: {
operation: [
'copy',
],
resource: [
'file',
],
},
},
description: 'The ID of folder to copy the file to. If not defined will be copied to the root folder',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'copy',
],
resource: [
'file',
],
},
},
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'An optional new name for the copied file.',
},
{
displayName: 'Version',
name: 'version',
type: 'string',
default: '',
description: 'An optional ID of the specific file version to copy.',
},
],
},
/* -------------------------------------------------------------------------- */
/* file:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'file',
],
},
},
default: '',
description: 'Field ID',
},
/* -------------------------------------------------------------------------- */
/* file:download */
/* -------------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
displayOptions: {
show: {
operation: [
'download',
],
resource: [
'file',
],
},
},
default: '',
description: 'File ID',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
operation: [
'download'
],
resource: [
'file',
],
},
},
description: 'Name of the binary property to which to<br />write the data of the read file.',
},
/* -------------------------------------------------------------------------- */
/* file:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',
type: 'string',
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'file',
],
},
},
default: '',
description: 'Field ID',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'file',
],
},
},
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
},
],
},
/* -------------------------------------------------------------------------- */
/* file:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Query',
name: 'query',
type: 'string',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'file',
],
},
},
default: '',
description: 'The string to search for. This query is matched against item names, descriptions, text content of files, and various other fields of the different item types.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'file',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'file',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'file',
],
},
},
default: {},
options: [
{
displayName: 'Content Types',
name: 'contet_types',
type: 'string',
default: '',
description: `Limits search results to items with the given content types.</br>
Content types are defined as a comma separated lists of Box recognized content types.`,
},
{
displayName: 'Created At Range',
name: 'createdRangeUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Range',
default: {},
options: [
{
displayName: 'Range',
name: 'createdRangeValuesUi',
values: [
{
displayName: 'From',
name: 'from',
type: 'dateTime',
default: '',
},
{
displayName: 'To',
name: 'to',
type: 'dateTime',
default: '',
},
],
},
],
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'ASC',
value: 'ASC',
},
{
name: 'DESC',
value: 'DESC',
},
],
default: '',
description: 'Defines the direction in which search results are ordered. Default value is DESC.',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
},
{
displayName: 'File Extensions',
name: 'file_extensions',
type: 'string',
default: '',
placeholder: 'pdf,png,gif',
description: 'Limits search results to a comma-separated list of file extensions.',
},
{
displayName: 'Folder IDs',
name: 'ancestor_folder_ids',
type: 'string',
default: '',
description: `Limits search results to items within the given list of folders.</br>
Folders are defined as a comma separated lists of folder IDs.`,
},
{
displayName: 'Scope',
name: 'scope',
type: 'options',
options: [
{
name: 'User Content',
value: 'user_content',
},
{
name: 'Enterprise Content',
value: 'enterprise_content',
},
],
default: '',
description: 'Limits search results to a user scope.',
},
{
displayName: 'Size Range',
name: 'size_range',
type: 'string',
default: '',
placeholder: '1000000,5000000',
description: `Limits search results to items within a given file size range.</br>
File size ranges are defined as comma separated byte sizes.`,
},
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{
name: 'Relevance',
value: 'relevance',
},
{
name: 'Modified At',
value: 'modified_at',
},
],
default: 'relevance',
description: 'returns the results ordered in descending order by date at which the item was last modified.',
},
{
displayName: 'Trash Content',
name: 'trash_content',
type: 'options',
options: [
{
name: 'Non Trashed Only',
value: 'non_trashed_only',
},
{
name: 'Trashed Only',
value: 'trashed_only',
},
],
default: 'non_trashed_only',
description: 'Controls if search results include the trash.',
},
{
displayName: 'Update At Range',
name: 'updatedRangeUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Range',
default: {},
options: [
{
displayName: 'Range',
name: 'updatedRangeValuesUi',
values: [
{
displayName: 'From',
name: 'from',
type: 'dateTime',
default: '',
},
{
displayName: 'To',
name: 'to',
type: 'dateTime',
default: '',
},
],
},
],
},
{
displayName: 'User IDs',
name: 'owner_user_ids',
type: 'string',
default: '',
description: `Limits search results to items owned by the given list of owners..</br>
Owners are defined as a comma separated list of user IDs.`,
},
],
},
/* -------------------------------------------------------------------------- */
/* file:upload */
/* -------------------------------------------------------------------------- */
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
placeholder: 'photo.png',
displayOptions: {
show: {
operation: [
'upload',
],
resource: [
'file',
],
},
},
default: '',
description: 'The name the file should be saved as.',
},
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
default: false,
required: true,
displayOptions: {
show: {
operation: [
'upload',
],
resource: [
'file',
],
},
},
description: 'If the data to upload should be taken from binary field.',
},
{
displayName: 'File Content',
name: 'fileContent',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
binaryData: [
false,
],
operation: [
'upload',
],
resource: [
'file',
],
},
},
description: 'The text content of the file.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
binaryData: [
true,
],
operation: [
'upload',
],
resource: [
'file',
],
},
},
description: 'Name of the binary property which contains<br />the data for the file.',
},
{
displayName: 'Parent ID',
name: 'parentId',
type: 'string',
displayOptions: {
show: {
operation: [
'upload',
],
resource: [
'file',
],
},
},
default: '',
description: 'ID of the parent folder that will contain the file. If not it will be uploaded to the root folder',
},
] as INodeProperties[];

View File

@@ -0,0 +1,419 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const folderOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'folder',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a folder',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a folder',
},
{
name: 'Search',
value: 'search',
description: 'Search files',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const folderFields = [
/* -------------------------------------------------------------------------- */
/* folder:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
required: true,
type: 'string',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'folder',
],
},
},
default: '',
description: `Folder's name`,
},
{
displayName: 'Parent ID',
name: 'parentId',
type: 'string',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'folder',
],
},
},
default: '',
description: 'ID of the folder you want to create the new folder in. if not defined it will be created on the root folder',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'folder',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Access',
name: 'access',
type: 'options',
options: [
{
name: 'Collaborators',
value: 'collaborators',
description: 'Only emails from registered email addresses for collaborators will be accepted.',
},
{
name: 'Open',
value: 'open',
description: 'It will accept emails from any email addres',
},
],
default: '',
description: 'ID of the folder you want to create the new folder in',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
},
],
},
/* -------------------------------------------------------------------------- */
/* folder:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Folder ID',
name: 'folderId',
type: 'string',
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'folder',
],
},
},
default: '',
description: 'Folder ID',
},
{
displayName: 'Recursive',
name: 'recursive',
type: 'boolean',
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'folder',
],
},
},
default: false,
description: 'Delete a folder that is not empty by recursively deleting the folder and all of its content.',
},
/* -------------------------------------------------------------------------- */
/* file:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Query',
name: 'query',
type: 'string',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'folder',
],
},
},
default: '',
description: 'The string to search for. This query is matched against item names, descriptions, text content of files, and various other fields of the different item types.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'folder',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'folder',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'folder',
],
},
},
default: {},
options: [
{
displayName: 'Content Types',
name: 'contet_types',
type: 'string',
default: '',
description: `Limits search results to items with the given content types.</br>
Content types are defined as a comma separated lists of Box recognized content types.`,
},
{
displayName: 'Created At Range',
name: 'createdRangeUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Range',
default: {},
options: [
{
displayName: 'Range',
name: 'createdRangeValuesUi',
values: [
{
displayName: 'From',
name: 'from',
type: 'dateTime',
default: '',
},
{
displayName: 'To',
name: 'to',
type: 'dateTime',
default: '',
},
],
},
],
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'ASC',
value: 'ASC',
},
{
name: 'DESC',
value: 'DESC',
},
],
default: '',
description: 'Defines the direction in which search results are ordered. Default value is DESC.',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.',
},
{
displayName: 'File Extensions',
name: 'file_extensions',
type: 'string',
default: '',
placeholder: 'pdf,png,gif',
description: 'Limits search results to a comma-separated list of file extensions.',
},
{
displayName: 'Folder IDs',
name: 'ancestor_folder_ids',
type: 'string',
default: '',
description: `Limits search results to items within the given list of folders.</br>
Folders are defined as a comma separated lists of folder IDs.`,
},
{
displayName: 'Scope',
name: 'scope',
type: 'options',
options: [
{
name: 'User Content',
value: 'user_content',
},
{
name: 'Enterprise Content',
value: 'enterprise_content',
},
],
default: '',
description: 'Limits search results to a user scope.',
},
{
displayName: 'Size Range',
name: 'size_range',
type: 'string',
default: '',
placeholder: '1000000,5000000',
description: `Limits search results to items within a given file size range.</br>
File size ranges are defined as comma separated byte sizes.`,
},
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{
name: 'Relevance',
value: 'relevance',
},
{
name: 'Modified At',
value: 'modified_at',
},
],
default: 'relevance',
description: 'returns the results ordered in descending order by date at which the item was last modified.',
},
{
displayName: 'Trash Content',
name: 'trash_content',
type: 'options',
options: [
{
name: 'Non Trashed Only',
value: 'non_trashed_only',
},
{
name: 'Trashed Only',
value: 'trashed_only',
},
],
default: 'non_trashed_only',
description: 'Controls if search results include the trash.',
},
{
displayName: 'Update At Range',
name: 'updatedRangeUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Range',
default: {},
options: [
{
displayName: 'Range',
name: 'updatedRangeValuesUi',
values: [
{
displayName: 'From',
name: 'from',
type: 'dateTime',
default: '',
},
{
displayName: 'To',
name: 'to',
type: 'dateTime',
default: '',
},
],
},
],
},
{
displayName: 'User IDs',
name: 'owner_user_ids',
type: 'string',
default: '',
description: `Limits search results to items owned by the given list of owners..</br>
Owners are defined as a comma separated list of user IDs.`,
},
],
},
] as INodeProperties[];

View File

@@ -0,0 +1,79 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
IOAuth2Options,
} from 'n8n-workflow';
export async function boxApiRequest(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 || `https://api.box.com/2.0${resource}`,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
const oAuth2Options: IOAuth2Options = {
includeCredentialsOnRefreshOnBody: true,
};
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'boxOAuth2Api', options, oAuth2Options);
} catch (error) {
let errorMessage;
if (error.response && error.response.body) {
if (error.response.body.context_info && error.response.body.context_info.errors) {
const errors = error.response.body.context_info.errors;
errorMessage = errors.map((e: IDataObject) => e.message);
errorMessage = errorMessage.join('|');
} else if (error.response.body.message) {
errorMessage = error.response.body.message;
}
throw new Error(`Box error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function boxApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.limit = 100;
query.offset = 0;
do {
responseData = await boxApiRequest.call(this, method, endpoint, body, query);
query.offset = responseData['offset'] + query.limit;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData[propertyName].length !== 0
);
return returnData;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -215,12 +215,12 @@ export class Chargebee implements INodeType {
{
name: 'List',
value: 'list',
description: 'Returns the invoices',
description: 'Return the invoices',
},
{
name: 'PDF Invoice URL',
value: 'pdfUrl',
description: 'Gets PDF invoice URL and adds it as property "pdfUrl"',
description: 'Get URL for the invoice PDF',
},
],
},

View File

@@ -0,0 +1,140 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import {
pipelineFields,
pipelineOperations,
} from './PipelineDescription';
import {
circleciApiRequest,
circleciApiRequestAllItems,
} from './GenericFunctions';
export class CircleCi implements INodeType {
description: INodeTypeDescription = {
displayName: 'CircleCI',
name: 'circleCi',
icon: 'file:circleCi.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume CircleCI API',
defaults: {
name: 'CircleCI',
color: '#04AA51',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'circleCiApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: ' Pipeline',
value: 'pipeline',
},
],
default: 'pipeline',
description: 'Resource to consume.',
},
...pipelineOperations,
...pipelineFields,
],
};
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 === 'pipeline') {
if (operation === 'get') {
const vcs = this.getNodeParameter('vcs', i) as string;
let slug = this.getNodeParameter('projectSlug', i) as string;
const pipelineNumber = this.getNodeParameter('pipelineNumber', i) as number;
slug = slug.replace(new RegExp(/\//g), '%2F');
const endpoint = `/project/${vcs}/${slug}/pipeline/${pipelineNumber}`;
responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
}
if (operation === 'getAll') {
const vcs = this.getNodeParameter('vcs', i) as string;
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
let slug = this.getNodeParameter('projectSlug', i) as string;
slug = slug.replace(new RegExp(/\//g), '%2F');
if (filters.branch) {
qs.branch = filters.branch;
}
const endpoint = `/project/${vcs}/${slug}/pipeline`;
if (returnAll === true) {
responseData = await circleciApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.items;
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'trigger') {
const vcs = this.getNodeParameter('vcs', i) as string;
let slug = this.getNodeParameter('projectSlug', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
slug = slug.replace(new RegExp(/\//g), '%2F');
const endpoint = `/project/${vcs}/${slug}/pipeline`;
const body: IDataObject = {};
if (additionalFields.branch) {
body.branch = additionalFields.branch as string;
}
if (additionalFields.tag) {
body.tag = additionalFields.tag as string;
}
responseData = await circleciApiRequest.call(this, 'POST', endpoint, body, qs);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View File

@@ -0,0 +1,67 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function circleciApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('circleCiApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: {
'Circle-Token': credentials.apiKey,
'Accept': 'application/json',
},
method,
qs,
body,
uri: uri ||`https://circleci.com/api/v2${resource}`,
json: true
};
options = Object.assign({}, options, option);
if (Object.keys(options.body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request!(options);
} catch (err) {
if (err.response && err.response.body && err.response.body.message) {
// Try to return the error prettier
throw new Error(`CircleCI error response [${err.statusCode}]: ${err.response.body.message}`);
}
// If that data does not exist for some reason return the actual error
throw err; }
}
/**
* Make an API request to paginated CircleCI endpoint
* and return all results
*/
export async function circleciApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await circleciApiRequest.call(this, method, resource, body, query);
returnData.push.apply(returnData, responseData[propertyName]);
query['page-token'] = responseData.next_page_token;
} while (
responseData.next_page_token !== undefined &&
responseData.next_page_token !== null
);
return returnData;
}

View File

@@ -0,0 +1,229 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const pipelineOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'pipeline',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a pipeline',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all pipelines',
},
{
name: 'Trigger',
value: 'trigger',
description: 'Trigger a pipeline',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const pipelineFields = [
/* -------------------------------------------------------------------------- */
/* pipeline:shared */
/* -------------------------------------------------------------------------- */
{
displayName: 'Provider',
name: 'vcs',
type: 'options',
options: [
{
name: 'Bitbucket',
value: 'bitbucket',
},
{
name: 'GitHub',
value: 'github',
},
],
displayOptions: {
show: {
operation: [
'get',
'getAll',
'trigger',
],
resource: [
'pipeline',
],
},
},
default: '',
description: 'Version control system',
},
{
displayName: 'Project Slug',
name: 'projectSlug',
type: 'string',
displayOptions: {
show: {
operation: [
'get',
'getAll',
'trigger',
],
resource: [
'pipeline',
],
},
},
default: '',
placeholder: 'n8n-io/n8n',
description: 'Project slug in the form org-name/repo-name',
},
/* -------------------------------------------------------------------------- */
/* pipeline:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Pipeline Number',
name: 'pipelineNumber',
type: 'number',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'pipeline',
],
},
},
default: 1,
description: 'The number of the pipeline',
},
/* -------------------------------------------------------------------------- */
/* pipeline:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'pipeline',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'pipeline',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'pipeline',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Branch',
name: 'branch',
type: 'string',
default: '',
description: 'The name of a vcs branch.',
},
],
},
/* -------------------------------------------------------------------------- */
/* pipeline:trigger */
/* -------------------------------------------------------------------------- */
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'pipeline',
],
operation: [
'trigger',
],
},
},
options: [
{
displayName: 'Branch',
name: 'branch',
type: 'string',
default: '',
description: `The branch where the pipeline ran.<br/>
The HEAD commit on this branch was used for the pipeline.<br/>
Note that branch and tag are mutually exclusive.`,
},
{
displayName: 'Tag',
name: 'tag',
type: 'string',
default: '',
description: `The tag used by the pipeline.<br/>
The commit that this tag points to was used for the pipeline.<br/>
Note that branch and tag are mutually exclusive`,
},
],
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -16,12 +16,12 @@ export const companyOperations = [
{
name: 'Enrich',
value: 'enrich',
description: 'Lets you look up person and company data based on an email or domain',
description: 'Look up person and company data based on an email or domain',
},
{
name: 'Autocomplete',
value: 'autocomplete',
description: 'Lets you auto-complete company names and retreive logo and domain',
description: 'Auto-complete company names and retrieve logo and domain',
},
],
default: 'enrich',

View File

@@ -16,7 +16,7 @@ export const personOperations = [
{
name: 'Enrich',
value: 'enrich',
description: 'Lets you look up person and company data based on an email or domain',
description: 'Look up a person and company data based on an email or domain',
},
],
default: 'enrich',

View File

@@ -28,7 +28,7 @@ export const timeTrackingOperations = [
{
name: 'Get All',
value: 'getAll',
description: 'Get all loggin times on task',
description: 'Get all logging times on task',
},
{
name: 'Update',

View File

@@ -26,7 +26,7 @@ export const collectionOperations = [
{
name: 'Update an Entry',
value: 'update',
description: 'Update a collection entries',
description: 'Update a collection entry',
},
],
default: 'getAll',

View File

@@ -16,7 +16,7 @@ export const formOperations = [
{
name: 'Submit a Form',
value: 'submit',
description: 'Store submission of a form',
description: 'Store data from a form submission',
},
],

View File

@@ -16,7 +16,7 @@ export const singletonOperations = [
{
name: 'Get',
value: 'get',
description: 'Gets a Singleton',
description: 'Get a singleton',
},
],
default: 'get',

View File

@@ -38,7 +38,7 @@ export class Coda implements INodeType {
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Coda Beta API',
description: 'Consume Coda API',
defaults: {
name: 'Coda',
color: '#c02428',
@@ -152,7 +152,7 @@ export class Coda implements INodeType {
async getViews(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const docId = this.getCurrentNodeParameter('docId');
const views = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/views`, {});
const views = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/tables?tableTypes=view`, {});
for (const view of views) {
const viewName = view.name;
const viewId = view.id;
@@ -185,7 +185,7 @@ export class Coda implements INodeType {
const returnData: INodePropertyOptions[] = [];
const docId = this.getCurrentNodeParameter('docId');
const viewId = this.getCurrentNodeParameter('viewId');
const viewRows = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/views/${viewId}/rows`, {});
const viewRows = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/tables/${viewId}/rows`, {});
for (const viewRow of viewRows) {
const viewRowName = viewRow.name;
const viewRowId = viewRow.id;
@@ -204,7 +204,7 @@ export class Coda implements INodeType {
const docId = this.getCurrentNodeParameter('docId');
const viewId = this.getCurrentNodeParameter('viewId');
const viewColumns = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/views/${viewId}/columns`, {});
const viewColumns = await codaApiRequestAllItems.call(this, 'items', 'GET', `/docs/${docId}/tables/${viewId}/columns`, {});
for (const viewColumn of viewColumns) {
const viewColumnName = viewColumn.name;
const viewColumnId = viewColumn.id;
@@ -488,7 +488,7 @@ export class Coda implements INodeType {
for (let i = 0; i < items.length; i++) {
const docId = this.getNodeParameter('docId', i) as string;
const viewId = this.getNodeParameter('viewId', i) as string;
const endpoint = `/docs/${docId}/views/${viewId}`;
const endpoint = `/docs/${docId}/tables/${viewId}`;
responseData = await codaApiRequest.call(this, 'GET', endpoint, {});
returnData.push(responseData);
}
@@ -499,7 +499,7 @@ export class Coda implements INodeType {
for (let i = 0; i < items.length; i++) {
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const docId = this.getNodeParameter('docId', i) as string;
const endpoint = `/docs/${docId}/views`;
const endpoint = `/docs/${docId}/tables?tableTypes=view`;
if (returnAll) {
responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {});
} else {
@@ -516,7 +516,7 @@ export class Coda implements INodeType {
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const viewId = this.getNodeParameter('viewId', 0) as string;
const options = this.getNodeParameter('options', 0) as IDataObject;
const endpoint = `/docs/${docId}/views/${viewId}/rows`;
const endpoint = `/docs/${docId}/tables/${viewId}/rows`;
if (options.useColumnNames === false) {
qs.useColumnNames = options.useColumnNames as boolean;
} else {
@@ -561,7 +561,7 @@ export class Coda implements INodeType {
const docId = this.getNodeParameter('docId', i) as string;
const viewId = this.getNodeParameter('viewId', i) as string;
const rowId = this.getNodeParameter('rowId', i) as string;
const endpoint = `/docs/${docId}/views/${viewId}/rows/${rowId}`;
const endpoint = `/docs/${docId}/tables/${viewId}/rows/${rowId}`;
responseData = await codaApiRequest.call(this, 'DELETE', endpoint);
returnData.push.apply(returnData,responseData);
}
@@ -574,7 +574,7 @@ export class Coda implements INodeType {
const viewId = this.getNodeParameter('viewId', i) as string;
const rowId = this.getNodeParameter('rowId', i) as string;
const columnId = this.getNodeParameter('columnId', i) as string;
const endpoint = `/docs/${docId}/views/${viewId}/rows/${rowId}/buttons/${columnId}`;
const endpoint = `/docs/${docId}/tables/${viewId}/rows/${rowId}/buttons/${columnId}`;
responseData = await codaApiRequest.call(this, 'POST', endpoint);
returnData.push.apply(returnData,responseData);
}
@@ -585,7 +585,7 @@ export class Coda implements INodeType {
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const docId = this.getNodeParameter('docId', i) as string;
const viewId = this.getNodeParameter('viewId', i) as string;
const endpoint = `/docs/${docId}/views/${viewId}/columns`;
const endpoint = `/docs/${docId}/tables/${viewId}/columns`;
if (returnAll) {
responseData = await codaApiRequestAllItems.call(this, 'items', 'GET', endpoint, {});
} else {
@@ -607,12 +607,13 @@ export class Coda implements INodeType {
const keyName = this.getNodeParameter('keyName', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {};
const endpoint = `/docs/${docId}/views/${viewId}/rows/${rowId}`;
const endpoint = `/docs/${docId}/tables/${viewId}/rows/${rowId}`;
if (options.disableParsing) {
qs.disableParsing = options.disableParsing as boolean;
}
const cells = [];
cells.length = 0;
//@ts-ignore
for (const key of Object.keys(items[i].json[keyName])) {
cells.push({
@@ -623,7 +624,7 @@ export class Coda implements INodeType {
}
body.row = {
cells
},
};
await codaApiRequest.call(this, 'PUT', endpoint, body, qs);
}
return [items];

View File

@@ -17,13 +17,14 @@ export async function codaApiRequest(this: IExecuteFunctions | IExecuteSingleFun
method,
qs,
body,
uri: uri ||`https://coda.io/apis/v1beta1${resource}`,
uri: uri ||`https://coda.io/apis/v1${resource}`,
json: true
};
options = Object.assign({}, options, option);
if (Object.keys(options.body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request!(options);
} catch (error) {

View File

@@ -16,7 +16,7 @@ export const tableOperations = [
{
name: 'Create Row',
value: 'createRow',
description: 'Create/Upsert a row',
description: 'Create/Insert a row',
},
{
name: 'Delete Row',
@@ -41,7 +41,7 @@ export const tableOperations = [
{
name: 'Get Row',
value: 'getRow',
description: 'Get row',
description: 'Get a row',
},
{
name: 'Push Button',

View File

@@ -0,0 +1,288 @@
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
getItemCopy,
pgInsert,
pgQuery,
} from '../Postgres/Postgres.node.functions';
import * as pgPromise from 'pg-promise';
export class CrateDb implements INodeType {
description: INodeTypeDescription = {
displayName: 'CrateDB',
name: 'crateDb',
icon: 'file:cratedb.png',
group: ['input'],
version: 1,
description: 'Gets, add and update data in CrateDB.',
defaults: {
name: 'CrateDB',
color: '#47889f',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'crateDb',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Executes a SQL query.',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database.',
},
{
name: 'Update',
value: 'update',
description: 'Updates rows in database.',
},
],
default: 'insert',
description: 'The operation to perform.',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: ['executeQuery'],
},
},
default: '',
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute.',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Schema',
name: 'schema',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: 'doc',
required: true,
description: 'Name of the schema the table belongs to',
},
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to.',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
placeholder: 'id,name,description',
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
{
displayName: 'Return Fields',
name: 'returnFields',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '*',
description: 'Comma separated list of the fields that the operation will return',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'Name of the table in which to update data in',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: 'id',
required: true,
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description:
'Comma separated list of the properties which should used as columns for rows to update.',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('crateDb');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const pgp = pgPromise();
const config = {
host: credentials.host as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: (credentials.ssl as string) || 'disable',
};
const db = pgp(config);
let returnItems = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
// Add the id to the data
for (let i = 0; i < insertData.length; i++) {
returnItems.push({
json: {
...insertData[i],
...insertItems[i],
},
});
}
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const tableName = this.getNodeParameter('table', 0) as string;
const updateKey = this.getNodeParameter('updateKey', 0) as string;
const queries : string[] = [];
const updatedKeys : string[] = [];
let updateKeyValue : string | number;
let columns : string[] = [];
items.map(item => {
const setOperations : string[] = [];
columns = Object.keys(item.json);
columns.map((col : string) => {
if (col !== updateKey) {
if (typeof item.json[col] === 'string') {
setOperations.push(`${col} = \'${item.json[col]}\'`);
} else {
setOperations.push(`${col} = ${item.json[col]}`);
}
}
});
updateKeyValue = item.json[updateKey] as string | number;
if (updateKeyValue === undefined) {
throw new Error('No value found for update key!');
}
updatedKeys.push(updateKeyValue as string);
const query = `UPDATE "${tableName}" SET ${setOperations.join(',')} WHERE ${updateKey} = ${updateKeyValue};`;
queries.push(query);
});
await db.any(pgp.helpers.concat(queries));
returnItems = this.helpers.returnJsonArray(getItemCopy(items, columns) as IDataObject[]);
} else {
await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`);
}
// Close the connection
await pgp.end();
return this.prepareOutputData(returnItems);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -0,0 +1,329 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
INodeTypeDescription,
INodeType,
IDataObject,
IWebhookResponseData,
} from 'n8n-workflow';
import {
apiRequest,
eventExists,
} from './GenericFunctions';
interface IEvent {
customer?: IDataObject;
email?: IDataObject;
push?: IDataObject;
slack?: IDataObject;
sms?: IDataObject;
webhook?: IDataObject;
}
export class CustomerIoTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Customer.io Trigger',
name: 'customerIoTrigger',
group: ['trigger'],
icon: 'file:customerio.png',
version: 1,
description: 'Starts the workflow on a Customer.io update. (Beta)',
defaults: {
name: 'Customer.io Trigger',
color: '#7131ff',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'customerIoApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
required: true,
default: [],
description: 'The events that can trigger the webhook and whether they are enabled.',
options: [
{
name: 'Customer Subscribed',
value: 'customer.subscribed',
description: 'Whether the webhook is triggered when a list subscriber is added.',
},
{
name: 'Customer Unsubscribe',
value: 'customer.unsubscribed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email Attempted',
value: 'email.attempted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email Bounced',
value: 'email.bounced',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email clicked',
value: 'email.clicked',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email converted',
value: 'email.converted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email delivered',
value: 'email.delivered',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email drafted',
value: 'email.drafted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email failed',
value: 'email.failed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email opened',
value: 'email.opened',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email sent',
value: 'email.sent',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Email spammed',
value: 'email.spammed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push attempted',
value: 'push.attempted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push bounced',
value: 'push.bounced',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push clicked',
value: 'push.clicked',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push delivered',
value: 'push.delivered',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push drafted',
value: 'push.drafted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push failed',
value: 'push.failed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push opened',
value: 'push.opened',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Push sent',
value: 'push.sent',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Slack attempted',
value: 'slack.attempted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Slack clicked',
value: 'slack.clicked',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Slack drafted',
value: 'slack.drafted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Slack failed',
value: 'slack.failed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'Slack sent',
value: 'slack.sent',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS attempted',
value: 'sms.attempted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS bounced',
value: 'sms.bounced',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS clicked',
value: 'sms.clicked',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS delivered',
value: 'sms.delivered',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS drafted',
value: 'sms.drafted',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS failed',
value: 'sms.failed',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
{
name: 'SMS sent',
value: 'sms.sent',
description: 'Whether the webhook is triggered when a list member unsubscribes.',
},
],
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const currentEvents = this.getNodeParameter('events', []) as string[];
const endpoint = '/reporting_webhooks';
let { reporting_webhooks: webhooks } = await apiRequest.call(this, 'GET', endpoint, {});
if (webhooks === null) {
webhooks = [];
}
for (const webhook of webhooks) {
if (webhook.endpoint === webhookUrl &&
eventExists(currentEvents, webhook.events)) {
webhookData.webhookId = webhook.id;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
let webhook;
const webhookUrl = this.getNodeWebhookUrl('default');
const events = this.getNodeParameter('events', []) as string[];
const endpoint = '/reporting_webhooks';
const data: IEvent = {
customer: {},
email: {},
push: {},
slack: {},
sms: {},
webhook: {},
};
for (const event of events) {
const option = event.split('.')[1];
if (event.startsWith('customer')) {
data.customer![option] = true;
}
if (event.startsWith('email')) {
data.email![option] = true;
}
if (event.startsWith('push')) {
data.push![option] = true;
}
if (event.startsWith('slack')) {
data.slack![option] = true;
}
if (event.startsWith('sms')) {
data.sms![option] = true;
}
if (event.startsWith('webhook')) {
data.webhook![option] = true;
}
}
const body = {
endpoint: webhookUrl,
events: data,
};
webhook = await apiRequest.call(this, 'POST', endpoint, body);
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = webhook.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/reporting_webhooks/${webhookData.webhookId}`;
try {
await apiRequest.call(this, 'DELETE', endpoint, {});
} catch (e) {
return false;
}
delete webhookData.webhookId;
}
return true;
},
}
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
return {
workflowData: [
this.helpers.returnJsonArray(bodyData)
],
};
}
}

View File

@@ -0,0 +1,65 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
import {
get,
} from 'lodash';
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('customerIoApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
query = query || {};
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${credentials.apiKey}`,
},
method,
body,
qs: query,
uri: `https://beta-api.customer.io/v1/api${endpoint}`,
json: true,
};
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Customer.io credentials are not valid!');
}
if (error.response && error.response.body && error.response.body.error_code) {
// Try to return the error prettier
const errorBody = error.response.body;
throw new Error(`Customer.io error response [${errorBody.error_code}]: ${errorBody.description}`);
}
// Expected error data did not get returned so throw the actual error
throw error;
}
}
export function eventExists(currentEvents: string[], webhookEvents: IDataObject) {
for (const currentEvent of currentEvents) {
if (get(webhookEvents, `${currentEvent.split('.')[0]}.${currentEvent.split('.')[1]}`) !== true) {
return false;
}
}
return true;
}

View File

@@ -65,22 +65,22 @@ export class Disqus implements INodeType {
{
name: 'Get',
value: 'get',
description: 'Returns forum details.',
description: 'Return forum details',
},
{
name: 'Get All Categories',
value: 'getCategories',
description: 'Returns a list of categories within a forum.',
description: 'Return a list of categories within a forum',
},
{
name: 'Get All Threads',
value: 'getThreads',
description: 'Returns a list of threads within a forum.',
description: 'Return a list of threads within a forum',
},
{
name: 'Get All Posts',
value: 'getPosts',
description: 'Returns a list of posts within a forum.',
description: 'Return a list of posts within a forum',
}
],
default: 'get',

View File

@@ -2,6 +2,7 @@ import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@@ -9,8 +10,9 @@ import {
INodeType,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import {
dropboxApiRequest
} from './GenericFunctions';
export class Dropbox implements INodeType {
description: INodeTypeDescription = {
@@ -23,7 +25,7 @@ export class Dropbox implements INodeType {
description: 'Access data on Dropbox',
defaults: {
name: 'Dropbox',
color: '#22BB44',
color: '#0062ff',
},
inputs: ['main'],
outputs: ['main'],
@@ -31,9 +33,44 @@ export class Dropbox implements INodeType {
{
name: 'dropboxApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'dropboxOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
}
],
default: 'accessToken',
description: 'Means of authenticating with the service.',
},
{
displayName: 'Resource',
name: 'resource',
@@ -441,11 +478,6 @@ export class Dropbox implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('dropboxApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
@@ -453,15 +485,13 @@ export class Dropbox implements INodeType {
let endpoint = '';
let requestMethod = '';
let body: IDataObject | Buffer;
let isJson = false;
let options;
const query: IDataObject = {};
let headers: IDataObject;
const headers: IDataObject = {};
for (let i = 0; i < items.length; i++) {
body = {};
headers = {
'Authorization': `Bearer ${credentials.accessToken}`,
};
if (resource === 'file') {
if (operation === 'download') {
@@ -470,8 +500,9 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
headers['Dropbox-API-Arg'] = JSON.stringify({
path: this.getNodeParameter('path', i) as string,
query.arg = JSON.stringify({
path: this.getNodeParameter('path', i) as string
});
endpoint = 'https://content.dropboxapi.com/2/files/download';
@@ -483,14 +514,18 @@ export class Dropbox implements INodeType {
requestMethod = 'POST';
headers['Content-Type'] = 'application/octet-stream';
headers['Dropbox-API-Arg'] = JSON.stringify({
query.arg = JSON.stringify({
mode: 'overwrite',
path: this.getNodeParameter('path', i) as string,
path: this.getNodeParameter('path', i) as string
});
endpoint = 'https://content.dropboxapi.com/2/files/upload';
if (this.getNodeParameter('binaryData', i) === true) {
options = { json: false };
// Is binary file to upload
const item = items[i];
@@ -518,7 +553,6 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
isJson = true;
body = {
path: this.getNodeParameter('path', i) as string,
};
@@ -531,7 +565,6 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
isJson = true;
body = {
path: this.getNodeParameter('path', i) as string,
limit: 2000,
@@ -551,7 +584,6 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
isJson = true;
body = {
from_path: this.getNodeParameter('path', i) as string,
to_path: this.getNodeParameter('toPath', i) as string,
@@ -565,7 +597,6 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
isJson = true;
body = {
path: this.getNodeParameter('path', i) as string,
};
@@ -578,7 +609,6 @@ export class Dropbox implements INodeType {
// ----------------------------------
requestMethod = 'POST';
isJson = true;
body = {
from_path: this.getNodeParameter('path', i) as string,
to_path: this.getNodeParameter('toPath', i) as string,
@@ -590,40 +620,15 @@ export class Dropbox implements INodeType {
throw new Error(`The resource "${resource}" is not known!`);
}
const options: OptionsWithUri = {
headers,
method: requestMethod,
qs: {},
uri: endpoint,
json: isJson,
};
if (Object.keys(body).length) {
options.body = body;
}
if (resource === 'file' && operation === 'download') {
// Return the data as a buffer
options.encoding = null;
options = { encoding: null };
}
let responseData;
try {
responseData = await this.helpers.request(options);
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Dropbox credentials are not valid!');
}
let responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options);
if (error.error && error.error.error_summary) {
// Try to return the error prettier
throw new Error(`Dropbox error response [${error.statusCode}]: ${error.error.error_summary}`);
}
// If that data does not exist for some reason return the actual error
throw error;
if (resource === 'file' && operation === 'upload') {
responseData = JSON.parse(responseData);
}
if (resource === 'file' && operation === 'download') {
@@ -645,7 +650,7 @@ export class Dropbox implements INodeType {
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData, filePathDownload);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(Buffer.from(responseData), filePathDownload);
} else if (resource === 'folder' && operation === 'list') {
@@ -672,8 +677,6 @@ export class Dropbox implements INodeType {
returnData.push(newItem as IDataObject);
}
} else if (resource === 'file' && operation === 'upload') {
returnData.push(JSON.parse(responseData) as IDataObject);
} else {
returnData.push(responseData as IDataObject);
}

View File

@@ -0,0 +1,69 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
/**
* Make an API request to Dropbox
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers?: object, option: IDataObject = {}): Promise<any> {// tslint:disable-line:no-any
const options: OptionsWithUri = {
headers,
method,
qs: query,
body,
uri: endpoint,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
Object.assign(options, option);
const authenticationMethod = this.getNodeParameter('authentication', 0) as string;
try {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('dropboxApi') as IDataObject;
options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
return await this.helpers.request(options);
} else {
return await this.helpers.requestOAuth2.call(this, 'dropboxOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Dropbox credentials are not valid!');
}
if (error.error && error.error.error_summary) {
// Try to return the error prettier
throw new Error(
`Dropbox error response [${error.statusCode}]: ${error.error.error_summary}`
);
}
// If that data does not exist for some reason return the actual error
throw error;
}
}

View File

@@ -133,7 +133,7 @@ export class FacebookGraphApi implements INodeType {
name: 'edge',
type: 'string',
default: '',
description: 'Edge of the node on which to operate. Edges represent collections of objects wich are attached to the node.',
description: 'Edge of the node on which to operate. Edges represent collections of objects which are attached to the node.',
placeholder: 'videos',
required: false,
},

View File

@@ -49,9 +49,7 @@ export class Flow implements INodeType {
{
name: 'Task',
value: 'task',
description: `The primary unit within Flow; tasks track units of work and can be assigned, sorted, nested, and tagged.</br>
Tasks can either be part of a List, or "private" (meaning "without a list", essentially).</br>
Through this endpoint you are able to do anything you wish to your tasks in Flow, including create new ones.`,
description: `Tasks are units of work that can be private or assigned to a list. Through this endpoint, you can manipulate your tasks in Flow, including creating new ones`,
},
],
default: 'task',

View File

@@ -99,7 +99,7 @@ export class FlowTrigger implements INodeType {
]
}
},
description: `Taks ids separated by ,`,
description: `Task ids separated by ,`,
},
],

View File

@@ -21,12 +21,12 @@ export const taskOpeations = [
{
name: 'Update',
value: 'update',
description: 'Update task',
description: 'Update a task',
},
{
name: 'Get',
value: 'get',
description: 'Get task',
description: 'Get a task',
},
{
name: 'Get All',

View File

@@ -0,0 +1,434 @@
import {
BINARY_ENCODING,
IExecuteFunctions
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription
} from 'n8n-workflow';
import {
basename,
dirname,
} from 'path';
import * as ftpClient from 'promise-ftp';
import * as sftpClient from 'ssh2-sftp-client';
export class Ftp implements INodeType {
description: INodeTypeDescription = {
displayName: 'FTP',
name: 'ftp',
icon: 'fa:server',
group: ['input'],
version: 1,
subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}',
description: 'Transfers files via FTP or SFTP.',
defaults: {
name: 'FTP',
color: '#303050',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'ftp',
required: true,
displayOptions: {
show: {
protocol: [
'ftp',
],
},
},
},
{
name: 'sftp',
required: true,
displayOptions: {
show: {
protocol: [
'sftp',
],
},
},
},
],
properties: [
{
displayName: 'Protocol',
name: 'protocol',
type: 'options',
options: [
{
name: 'FTP',
value: 'ftp',
},
{
name: 'SFTP',
value: 'sftp',
},
],
default: 'ftp',
description: 'File transfer protocol.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Download',
value: 'download',
description: 'Download a file.',
},
{
name: 'List',
value: 'list',
description: 'List folder content.',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload a file.',
},
],
default: 'download',
description: 'Operation to perform.',
},
// ----------------------------------
// download
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'download',
],
},
},
name: 'path',
type: 'string',
default: '',
placeholder: '/documents/invoice.txt',
description: 'The file path of the file to download. Has to contain the full path.',
required: true,
},
{
displayName: 'Binary Property',
displayOptions: {
show: {
operation: [
'download',
],
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
description: 'Object property name which holds binary data.',
required: true,
},
// ----------------------------------
// upload
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'upload',
],
},
},
name: 'path',
type: 'string',
default: '',
description: 'The file path of the file to upload. Has to contain the full path.',
required: true,
},
{
displayName: 'Binary Data',
displayOptions: {
show: {
operation: [
'upload',
],
},
},
name: 'binaryData',
type: 'boolean',
default: true,
description: 'The text content of the file to upload.',
},
{
displayName: 'Binary Property',
displayOptions: {
show: {
operation: [
'upload',
],
binaryData: [
true,
]
},
},
name: 'binaryPropertyName',
type: 'string',
default: 'data',
description: 'Object property name which holds binary data.',
required: true,
},
{
displayName: 'File Content',
displayOptions: {
show: {
operation: [
'upload',
],
binaryData: [
false,
]
},
},
name: 'fileContent',
type: 'string',
default: '',
description: 'The text content of the file to upload.',
},
// ----------------------------------
// list
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'list',
],
},
},
name: 'path',
type: 'string',
default: '/',
description: 'Path of directory to list contents of.',
required: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
// const returnData: IDataObject[] = [];
const returnItems: INodeExecutionData[] = [];
const qs: IDataObject = {};
let responseData;
const operation = this.getNodeParameter('operation', 0) as string;
let credentials: ICredentialDataDecryptedObject | undefined = undefined;
const protocol = this.getNodeParameter('protocol', 0) as string;
if (protocol === 'sftp') {
credentials = this.getCredentials('sftp');
} else {
credentials = this.getCredentials('ftp');
}
if (credentials === undefined) {
throw new Error('Failed to get credentials!');
}
let ftp: ftpClient;
let sftp: sftpClient;
if (protocol === 'sftp') {
sftp = new sftpClient();
await sftp.connect({
host: credentials.host as string,
port: credentials.port as number,
username: credentials.username as string,
password: credentials.password as string,
});
} else {
ftp = new ftpClient();
await ftp.connect({
host: credentials.host as string,
port: credentials.port as number,
user: credentials.username as string,
password: credentials.password as string
});
}
for (let i = 0; i < items.length; i++) {
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
if (protocol === 'sftp') {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
responseData = await sftp!.list(path);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
if (operation === 'download') {
responseData = await sftp!.get(path);
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData as Buffer, filePathDownload);
returnItems.push(items[i]);
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i) as string;
// Check if dir path exists
const dirExists = await sftp!.exists(dirname(remotePath));
// If dir does not exist, create all recursively in path
if (!dirExists) {
// Separate filename from dir path
const fileName = basename(remotePath);
const dirPath = remotePath.replace(fileName, '');
// Create directory
await sftp!.mkdir(dirPath, true);
}
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new Error('No binary data exists on item!');
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) {
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`);
}
const buffer = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING) as Buffer;
await sftp!.put(buffer, remotePath);
} else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8') as Buffer;
await sftp!.put(buffer, remotePath);
}
returnItems.push(items[i]);
}
}
if (protocol === 'ftp') {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
responseData = await ftp!.list(path);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
if (operation === 'download') {
responseData = await ftp!.get(path);
// Convert readable stream to buffer so that can be displayed properly
const chunks = [];
for await (const chunk of responseData) {
chunks.push(chunk);
}
// @ts-ignore
responseData = Buffer.concat(chunks);
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData, filePathDownload);
returnItems.push(items[i]);
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i) as string;
const fileName = basename(remotePath);
const dirPath = remotePath.replace(fileName, '');
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new Error('No binary data exists on item!');
}
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[propertyNameUpload] === undefined) {
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`);
}
const buffer = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING) as Buffer;
try {
await ftp!.put(buffer, remotePath);
} catch (error) {
if (error.code === 553) {
// Create directory
await ftp!.mkdir(dirPath, true);
await ftp!.put(buffer, remotePath);
} else {
throw new Error(error);
}
}
} else {
// Is text file
const buffer = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8') as Buffer;
try {
await ftp!.put(buffer, remotePath);
} catch (error) {
if (error.code === 553) {
// Create directory
await ftp!.mkdir(dirPath, true);
await ftp!.put(buffer, remotePath);
} else {
throw new Error(error);
}
}
}
returnItems.push(items[i]);
}
}
}
if (protocol === 'sftp') {
await sftp!.end();
} else {
await ftp!.end();
}
return [returnItems];
}
}

View File

@@ -17,14 +17,14 @@ import {
export class Github implements INodeType {
description: INodeTypeDescription = {
displayName: 'Github',
displayName: 'GitHub',
name: 'github',
icon: 'file:github.png',
group: ['input'],
version: 1,
description: 'Retrieve data from Github API.',
description: 'Retrieve data from GitHub API.',
defaults: {
name: 'Github',
name: 'GitHub',
color: '#665533',
},
inputs: ['main'],
@@ -178,7 +178,7 @@ export class Github implements INodeType {
{
name: 'Get',
value: 'get',
description: 'Get the data of a single issues',
description: 'Get the data of a single issue',
},
],
default: 'create',
@@ -220,7 +220,7 @@ export class Github implements INodeType {
{
name: 'List Popular Paths',
value: 'listPopularPaths',
description: 'Get the data of a file in repositoryGet the top 10 popular content paths over the last 14 days.',
description: 'Get the top 10 popular content paths over the last 14 days.',
},
{
name: 'List Referrers',
@@ -458,7 +458,7 @@ export class Github implements INodeType {
description: 'The name of the author of the commit.',
},
{
displayName: 'EMail',
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
@@ -491,7 +491,7 @@ export class Github implements INodeType {
description: 'The name of the committer of the commit.',
},
{
displayName: 'EMail',
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
@@ -1014,28 +1014,28 @@ export class Github implements INodeType {
name: 'assignee',
type: 'string',
default: '',
description: 'Return only issuse which are assigned to a specific user.',
description: 'Return only issues which are assigned to a specific user.',
},
{
displayName: 'Creator',
name: 'creator',
type: 'string',
default: '',
description: 'Return only issuse which were created by a specific user.',
description: 'Return only issues which were created by a specific user.',
},
{
displayName: 'Mentioned',
name: 'mentioned',
type: 'string',
default: '',
description: 'Return only issuse in which a specific user was mentioned.',
description: 'Return only issues in which a specific user was mentioned.',
},
{
displayName: 'Labels',
name: 'labels',
type: 'string',
default: '',
description: 'Return only issuse with the given labels. Multiple lables can be separated by comma.',
description: 'Return only issues with the given labels. Multiple lables can be separated by comma.',
},
{
displayName: 'Updated Since',

View File

@@ -7,6 +7,7 @@ import {
import {
IDataObject,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
/**
* Make an API request to Gitlab
@@ -17,31 +18,48 @@ import {
* @param {object} body
* @returns {Promise<any>}
*/
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('gitlabApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const options = {
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
const options : OptionsWithUri = {
method,
headers: {
'Private-Token': `${credentials.accessToken}`,
},
headers: {},
body,
qs: query,
uri: `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`,
uri: '',
json: true
};
try {
//@ts-ignore
return await this.helpers?.request(options);
if (query === undefined) {
delete options.qs;
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('gitlabApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.headers!['Private-Token'] = `${credentials.accessToken}`;
options.uri = `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`;
return await this.helpers.request(options);
} else {
const credentials = this.getCredentials('gitlabOAuth2Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.uri = `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`;
return await this.helpers.requestOAuth2!.call(this, 'gitlabOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Gitlab credentials are not valid!');
throw new Error('The GitLab credentials are not valid!');
}
if (error.response && error.response.body && error.response.body.message) {

View File

@@ -13,16 +13,15 @@ import {
gitlabApiRequest,
} from './GenericFunctions';
export class Gitlab implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gitlab',
displayName: 'GitLab',
name: 'gitlab',
icon: 'file:gitlab.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Retrieve data from Gitlab API.',
description: 'Retrieve data from GitLab API.',
defaults: {
name: 'Gitlab',
color: '#FC6D27',
@@ -33,9 +32,44 @@ export class Gitlab implements INodeType {
{
name: 'gitlabApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'gitlabOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'The resource to operate on.',
},
{
displayName: 'Resource',
name: 'resource',
@@ -97,7 +131,7 @@ export class Gitlab implements INodeType {
{
name: 'Get',
value: 'get',
description: 'Get the data of a single issues',
description: 'Get the data of a single issue',
},
{
name: 'Lock',
@@ -173,7 +207,7 @@ export class Gitlab implements INodeType {
{
name: 'Create',
value: 'create',
description: 'Creates a new release',
description: 'Create a new release',
},
],
default: 'create',
@@ -793,10 +827,26 @@ export class Gitlab implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('gitlabApi');
let credentials;
if (credentials === undefined) {
throw new Error('No credentials got returned!');
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
if (authenticationMethod === 'accessToken') {
credentials = this.getCredentials('gitlabApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
} else {
credentials = this.getCredentials('gitlabOAuth2Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
}
} catch (error) {
throw new Error(error);
}
// Operations which overwrite the returned data

View File

@@ -14,16 +14,15 @@ import {
gitlabApiRequest,
} from './GenericFunctions';
export class GitlabTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gitlab Trigger',
displayName: 'GitLab Trigger',
name: 'gitlabTrigger',
icon: 'file:gitlab.png',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
description: 'Starts the workflow when a Gitlab events occurs.',
description: 'Starts the workflow when a GitLab event occurs.',
defaults: {
name: 'Gitlab Trigger',
color: '#FC6D27',
@@ -34,7 +33,25 @@ export class GitlabTrigger implements INodeType {
{
name: 'gitlabApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'gitlabOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -45,6 +62,23 @@ export class GitlabTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'The resource to operate on.',
},
{
displayName: 'Repository Owner',
name: 'owner',
@@ -203,7 +237,7 @@ export class GitlabTrigger implements INodeType {
if (responseData.id === undefined) {
// Required data is missing so was not successful
throw new Error('Gitlab webhook creation response did not contain the expected data.');
throw new Error('GitLab webhook creation response did not contain the expected data.');
}
const webhookData = this.getWorkflowStaticData('node');

View File

@@ -122,7 +122,7 @@ export class GoogleDrive implements INodeType {
{
name: 'List',
value: 'list',
description: 'Returns files and folders',
description: 'List files and folders',
},
{
name: 'Upload',

View File

@@ -181,7 +181,7 @@ export class GoogleSheet {
}
/**
* Returns the given sheet data in a strucutred way
* Returns the given sheet data in a structured way
*/
structureData(inputData: string[][], startRow: number, keys: string[], addEmpty?: boolean): IDataObject[] {
const returnData = [];
@@ -208,7 +208,7 @@ export class GoogleSheet {
/**
* Returns the given sheet data in a strucutred way using
* Returns the given sheet data in a structured way using
* the startRow as the one with the name of the key
*/
structureArrayDataByColumn(inputData: string[][], keyRow: number, dataStartRow: number): IDataObject[] {
@@ -216,7 +216,7 @@ export class GoogleSheet {
const keys: string[] = [];
if (keyRow < 0 || dataStartRow < keyRow || keyRow >= inputData.length) {
// The key row does not exist so it is not possible to strucutre data
// The key row does not exist so it is not possible to structure data
return [];
}

View File

@@ -84,32 +84,32 @@ export class GoogleSheets implements INodeType {
{
name: 'Append',
value: 'append',
description: 'Appends the data to a Sheet',
description: 'Append data to a sheet',
},
{
name: 'Clear',
value: 'clear',
description: 'Clears data from a Sheet',
description: 'Clear data from a sheet',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete columns and rows from a Sheet',
description: 'Delete columns and rows from a sheet',
},
{
name: 'Lookup',
value: 'lookup',
description: 'Looks for a specific column value and then returns the matching row'
description: 'Look up a specific column value and return the matching row'
},
{
name: 'Read',
value: 'read',
description: 'Reads data from a Sheet'
description: 'Read data from a sheet'
},
{
name: 'Update',
value: 'update',
description: 'Updates rows in a sheet'
description: 'Update rows in a sheet'
},
],
default: 'read',
@@ -151,7 +151,7 @@ export class GoogleSheets implements INodeType {
displayName: 'To Delete',
name: 'toDelete',
placeholder: 'Add Columns/Rows to delete',
description: 'Deletes colums and rows from a sheet.',
description: 'Deletes columns and rows from a sheet.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
@@ -363,7 +363,7 @@ export class GoogleSheets implements INodeType {
},
},
default: 0,
description: 'Index of the row which contains the keys. Starts at 0.<br />The incoming node data is matched to the keys for assignment. The matching is case sensitve.',
description: 'Index of the row which contains the keys. Starts at 0.<br />The incoming node data is matched to the keys for assignment. The matching is case sensitive.',
},

View File

@@ -16,11 +16,11 @@ export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: string,
resource: string,
body: any = {},
body: IDataObject = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {}
): Promise<any> {
): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json'
@@ -65,9 +65,9 @@ export async function googleApiRequestAllItems(
propertyName: string,
method: string,
endpoint: string,
body: any = {},
body: IDataObject = {},
query: IDataObject = {}
): Promise<any> {
): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;

View File

@@ -1,6 +1,6 @@
import {
IExecuteFunctions,
} from 'n8n-core';
} from 'n8n-core';
import {
IDataObject,
@@ -102,6 +102,7 @@ export class GoogleTasks implements INodeType {
body = {};
//https://developers.google.com/tasks/v1/reference/tasks/insert
const taskId = this.getNodeParameter('task', i) as string;
body.title = this.getNodeParameter('title', i) as string;
const additionalFields = this.getNodeParameter(
'additionalFields',
i
@@ -121,11 +122,6 @@ export class GoogleTasks implements INodeType {
if (additionalFields.notes) {
body.notes = additionalFields.notes as string;
}
if (additionalFields.title) {
body.title = additionalFields.title as string;
}
if (additionalFields.dueDate) {
body.dueDate = additionalFields.dueDate as string;
}

View File

@@ -70,6 +70,13 @@ export const taskFields = [
},
default: '',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the task.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
@@ -146,13 +153,7 @@ export const taskFields = [
default: '',
description: 'Current status of the task.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the task.',
},
],
},
/* -------------------------------------------------------------------------- */

View File

@@ -0,0 +1,80 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an API request to HackerNews
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} endpoint
* @param {IDataObject} qs
* @returns {Promise<any>}
*/
export async function hackerNewsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
method,
qs,
uri: `http://hn.algolia.com/api/v1/${endpoint}`,
json: true,
};
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
// Try to return the error prettier
throw new Error(`Hacker News error response [${error.statusCode}]: ${error.response.body.error}`);
}
throw error;
}
}
/**
* Make an API request to HackerNews
* and return all results
*
* @export
* @param {(IHookFunctions | IExecuteFunctions)} this
* @param {string} method
* @param {string} endpoint
* @param {IDataObject} qs
* @returns {Promise<any>}
*/
export async function hackerNewsApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
qs.hitsPerPage = 100;
const returnData: IDataObject[] = [];
let responseData;
let itemsReceived = 0;
do {
responseData = await hackerNewsApiRequest.call(this, method, endpoint, qs);
returnData.push.apply(returnData, responseData.hits);
if (returnData !== undefined) {
itemsReceived += returnData.length;
}
} while (
responseData.nbHits > itemsReceived
);
return returnData;
}

View File

@@ -0,0 +1,384 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
IDataObject,
} from 'n8n-workflow';
import {
hackerNewsApiRequest,
hackerNewsApiRequestAllItems,
} from './GenericFunctions';
export class HackerNews implements INodeType {
description: INodeTypeDescription = {
displayName: 'Hacker News',
name: 'hackerNews',
icon: 'file:hackernews.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Hacker News API',
defaults: {
name: 'Hacker News',
color: '#ff6600',
},
inputs: ['main'],
outputs: ['main'],
properties: [
// ----------------------------------
// Resources
// ----------------------------------
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'All',
value: 'all',
},
{
name: 'Article',
value: 'article',
},
{
name: 'User',
value: 'user',
},
],
default: 'article',
description: 'Resource to consume.',
},
// ----------------------------------
// Operations
// ----------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'all',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all items',
},
],
default: 'getAll',
description: 'Operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'article',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a Hacker News article',
},
],
default: 'get',
description: 'Operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a Hacker News user',
},
],
default: 'get',
description: 'Operation to perform.',
},
// ----------------------------------
// Fields
// ----------------------------------
{
displayName: 'Article ID',
name: 'articleId',
type: 'string',
required: true,
default: '',
description: 'The ID of the Hacker News article to be returned',
displayOptions: {
show: {
resource: [
'article',
],
operation: [
'get',
],
},
},
},
{
displayName: 'Username',
name: 'username',
type: 'string',
required: true,
default: '',
description: 'The Hacker News user to be returned',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results for the query or only up to a limit.',
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
description: 'Limit of Hacker News articles to be returned for the query.',
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'article',
],
operation: [
'get',
],
},
},
options: [
{
displayName: 'Include comments',
name: 'includeComments',
type: 'boolean',
default: false,
description: 'Whether to include all the comments in a Hacker News article.',
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'all',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Keyword',
name: 'keyword',
type: 'string',
default: '',
description: 'The keyword for filtering the results of the query.',
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
options: [
{
name: 'Story',
value: 'story',
description: 'Returns query results filtered by story tag',
},
{
name: 'Comment',
value: 'comment',
description: 'Returns query results filtered by comment tag',
},
{
name: 'Poll',
value: 'poll',
description: 'Returns query results filtered by poll tag',
},
{
name: 'Show HN',
value: 'show_hn', // snake case per HN tags
description: 'Returns query results filtered by Show HN tag',
},
{
name: 'Ask HN',
value: 'ask_hn', // snake case per HN tags
description: 'Returns query results filtered by Ask HN tag',
},
{
name: 'Front Page',
value: 'front_page', // snake case per HN tags
description: 'Returns query results filtered by Front Page tag',
},
],
default: '',
description: 'Tags for filtering the results of the query.',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let returnAll = false;
for (let i = 0; i < items.length; i++) {
let qs: IDataObject = {};
let endpoint = '';
let includeComments = false;
if (resource === 'all') {
if (operation === 'getAll') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const keyword = additionalFields.keyword as string;
const tags = additionalFields.tags as string[];
qs = {
query: keyword,
tags: tags ? tags.join() : '',
};
returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (!returnAll) {
qs.hitsPerPage = this.getNodeParameter('limit', i) as number;
}
endpoint = 'search?';
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else if (resource === 'article') {
if (operation === 'get') {
endpoint = `items/${this.getNodeParameter('articleId', i)}`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
includeComments = additionalFields.includeComments as boolean;
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else if (resource === 'user') {
if (operation === 'get') {
endpoint = `users/${this.getNodeParameter('username', i)}`;
} else {
throw new Error(`The operation '${operation}' is unknown!`);
}
} else {
throw new Error(`The resource '${resource}' is unknown!`);
}
let responseData;
if (returnAll === true) {
responseData = await hackerNewsApiRequestAllItems.call(this, 'GET', endpoint, qs);
} else {
responseData = await hackerNewsApiRequest.call(this, 'GET', endpoint, qs);
if (resource === 'all' && operation === 'getAll') {
responseData = responseData.hits;
}
}
if (resource === 'article' && operation === 'get' && !includeComments) {
delete responseData.children;
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -16,7 +16,7 @@ export const estimateOperations = [
{
name: 'Create',
value: 'create',
description: `Create a estimate`,
description: `Create an estimate`,
},
{
name: 'Delete',
@@ -36,7 +36,7 @@ export const estimateOperations = [
{
name: 'Update',
value: 'update',
description: `Update a estimate`,
description: `Update an estimate`,
},
],
default: 'getAll',

View File

@@ -26,12 +26,12 @@ export const expenseOperations = [
{
name: 'Create',
value: 'create',
description: `Create a expense`,
description: `Create an expense`,
},
{
name: 'Update',
value: 'update',
description: `Update a expense`,
description: `Update an expense`,
},
{
name: 'Delete',

View File

@@ -16,7 +16,7 @@ export const invoiceOperations = [
{
name: 'Get',
value: 'get',
description: 'Get data of a invoice',
description: 'Get data of an invoice',
},
{
name: 'Get All',
@@ -26,17 +26,17 @@ export const invoiceOperations = [
{
name: 'Create',
value: 'create',
description: `Create a invoice`,
description: `Create an invoice`,
},
{
name: 'Update',
value: 'update',
description: `Update a invoice`,
description: `Update an invoice`,
},
{
name: 'Delete',
value: 'delete',
description: `Delete a invoice`,
description: `Delete an invoice`,
},
],
default: 'getAll',

View File

@@ -71,6 +71,17 @@ export class HttpRequest implements INodeType {
},
},
},
{
name: 'oAuth1Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth1',
],
},
},
},
{
name: 'oAuth2Api',
required: true,
@@ -101,6 +112,10 @@ export class HttpRequest implements INodeType {
name: 'Header Auth',
value: 'headerAuth'
},
{
name: 'OAuth1',
value: 'oAuth1'
},
{
name: 'OAuth2',
value: 'oAuth2'
@@ -578,6 +593,7 @@ export class HttpRequest implements INodeType {
const httpBasicAuth = this.getCredentials('httpBasicAuth');
const httpDigestAuth = this.getCredentials('httpDigestAuth');
const httpHeaderAuth = this.getCredentials('httpHeaderAuth');
const oAuth1Api = this.getCredentials('oAuth1Api');
const oAuth2Api = this.getCredentials('oAuth2Api');
let requestOptions: OptionsWithUri;
@@ -799,10 +815,13 @@ export class HttpRequest implements INodeType {
}
try {
// Now that the options are all set make the actual http request
if (oAuth2Api !== undefined) {
if (oAuth1Api !== undefined) {
//@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
response = await this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions);
}
else if (oAuth2Api !== undefined) {
//@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
} else {
response = await this.helpers.request(requestOptions);
}

View File

@@ -33,7 +33,7 @@ export const companyOperations = [
{
name: 'Get All',
value: 'getAll',
description: 'Get all company',
description: 'Get all companies',
},
{
name: 'Get Recently Created',

View File

@@ -23,7 +23,7 @@ export const contactOperations = [
{
name: 'Delete',
value: 'delete',
description: 'Delete a contacts',
description: 'Delete a contact',
},
{
name: 'Get',

View File

@@ -23,7 +23,7 @@ export const dealOperations = [
{
name: 'Delete',
value: 'delete',
description: 'Delete a deals',
description: 'Delete a deal',
},
{
name: 'Get',

View File

@@ -45,7 +45,7 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions
return await this.helpers.request!(options);
} else {
// @ts-ignore
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, 'Bearer');
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer' });
}
} catch (error) {
let errorMessages;

View File

@@ -56,13 +56,13 @@ import {
export class Hubspot implements INodeType {
description: INodeTypeDescription = {
displayName: 'Hubspot',
displayName: 'HubSpot',
name: 'hubspot',
icon: 'file:hubspot.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Hubspot API',
description: 'Consume HubSpot API',
defaults: {
name: 'Hubspot',
color: '#ff7f64',

View File

@@ -20,13 +20,13 @@ import {
export class HubspotTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Hubspot Trigger',
displayName: 'HubSpot Trigger',
name: 'hubspotTrigger',
icon: 'file:hubspot.png',
group: ['trigger'],
version: 1,
subtitle: '={{($parameter["appId"]) ? $parameter["event"] : ""}}',
description: 'Starts the workflow when Hubspot events occure.',
description: 'Starts the workflow when HubSpot events occur.',
defaults: {
name: 'Hubspot Trigger',
color: '#ff7f64',

View File

@@ -23,7 +23,7 @@ export const ticketOperations = [
{
name: 'Delete',
value: 'delete',
description: 'Delete a tickets',
description: 'Delete a ticket',
},
{
name: 'Get',

View File

@@ -42,17 +42,17 @@ export class Hunter implements INodeType {
{
name: ' Domain Search',
value: 'domainSearch',
description: 'Get every email address found on the internet using a given domain name, with sources.',
description: 'Get every email address found on the internet using a given domain name, with sources',
},
{
name: ' Email Finder',
value: 'emailFinder',
description: 'Generates or retrieves the most likely email address from a domain name, a first name and a last name.',
description: 'Generate or retrieve the most likely email address from a domain name, a first name and a last name',
},
{
name: 'Email Verifier',
value: 'emailVerifier',
description: 'Allows you to verify the deliverability of an email address.',
description: 'Verify the deliverability of an email address',
},
],
default: 'domainSearch',

View File

@@ -46,7 +46,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut
},
method,
qs: query,
uri: uri || `${domain}/rest/api/2${endpoint}`,
uri: uri || `${domain}/rest${endpoint}`,
body,
json: true
};
@@ -54,6 +54,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage = error.message;
if (error.response.body) {
@@ -104,3 +105,58 @@ export function validateJSON(json: string | undefined): any { // tslint:disable-
}
return result;
}
export function eventExists (currentEvents : string[], webhookEvents: string[]) {
for (const currentEvent of currentEvents) {
if (!webhookEvents.includes(currentEvent)) {
return false;
}
}
return true;
}
export function getId (url: string) {
return url.split('/').pop();
}
export const allEvents = [
'board_created',
'board_updated',
'board_deleted',
'board_configuration_changed',
'comment_created',
'comment_updated',
'comment_deleted',
'jira:issue_created',
'jira:issue_updated',
'jira:issue_deleted',
'option_voting_changed',
'option_watching_changed',
'option_unassigned_issues_changed',
'option_subtasks_changed',
'option_attachments_changed',
'option_issuelinks_changed',
'option_timetracking_changed',
'project_created',
'project_updated',
'project_deleted',
'sprint_created',
'sprint_deleted',
'sprint_updated',
'sprint_started',
'sprint_closed',
'user_created',
'user_updated',
'user_deleted',
'jira:version_released',
'jira:version_unreleased',
'jira:version_created',
'jira:version_moved',
'jira:version_updated',
'jira:version_deleted',
'issuelink_created',
'issuelink_deleted',
'worklog_created',
'worklog_updated',
'worklog_deleted',
];

View File

@@ -41,12 +41,12 @@ export const issueOperations = [
{
name: 'Notify',
value: 'notify',
description: 'Creates an email notification for an issue and adds it to the mail queue.',
description: 'Create an email notification for an issue and add it to the mail queue',
},
{
name: 'Status',
value: 'transitions',
description: `Returns either all transitions or a transition that can be performed by the user on an issue, based on the issue's status.`,
description: `Return either all transitions or a transition that can be performed by the user on an issue, based on the issue's status`,
},
{
name: 'Delete',

View File

@@ -113,9 +113,9 @@ export class Jira implements INodeType {
const returnData: INodePropertyOptions[] = [];
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
let endpoint = '/project/search';
let endpoint = '/api/2/project/search';
if (jiraVersion === 'server') {
endpoint = '/project';
endpoint = '/api/2/project';
}
let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET');
@@ -139,7 +139,7 @@ export class Jira implements INodeType {
const projectId = this.getCurrentNodeParameter('project');
const returnData: INodePropertyOptions[] = [];
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET');
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET');
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
if (jiraVersion === 'server') {
for (const issueType of issueTypes) {
@@ -173,7 +173,7 @@ export class Jira implements INodeType {
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const labels = await jiraSoftwareCloudApiRequest.call(this, '/label', 'GET');
const labels = await jiraSoftwareCloudApiRequest.call(this, '/api/2/label', 'GET');
for (const label of labels.values) {
const labelName = label;
@@ -192,7 +192,7 @@ export class Jira implements INodeType {
async getPriorities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const priorities = await jiraSoftwareCloudApiRequest.call(this, '/priority', 'GET');
const priorities = await jiraSoftwareCloudApiRequest.call(this, '/api/2/priority', 'GET');
for (const priority of priorities) {
const priorityName = priority.name;
@@ -213,7 +213,7 @@ export class Jira implements INodeType {
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
if (jiraVersion === 'server') {
// the interface call must bring username
const users = await jiraSoftwareCloudApiRequest.call(this, '/user/search', 'GET', {},
const users = await jiraSoftwareCloudApiRequest.call(this, '/api/2/user/search', 'GET', {},
{
username: "'",
}
@@ -228,7 +228,7 @@ export class Jira implements INodeType {
});
}
} else {
const users = await jiraSoftwareCloudApiRequest.call(this, '/users/search', 'GET');
const users = await jiraSoftwareCloudApiRequest.call(this, '/api/2/users/search', 'GET');
for (const user of users) {
const userName = user.displayName;
@@ -249,7 +249,7 @@ export class Jira implements INodeType {
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const groups = await jiraSoftwareCloudApiRequest.call(this, '/groups/picker', 'GET');
const groups = await jiraSoftwareCloudApiRequest.call(this, '/api/2/groups/picker', 'GET');
for (const group of groups.groups) {
const groupName = group.name;
@@ -269,7 +269,7 @@ export class Jira implements INodeType {
const returnData: INodePropertyOptions[] = [];
const issueKey = this.getCurrentNodeParameter('issueKey');
const transitions = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET');
const transitions = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/transitions`, 'GET');
for (const transition of transitions.transitions) {
returnData.push({
@@ -340,7 +340,7 @@ export class Jira implements INodeType {
if (additionalFields.updateHistory) {
qs.updateHistory = additionalFields.updateHistory as boolean;
}
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET', body, qs);
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body, qs);
const subtaskIssues = [];
for (const issueType of issueTypes) {
if (issueType.subtask) {
@@ -358,7 +358,7 @@ export class Jira implements INodeType {
};
}
body.fields = fields;
responseData = await jiraSoftwareCloudApiRequest.call(this, '/issue', 'POST', body);
responseData = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issue', 'POST', body);
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-put
if (operation === 'update') {
@@ -399,7 +399,7 @@ export class Jira implements INodeType {
if (updateFields.description) {
fields.description = updateFields.description as string;
}
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/issuetype', 'GET', body);
const issueTypes = await jiraSoftwareCloudApiRequest.call(this, '/api/2/issuetype', 'GET', body);
const subtaskIssues = [];
for (const issueType of issueTypes) {
if (issueType.subtask) {
@@ -419,10 +419,10 @@ export class Jira implements INodeType {
body.fields = fields;
if (updateFields.statusId) {
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'POST', { transition: { id: updateFields.statusId } });
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/transitions`, 'POST', { transition: { id: updateFields.statusId } });
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'PUT', body);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'PUT', body);
responseData = { success: true };
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get
@@ -445,7 +445,7 @@ export class Jira implements INodeType {
qs.updateHistory = additionalFields.updateHistory as string;
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'GET', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'GET', {}, qs);
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-search-post
@@ -463,11 +463,11 @@ export class Jira implements INodeType {
body.expand = options.expand as string;
}
if (returnAll) {
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'issues', `/search`, 'POST', body);
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'issues', `/api/2/search`, 'POST', body);
} else {
const limit = this.getNodeParameter('limit', i) as number;
body.maxResults = limit;
responseData = await jiraSoftwareCloudApiRequest.call(this, `/search`, 'POST', body);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/search`, 'POST', body);
responseData = responseData.issues;
}
}
@@ -476,10 +476,10 @@ export class Jira implements INodeType {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/issue/${issueKey}/changelog`, 'GET');
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values',`/api/2/issue/${issueKey}/changelog`, 'GET');
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/changelog`, 'GET', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/changelog`, 'GET', {}, qs);
responseData = responseData.values;
}
}
@@ -563,7 +563,7 @@ export class Jira implements INodeType {
body.restrict = notificationRecipientsRestrictionsJson;
}
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/notify`, 'POST', body, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/notify`, 'POST', body, qs);
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-transitions-get
@@ -579,7 +579,7 @@ export class Jira implements INodeType {
if (additionalFields.skipRemoteOnlyCondition) {
qs.skipRemoteOnlyCondition = additionalFields.skipRemoteOnlyCondition as boolean;
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}/transitions`, 'GET', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}/transitions`, 'GET', {}, qs);
responseData = responseData.transitions;
}
@@ -588,7 +588,7 @@ export class Jira implements INodeType {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const deleteSubtasks = this.getNodeParameter('deleteSubtasks', i) as boolean;
qs.deleteSubtasks = deleteSubtasks;
responseData = await jiraSoftwareCloudApiRequest.call(this, `/issue/${issueKey}`, 'DELETE', {}, qs);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'DELETE', {}, qs);
}
}
if (Array.isArray(responseData)) {

View File

@@ -0,0 +1,454 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeType,
IWebhookResponseData,
} from 'n8n-workflow';
import {
jiraSoftwareCloudApiRequest,
eventExists,
getId,
allEvents,
} from './GenericFunctions';
import * as queryString from 'querystring';
export class JiraTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Jira Trigger',
name: 'jiraTrigger',
icon: 'file:jira.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Jira events occurs.',
defaults: {
name: 'Jira Trigger',
color: '#4185f7',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'jiraSoftwareCloudApi',
required: true,
displayOptions: {
show: {
jiraVersion: [
'cloud',
],
},
},
},
{
name: 'jiraSoftwareServerApi',
required: true,
displayOptions: {
show: {
jiraVersion: [
'server',
],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Jira Version',
name: 'jiraVersion',
type: 'options',
options: [
{
name: 'Cloud',
value: 'cloud',
},
{
name: 'Server (Self Hosted)',
value: 'server',
},
],
default: 'cloud',
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: '*',
value: '*',
},
{
name: 'Board Configuration Changed',
value: 'board_configuration_changed',
},
{
name: 'Board Created',
value: 'board_created',
},
{
name: 'Board Deleted',
value: 'board_deleted',
},
{
name: 'Board Updated',
value: 'board_updated',
},
{
name: 'Comment Created',
value: 'comment_created',
},
{
name: 'Comment Deleted',
value: 'comment_deleted',
},
{
name: 'Comment Updated',
value: 'comment_updated',
},
{
name: 'Issue Created',
value: 'jira:issue_created',
},
{
name: 'Issue Deleted',
value: 'jira:issue_deleted',
},
{
name: 'Issue Link Created',
value: 'issuelink_created',
},
{
name: 'Issue Link Deleted',
value: 'issuelink_deleted',
},
{
name: 'Issue Updated',
value: 'jira:issue_updated',
},
{
name: 'Option Attachments Changed',
value: 'option_attachments_changed',
},
{
name: 'Option Issue Links Changed',
value: 'option_issuelinks_changed',
},
{
name: 'Option Subtasks Changed',
value: 'option_subtasks_changed',
},
{
name: 'Option Timetracking Changed',
value: 'option_timetracking_changed',
},
{
name: 'Option Unassigned Issues Changed',
value: 'option_unassigned_issues_changed',
},
{
name: 'Option Voting Changed',
value: 'option_voting_changed',
},
{
name: 'Option Watching Changed',
value: 'option_watching_changed',
},
{
name: 'Project Created',
value: 'project_created',
},
{
name: 'Project Deleted',
value: 'project_deleted',
},
{
name: 'Project Updated',
value: 'project_updated',
},
{
name: 'Sprint Closed',
value: 'sprint_closed',
},
{
name: 'Sprint Created',
value: 'sprint_created',
},
{
name: 'Sprint Deleted',
value: 'sprint_deleted',
},
{
name: 'Sprint Started',
value: 'sprint_started',
},
{
name: 'Sprint Updated',
value: 'sprint_updated',
},
{
name: 'User Created',
value: 'user_created',
},
{
name: 'User Deleted',
value: 'user_deleted',
},
{
name: 'User Updated',
value: 'user_updated',
},
{
name: 'Version Created',
value: 'jira:version_created',
},
{
name: 'Version Deleted',
value: 'jira:version_deleted',
},
{
name: 'Version Moved',
value: 'jira:version_moved',
},
{
name: 'Version Released',
value: 'jira:version_released',
},
{
name: 'Version Unreleased',
value: 'jira:version_unreleased',
},
{
name: 'Version Updated',
value: 'jira:version_updated',
},
{
name: 'Worklog Created',
value: 'worklog_created',
},
{
name: 'Worklog Deleted',
value: 'worklog_deleted',
},
{
name: 'Worklog Updated',
value: 'worklog_updated',
},
],
required: true,
default: [],
description: 'The events to listen to.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Exclude Body',
name: 'excludeBody',
type: 'boolean',
default: false,
description: 'Request with empty body will be sent to the URL. Leave unchecked if you want to receive JSON.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
placeholder: 'Project = JRA AND resolution = Fixed',
description: 'You can specify a JQL query to send only events triggered by matching issues. The JQL filter only applies to events under the Issue and Comment columns.',
},
{
displayName: 'Include Fields',
name: 'includeFields',
type: 'multiOptions',
options: [
{
name: 'Attachment ID',
value: 'attachment.id'
},
{
name: 'Board ID',
value: 'board.id'
},
{
name: 'Comment ID',
value: 'comment.id'
},
{
name: 'Issue ID',
value: 'issue.id'
},
{
name: 'Merge Version ID',
value: 'mergeVersion.id'
},
{
name: 'Modified User Account ID',
value: 'modifiedUser.accountId'
},
{
name: 'Modified User Key',
value: 'modifiedUser.key'
},
{
name: 'Modified User Name',
value: 'modifiedUser.name'
},
{
name: 'Project ID',
value: 'project.id'
},
{
name: 'Project Key',
value: 'project.key'
},
{
name: 'Propery Key',
value: 'property.key'
},
{
name: 'Sprint ID',
value: 'sprint.id'
},
{
name: 'Version ID',
value: 'version.id'
},
{
name: 'Worklog ID',
value: 'worklog.id'
},
],
default: [],
},
],
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const events = this.getNodeParameter('events') as string[];
const endpoint = `/webhooks/1.0/webhook`;
const webhooks = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET', {});
for (const webhook of webhooks) {
if (webhook.url === webhookUrl && eventExists(events, webhook.events)) {
webhookData.webhookId = getId(webhook.self);
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
let events = this.getNodeParameter('events', []) as string[];
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
const endpoint = `/webhooks/1.0/webhook`;
const webhookData = this.getWorkflowStaticData('node');
if (events.includes('*')) {
events = allEvents;
}
const body = {
name: `n8n-webhook:${webhookUrl}`,
url: webhookUrl,
events,
filters: {
},
excludeBody: false,
};
if (additionalFields.filter) {
body.filters = {
'issue-related-events-section': additionalFields.filter,
};
}
if (additionalFields.excludeBody) {
body.excludeBody = additionalFields.excludeBody as boolean;
}
if (additionalFields.includeFields) {
const parameters: IDataObject = {};
for (const field of additionalFields.includeFields as string[]) {
parameters[field] = '${' + field + '}';
}
body.url = `${body.url}?${queryString.unescape(queryString.stringify(parameters))}`;
}
const responseData = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'POST', body);
webhookData.webhookId = getId(responseData.self);
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/1.0/webhook/${webhookData.webhookId}`;
const body = {};
try {
await jiraSoftwareCloudApiRequest.call(this, endpoint, 'DELETE', body);
} catch (e) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
const queryData = this.getQueryData();
Object.assign(bodyData, queryData);
return {
workflowData: [
this.helpers.returnJsonArray(bodyData)
],
};
}
}

View File

@@ -49,7 +49,6 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio
const datacenter = (credentials.apiKey as string).split('-').pop();
options.url = `https://${datacenter}.${host}${endpoint}`;
return await this.helpers.request!(options);
} else {
const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject;
@@ -58,7 +57,7 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio
options.url = `${api_endpoint}/3.0${endpoint}`;
//@ts-ignore
return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, 'Bearer');
return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, { tokenType: 'Bearer' });
}
} catch (error) {
if (error.respose && error.response.body && error.response.body.detail) {

View File

@@ -47,6 +47,7 @@ interface ICreateMemberBody {
timestamp_opt?: string;
tags?: string[];
merge_fields?: IDataObject;
interests?: IDataObject;
}
export class Mailchimp implements INodeType {
@@ -112,6 +113,10 @@ export class Mailchimp implements INodeType {
name: 'resource',
type: 'options',
options: [
{
name: 'List Group',
value: 'listGroup',
},
{
name: 'Member',
value: 'member',
@@ -194,6 +199,28 @@ export class Mailchimp implements INodeType {
default: 'create',
description: 'The operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
required: true,
displayOptions: {
show: {
resource: [
'listGroup',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all groups',
},
],
default: 'getAll',
description: 'The operation to perform.',
},
/* -------------------------------------------------------------------------- */
/* member:create */
/* -------------------------------------------------------------------------- */
@@ -256,27 +283,22 @@ export class Mailchimp implements INodeType {
{
name: 'Subscribed',
value: 'subscribed',
description: '',
},
{
name: 'Unsubscribed',
value: 'unsubscribed',
description: '',
},
{
name: 'Cleaned',
value: 'cleaned',
description: '',
},
{
name: 'Pending',
value: 'pending',
description: '',
},
{
name: 'Transactional',
value: 'transactional',
description: '',
},
],
default: '',
@@ -287,7 +309,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource:[
@@ -324,12 +345,10 @@ export class Mailchimp implements INodeType {
{
name: 'HTML',
value: 'html',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
@@ -496,7 +515,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
@@ -519,7 +537,86 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
jsonParameters: [
true,
],
},
},
},
{
displayName: 'Interest Groups',
name: 'groupsUi',
placeholder: 'Add Interest Group',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource:[
'member'
],
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'groupsValues',
displayName: 'Group',
typeOptions: {
multipleValueButtonText: 'Add Interest Group',
},
values: [
{
displayName: 'Category ID',
name: 'categoryId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Category Field ID',
name: 'categoryFieldId',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
},
],
},
],
},
{
displayName: 'Interest Groups',
name: 'groupJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource:[
@@ -772,12 +869,10 @@ export class Mailchimp implements INodeType {
{
name: 'HTML',
value: 'html',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
@@ -791,27 +886,22 @@ export class Mailchimp implements INodeType {
{
name: 'Subscribed',
value: 'subscribed',
description: '',
},
{
name: 'Unsubscribed',
value: 'unsubscribed',
description: '',
},
{
name: 'Cleaned',
value: 'cleaned',
description: '',
},
{
name: 'Pending',
value: 'pending',
description: '',
},
{
name: 'Transactional',
value: 'transactional',
description: '',
},
],
default: '',
@@ -874,7 +964,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource:[
@@ -911,17 +1000,73 @@ export class Mailchimp implements INodeType {
{
name: 'HTML',
value: 'html',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
description: 'Type of email this member asked to get',
},
{
displayName: 'Interest Groups',
name: 'groupsUi',
placeholder: 'Add Interest Group',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
'/resource':[
'member'
],
'/operation':[
'update',
],
'/jsonParameters': [
false,
],
},
},
options: [
{
name: 'groupsValues',
displayName: 'Group',
typeOptions: {
multipleValueButtonText: 'Add Interest Group',
},
values: [
{
displayName: 'Category ID',
name: 'categoryId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Category Field ID',
name: 'categoryFieldId',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
},
],
},
],
},
{
displayName: 'Language',
name: 'language',
@@ -1024,27 +1169,22 @@ export class Mailchimp implements INodeType {
{
name: 'Subscribed',
value: 'subscribed',
description: '',
},
{
name: 'Unsubscribed',
value: 'unsubscribed',
description: '',
},
{
name: 'Cleaned',
value: 'cleaned',
description: '',
},
{
name: 'Pending',
value: 'pending',
description: '',
},
{
name: 'Transactional',
value: 'transactional',
description: '',
},
],
default: '',
@@ -1119,7 +1259,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
@@ -1142,7 +1281,28 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'update',
],
jsonParameters: [
true,
],
},
},
},
{
displayName: 'Interest Groups',
name: 'groupJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource:[
@@ -1250,6 +1410,96 @@ export class Mailchimp implements INodeType {
},
],
},
/* -------------------------------------------------------------------------- */
/* member:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'List',
name: 'list',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: '',
options: [],
required: true,
description: 'List of lists',
},
{
displayName: 'Group Category',
name: 'groupCategory',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroupCategories',
loadOptionsDependsOn: [
'list',
],
},
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: '',
options: [],
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'listGroup',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 1000,
},
default: 500,
description: 'How many results to return.',
},
],
};
@@ -1261,7 +1511,7 @@ export class Mailchimp implements INodeType {
// select them easily
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { lists } = await mailchimpApiRequest.call(this, '/lists', 'GET');
const lists = await mailchimpApiRequestAllItems.call(this, '/lists', 'GET', 'lists');
for (const list of lists) {
const listName = list.name;
const listId = list.id;
@@ -1289,6 +1539,23 @@ export class Mailchimp implements INodeType {
}
return returnData;
},
// Get all the interest fields to display them to user so that he can
// select them easily
async getGroupCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const listId = this.getCurrentNodeParameter('list');
const { categories } = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories`, 'GET');
for (const category of categories) {
const categoryName = category.title;
const categoryId = category.id;
returnData.push({
name: categoryName,
value: categoryId,
});
}
return returnData;
},
}
};
@@ -1302,6 +1569,22 @@ export class Mailchimp implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'listGroup') {
//https://mailchimp.com/developer/reference/lists/interest-categories/#get_/lists/-list_id-/interest-categories/-interest_category_id-
if (operation === 'getAll') {
const listId = this.getNodeParameter('list', i) as string;
const categoryId = this.getNodeParameter('groupCategory', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === true) {
responseData = await mailchimpApiRequestAllItems.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', 'interests', {}, qs);
} else {
qs.count = this.getNodeParameter('limit', i) as number;
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', {}, qs);
responseData = responseData.interests;
}
}
}
if (resource === 'member') {
//https://mailchimp.com/developer/reference/lists/list-members/#post_/lists/-list_id-/members
if (operation === 'create') {
@@ -1363,15 +1646,29 @@ export class Mailchimp implements INodeType {
}
body.merge_fields = mergeFields;
}
const groupsValues = (this.getNodeParameter('groupsUi', i) as IDataObject).groupsValues as IDataObject[];
if (groupsValues) {
const groups = {};
for (let i = 0; i < groupsValues.length; i++) {
// @ts-ignore
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
}
body.interests = groups;
}
} else {
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
if (locationJson) {
body.location = locationJson;
}
if (mergeFieldsJson) {
body.merge_fields = mergeFieldsJson;
}
if (groupJson) {
body.interests = groupJson;
}
}
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body);
}
@@ -1504,15 +1801,31 @@ export class Mailchimp implements INodeType {
body.merge_fields = mergeFields;
}
}
if (updateFields.groupsUi) {
const groupsValues = (updateFields.groupsUi as IDataObject).groupsValues as IDataObject[];
if (groupsValues) {
const groups = {};
for (let i = 0; i < groupsValues.length; i++) {
// @ts-ignore
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
}
body.interests = groups;
}
}
} else {
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
if (locationJson) {
body.location = locationJson;
}
if (mergeFieldsJson) {
body.merge_fields = mergeFieldsJson;
}
if (groupJson) {
body.interests = groupJson;
}
}
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'PUT', body);
}

View File

@@ -1,19 +1,19 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
} from 'n8n-core';
import {
import {
IDataObject,
INodeTypeDescription,
INodeType,
IWebhookResponseData,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
} from 'n8n-workflow';
import {
mailchimpApiRequest,
} from './GenericFunctions';
} from './GenericFunctions';
export class MailchimpTrigger implements INodeType {
description: INodeTypeDescription = {
@@ -24,8 +24,8 @@ export class MailchimpTrigger implements INodeType {
version: 1,
description: 'Handle Mailchimp events via webhooks',
defaults: {
name: 'Mailchimp Trigger',
color: '#32325d',
name: 'Mailchimp Trigger',
color: '#32325d',
},
inputs: [],
outputs: ['main'],
@@ -285,8 +285,8 @@ export class MailchimpTrigger implements INodeType {
}
// @ts-ignore
if (!webhookData.events.includes(req.body.type)
// @ts-ignore
&& !webhookData.sources.includes(req.body.type)) {
// @ts-ignore
&& !webhookData.sources.includes(req.body.type)) {
return {};
}
return {

View File

@@ -62,7 +62,7 @@ export class Mattermost implements INodeType {
},
],
default: 'message',
description: 'The resource to operate on.',
description: 'The resource to operate on',
},
@@ -95,22 +95,22 @@ export class Mattermost implements INodeType {
{
name: 'Delete',
value: 'delete',
description: 'Soft-deletes a channel',
description: 'Soft delete a channel',
},
{
name: 'Member',
value: 'members',
description: 'Get a page of members for a channel.',
description: 'Get a page of members for a channel',
},
{
name: 'Restore',
value: 'restore',
description: 'Restores a soft-deleted channel',
description: 'Restores a soft deleted channel',
},
{
name: 'Statistics',
value: 'statistics',
description: 'Get statistics for a channel.',
description: 'Get statistics for a channel',
},
],
default: 'create',
@@ -131,7 +131,7 @@ export class Mattermost implements INodeType {
{
name: 'Delete',
value: 'delete',
description: 'Soft deletes a post, by marking the post as deleted in the database.',
description: 'Soft delete a post, by marking the post as deleted in the database',
},
{
name: 'Post',
@@ -140,7 +140,7 @@ export class Mattermost implements INodeType {
},
],
default: 'post',
description: 'The operation to perform.',
description: 'The operation to perform',
},
@@ -191,7 +191,7 @@ export class Mattermost implements INodeType {
},
},
required: true,
description: 'The non-unique UI name for the channel.',
description: 'The non-unique UI name for the channel',
},
{
displayName: 'Name',
@@ -210,7 +210,7 @@ export class Mattermost implements INodeType {
},
},
required: true,
description: 'The unique handle for the channel, will be present in the channel URL.',
description: 'The unique handle for the channel, will be present in the channel URL',
},
{
displayName: 'Type',
@@ -264,7 +264,7 @@ export class Mattermost implements INodeType {
],
},
},
description: 'The ID of the channel to soft-delete.',
description: 'The ID of the channel to soft delete',
},
// ----------------------------------

View File

@@ -0,0 +1,54 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function mediumApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
method,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Accept-Charset': 'utf-8',
},
qs: query,
uri: uri || `https://api.medium.com/v1${endpoint}`,
body,
json: true,
};
try {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('mediumApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
return await this.helpers.request!(options);
}
else {
return await this.helpers.requestOAuth2!.call(this, 'mediumOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
throw new Error('The Medium credentials are not valid!');
}
throw error;
}
}

View File

@@ -0,0 +1,577 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodePropertyOptions,
INodeTypeDescription,
} from 'n8n-workflow';
import {
mediumApiRequest,
} from './GenericFunctions';
export class Medium implements INodeType {
description: INodeTypeDescription = {
displayName: 'Medium',
name: 'medium',
group: ['output'],
icon: 'file:medium.png',
version: 1,
description: 'Consume Medium API',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
defaults: {
name: 'Medium',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mediumApi',
required: true,
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'mediumOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'The method of authentication.',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Post',
value: 'post',
},
{
name: 'Publication',
value: 'publication',
},
],
default: 'post',
description: 'Resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'post',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a post',
},
],
default: 'create',
description: 'The operation to perform.',
},
// ----------------------------------
// post:create
// ----------------------------------
{
displayName: 'Publication',
name: 'publication',
type: 'boolean',
displayOptions: {
show: {
resource: [
'post',
],
operation: [
'create',
],
},
},
default: false,
description: 'Are you posting for a publication?'
},
{
displayName: 'Publication ID',
name: 'publicationId',
type: 'options',
displayOptions: {
show: {
resource: [
'post',
],
operation: [
'create',
],
publication: [
true,
],
},
},
typeOptions: {
loadOptionsMethod: 'getPublications',
},
default: '',
description: 'Publication ids',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
placeholder: 'My Open Source Contribution',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'post',
],
},
},
description: 'Title of the post. Max Length : 100 characters',
},
{
displayName: 'Content Format',
name: 'contentFormat',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'post',
],
},
},
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Markdown',
value: 'markdown',
},
],
description: 'The format of the content to be posted.',
},
{
displayName: 'Content',
name: 'content',
type: 'string',
default: '',
placeholder: 'My open source contribution',
required: true,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'post',
],
},
},
description: 'The body of the post, in a valid semantic HTML fragment, or Markdown.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'post',
],
},
},
default: {},
options: [
{
displayName: 'Canonical Url',
name: 'canonicalUrl',
type: 'string',
default: '',
description: 'The original home of this content, if it was originally published elsewhere.',
},
{
displayName: 'License',
name: 'license',
type: 'options',
default: 'all-rights-reserved',
options: [
{
name: 'all-rights-reserved',
value: 'all-rights-reserved',
},
{
name: 'cc-40-by',
value: 'cc-40-by',
},
{
name: 'cc-40-by-nc',
value: 'cc-40-by-nc',
},
{
name: 'cc-40-by-nc-nd',
value: 'cc-40-by-nc-nd',
},
{
name: 'cc-40-by-nc-sa',
value: 'cc-40-by-nc-sa',
},
{
name: 'cc-40-by-nd',
value: 'cc-40-by-nd',
},
{
name: 'cc-40-by-sa',
value: 'cc-40-by-sa',
},
{
name: 'cc-40-zero',
value: 'cc-40-zero',
},
{
name: 'public-domain',
value: 'public-domain',
},
],
description: 'License of the post.',
},
{
displayName: 'Notify Followers',
name: 'notifyFollowers',
type: 'boolean',
default: false,
description: 'Whether to notify followers that the user has published.',
},
{
displayName: 'Publish Status',
name: 'publishStatus',
default: 'public',
type: 'options',
options: [
{
name: 'Public',
value: 'public',
},
{
name: 'Draft',
value: 'draft',
},
{
name: 'Unlisted',
value: 'unlisted',
},
],
description: 'The status of the post.',
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
placeholder: 'open-source,mlh,fellowship',
description: 'Comma-separated strings to be used as tags for post classification. Max allowed tags: 5. Max tag length: 25 characters.',
},
],
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'publication',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all publications',
},
],
default: 'publication',
description: 'The operation to perform.',
},
// ----------------------------------
// publication:getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'publication',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'publication',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 200,
},
default: 100,
description: 'How many results to return.',
},
],
};
methods = {
loadOptions: {
// Get all the available publications to display them to user so that he can
// select them easily
async getPublications(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
//Get the User Id
const user = await mediumApiRequest.call(
this,
'GET',
`/me`,
);
const userId = user.data.id;
//Get all publications of that user
const publications = await mediumApiRequest.call(
this,
'GET',
`/users/${userId}/publications`,
);
const publicationsList = publications.data;
for (const publication of publicationsList) {
const publicationName = publication.name;
const publicationId = publication.id;
returnData.push({
name: publicationName,
value: publicationId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let operation: string;
let resource: string;
// For POST
let bodyRequest: IDataObject;
// For Query string
let qs: IDataObject;
let responseData;
for (let i = 0; i < items.length; i++) {
qs = {};
resource = this.getNodeParameter('resource', i) as string;
operation = this.getNodeParameter('operation', i) as string;
if (resource === 'post') {
//https://github.com/Medium/medium-api-docs
if (operation === 'create') {
// ----------------------------------
// post:create
// ----------------------------------
const title = this.getNodeParameter('title', i) as string;
const contentFormat = this.getNodeParameter('contentFormat', i) as string;
const content = this.getNodeParameter('content', i) as string;
bodyRequest = {
tags: [],
title,
contentFormat,
content,
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.tags) {
const tags = additionalFields.tags as string;
bodyRequest.tags = tags.split(',').map(name => {
const returnValue = name.trim();
if (returnValue.length > 25) {
throw new Error(`The tag "${returnValue}" is to long. Maximum lenght of a tag is 25 characters.`);
}
return returnValue;
});
if ((bodyRequest.tags as string[]).length > 5) {
throw new Error('To many tags got used. Maximum 5 can be set.');
}
}
if (additionalFields.canonicalUrl) {
bodyRequest.canonicalUrl = additionalFields.canonicalUrl as string;
}
if (additionalFields.publishStatus) {
bodyRequest.publishStatus = additionalFields.publishStatus as string;
}
if (additionalFields.license) {
bodyRequest.license = additionalFields.license as string;
}
if (additionalFields.notifyFollowers) {
bodyRequest.notifyFollowers = additionalFields.notifyFollowers as string;
}
const underPublication = this.getNodeParameter('publication', i) as boolean;
// if user wants to publish it under a specific publication
if (underPublication) {
const publicationId = this.getNodeParameter('publicationId', i) as number;
responseData = await mediumApiRequest.call(
this,
'POST',
`/publications/${publicationId}/posts`,
bodyRequest,
qs
);
}
else {
const responseAuthorId = await mediumApiRequest.call(
this,
'GET',
'/me',
{},
qs
);
const authorId = responseAuthorId.data.id;
responseData = await mediumApiRequest.call(
this,
'POST',
`/users/${authorId}/posts`,
bodyRequest,
qs
);
responseData = responseData.data;
}
}
}
if (resource === 'publication') {
//https://github.com/Medium/medium-api-docs#32-publications
if (operation === 'getAll') {
// ----------------------------------
// publication:getAll
// ----------------------------------
const returnAll = this.getNodeParameter('returnAll', i) as string;
const user = await mediumApiRequest.call(
this,
'GET',
`/me`,
);
const userId = user.data.id;
//Get all publications of that user
responseData = await mediumApiRequest.call(
this,
'GET',
`/users/${userId}/publications`,
);
responseData = responseData.data;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,144 @@
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
import { ITables } from './TableInterface';
/**
* Returns a copy of the item which only contains the json data and
* of that only the defined properties
*
* @param {INodeExecutionData} item The item to copy
* @param {string[]} properties The properties it should include
* @returns
*/
export function copyInputItem(
item: INodeExecutionData,
properties: string[],
): IDataObject {
// Prepare the data to insert and copy it to be returned
const newItem: IDataObject = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
}
/**
* Creates an ITables with the columns for the operations
*
* @param {INodeExecutionData[]} items The items to extract the tables/columns for
* @param {function} getNodeParam getter for the Node's Parameters
* @returns {ITables} {tableName: {colNames: [items]}};
*/
export function createTableStruct(
getNodeParam: Function,
items: INodeExecutionData[],
additionalProperties: string[] = [],
keyName?: string,
): ITables {
return items.reduce((tables, item, index) => {
const table = getNodeParam('table', index) as string;
const columnString = getNodeParam('columns', index) as string;
const columns = columnString.split(',').map(column => column.trim());
const itemCopy = copyInputItem(item, columns.concat(additionalProperties));
const keyParam = keyName
? (getNodeParam(keyName, index) as string)
: undefined;
if (tables[table] === undefined) {
tables[table] = {};
}
if (tables[table][columnString] === undefined) {
tables[table][columnString] = [];
}
if (keyName) {
itemCopy[keyName] = keyParam;
}
tables[table][columnString].push(itemCopy);
return tables;
}, {} as ITables);
}
/**
* Executes a queue of queries on given ITables.
*
* @param {ITables} tables The ITables to be processed.
* @param {function} buildQueryQueue function that builds the queue of promises
* @returns {Promise}
*/
export function executeQueryQueue(
tables: ITables,
buildQueryQueue: Function,
): Promise<any[]> { // tslint:disable-line:no-any
return Promise.all(
Object.keys(tables).map(table => {
const columnsResults = Object.keys(tables[table]).map(columnString => {
return Promise.all(
buildQueryQueue({
table,
columnString,
items: tables[table][columnString],
}),
);
});
return Promise.all(columnsResults);
}),
);
}
/**
* Extracts the values from the item for INSERT
*
* @param {IDataObject} item The item to extract
* @returns {string} (Val1, Val2, ...)
*/
export function extractValues(item: IDataObject): string {
return `(${Object.values(item as any) // tslint:disable-line:no-any
.map(val => (typeof val === 'string' ? `'${val}'` : val)) // maybe other types such as dates have to be handled as well
.join(',')})`;
}
/**
* Extracts the SET from the item for UPDATE
*
* @param {IDataObject} item The item to extract from
* @param {string[]} columns The columns to update
* @returns {string} col1 = val1, col2 = val2
*/
export function extractUpdateSet(item: IDataObject, columns: string[]): string {
return columns
.map(
column =>
`${column} = ${
typeof item[column] === 'string' ? `'${item[column]}'` : item[column]
}`,
)
.join(',');
}
/**
* Extracts the WHERE condition from the item for UPDATE
*
* @param {IDataObject} item The item to extract from
* @param {string} key The column name to build the condition with
* @returns {string} id = '123'
*/
export function extractUpdateCondition(item: IDataObject, key: string): string {
return `${key} = ${
typeof item[key] === 'string' ? `'${item[key]}'` : item[key]
}`;
}
/**
* Extracts the WHERE condition from the items for DELETE
*
* @param {IDataObject[]} items The items to extract the values from
* @param {string} key The column name to extract the value from for the delete condition
* @returns {string} (Val1, Val2, ...)
*/
export function extractDeleteValues(items: IDataObject[], key: string): string {
return `(${items
.map(item => (typeof item[key] === 'string' ? `'${item[key]}'` : item[key]))
.join(',')})`;
}

View File

@@ -0,0 +1,394 @@
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { chunk, flatten } from '../../utils/utilities';
import * as mssql from 'mssql';
import { ITables } from './TableInterface';
import {
copyInputItem,
createTableStruct,
executeQueryQueue,
extractDeleteValues,
extractUpdateCondition,
extractUpdateSet,
extractValues,
} from './GenericFunctions';
export class MicrosoftSql implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft SQL',
name: 'microsoftSql',
icon: 'file:mssql.png',
group: ['input'],
version: 1,
description: 'Gets, add and update data in Microsoft SQL.',
defaults: {
name: 'Microsoft SQL',
color: '#1d4bab',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'microsoftSql',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Execute an SQL query',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database',
},
{
name: 'Update',
value: 'update',
description: 'Update rows in database',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete rows in database',
},
],
default: 'insert',
description: 'The operation to perform.',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: ['executeQuery'],
},
},
default: '',
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute.',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to.',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
placeholder: 'id,name,description',
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'Name of the table in which to update data in',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: 'id',
required: true,
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description:
'Comma separated list of the properties which should used as columns for rows to update.',
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: '',
required: true,
description: 'Name of the table in which to delete data.',
},
{
displayName: 'Delete Key',
name: 'deleteKey',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: 'id',
required: true,
description:
'Name of the property which decides which rows in the database should be deleted. Normally that would be "id".',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('microsoftSql');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const config = {
server: credentials.server as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
domain: credentials.domain ? (credentials.domain as string) : undefined,
};
const pool = new mssql.ConnectionPool(config);
await pool.connect();
let returnItems = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
try {
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
const rawQuery = this.getNodeParameter('query', 0) as string;
const queryResult = await pool.request().query(rawQuery);
const result =
queryResult.recordsets.length > 1
? flatten(queryResult.recordsets)
: queryResult.recordsets[0];
returnItems = this.helpers.returnJsonArray(result as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const tables = createTableStruct(this.getNodeParameter, items);
await executeQueryQueue(
tables,
({
table,
columnString,
items,
}: {
table: string;
columnString: string;
items: IDataObject[];
}): Array<Promise<object>> => {
return chunk(items, 1000).map(insertValues => {
const values = insertValues
.map((item: IDataObject) => extractValues(item))
.join(',');
return pool
.request()
.query(
`INSERT INTO ${table}(${columnString}) VALUES ${values};`,
);
});
},
);
returnItems = items;
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const updateKeys = items.map(
(item, index) => this.getNodeParameter('updateKey', index) as string,
);
const tables = createTableStruct(
this.getNodeParameter,
items,
['updateKey'].concat(updateKeys),
'updateKey',
);
await executeQueryQueue(
tables,
({
table,
columnString,
items,
}: {
table: string;
columnString: string;
items: IDataObject[];
}): Array<Promise<object>> => {
return items.map(item => {
const columns = columnString
.split(',')
.map(column => column.trim());
const setValues = extractUpdateSet(item, columns);
const condition = extractUpdateCondition(
item,
item.updateKey as string,
);
return pool
.request()
.query(`UPDATE ${table} SET ${setValues} WHERE ${condition};`);
});
},
);
returnItems = items;
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const tables = items.reduce((tables, item, index) => {
const table = this.getNodeParameter('table', index) as string;
const deleteKey = this.getNodeParameter('deleteKey', index) as string;
if (tables[table] === undefined) {
tables[table] = {};
}
if (tables[table][deleteKey] === undefined) {
tables[table][deleteKey] = [];
}
tables[table][deleteKey].push(item);
return tables;
}, {} as ITables);
const queriesResults = await Promise.all(
Object.keys(tables).map(table => {
const deleteKeyResults = Object.keys(tables[table]).map(
deleteKey => {
const deleteItemsList = chunk(
tables[table][deleteKey].map(item =>
copyInputItem(item as INodeExecutionData, [deleteKey]),
),
1000,
);
const queryQueue = deleteItemsList.map(deleteValues => {
return pool
.request()
.query(
`DELETE FROM ${table} WHERE ${deleteKey} IN ${extractDeleteValues(
deleteValues,
deleteKey,
)};`,
);
});
return Promise.all(queryQueue);
},
);
return Promise.all(deleteKeyResults);
}),
);
const rowsDeleted = flatten(queriesResults).reduce(
(acc: number, resp: mssql.IResult<object>): number =>
(acc += resp.rowsAffected.reduce((sum, val) => (sum += val))),
0,
);
returnItems = this.helpers.returnJsonArray({
rowsDeleted,
} as IDataObject);
} else {
await pool.close();
throw new Error(`The operation "${operation}" is not supported!`);
}
} catch (err) {
if (this.continueOnFail() === true) {
returnItems = items;
} else {
await pool.close();
throw err;
}
}
// Close the connection
await pool.close();
return this.prepareOutputData(returnItems);
}
}

View File

@@ -0,0 +1,7 @@
import { IDataObject } from 'n8n-workflow';
export interface ITables {
[key: string]: {
[key: string]: IDataObject[];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -322,7 +322,7 @@ export class MondayCom implements INodeType {
if (returnAll === true) {
responseData = await mondayComApiRequestAllItems.call(this, 'data.boards', body);
} else {
body.variables.limit = this.getNodeParameter('limit', i) as number,
body.variables.limit = this.getNodeParameter('limit', i) as number;
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards;
}

View File

@@ -318,7 +318,7 @@ export class MoveBinaryData implements INodeType {
let options: IDataObject;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = items[itemIndex];
options = this.getNodeParameter('options', 0, {}) as IDataObject;
options = this.getNodeParameter('options', itemIndex, {}) as IDataObject;
// Copy the whole JSON data as data on any level can be renamed
newItem = {

View File

@@ -13,7 +13,7 @@ import {
export class Msg91 implements INodeType {
description: INodeTypeDescription = {
displayName: 'Msg91',
displayName: 'MSG91',
name: 'msg91',
icon: 'file:msg91.png',
group: ['transform'],
@@ -68,7 +68,7 @@ export class Msg91 implements INodeType {
description: 'The operation to perform.',
},
{
displayName: 'From',
displayName: 'Sender ID',
name: 'from',
type: 'string',
default: '',

View File

@@ -17,7 +17,7 @@ export class MySql implements INodeType {
icon: 'file:mysql.png',
group: ['input'],
version: 1,
description: 'Gets, add and update data in MySQL.',
description: 'Get, add and update data in MySQL.',
defaults: {
name: 'MySQL',
color: '#4279a2',
@@ -39,7 +39,7 @@ export class MySql implements INodeType {
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Executes a SQL query.',
description: 'Execute an SQL query.',
},
{
name: 'Insert',
@@ -49,7 +49,7 @@ export class MySql implements INodeType {
{
name: 'Update',
value: 'update',
description: 'Updates rows in database.',
description: 'Update rows in database.',
},
],
default: 'insert',

View File

@@ -0,0 +1,63 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
/**
* Make an API request to NextCloud
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function nextCloudApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object | string | Buffer, headers?: object, encoding?: null | undefined, query?: object): Promise<any> { // tslint:disable-line:no-any
const options : OptionsWithUri = {
headers,
method,
body,
qs: {},
uri: '',
json: false,
};
if (encoding === null) {
options.encoding = null;
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('nextCloudApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.auth = {
user: credentials.user as string,
pass: credentials.password as string,
};
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
return await this.helpers.request(options);
} else {
const credentials = this.getCredentials('nextCloudOAuth2Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
return await this.helpers.requestOAuth2!.call(this, 'nextCloudOAuth2Api', options);
}
} catch (error) {
throw new Error(`NextCloud Error. Status Code: ${error.statusCode}. Message: ${error.message}`);
}
}

View File

@@ -2,6 +2,7 @@ import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@@ -9,9 +10,13 @@ import {
INodeType,
} from 'n8n-workflow';
import { parseString } from 'xml2js';
import { OptionsWithUri } from 'request';
import {
parseString,
} from 'xml2js';
import {
nextCloudApiRequest,
} from './GenericFunctions';
export class NextCloud implements INodeType {
description: INodeTypeDescription = {
@@ -24,7 +29,7 @@ export class NextCloud implements INodeType {
description: 'Access data on NextCloud',
defaults: {
name: 'NextCloud',
color: '#22BB44',
color: '#1cafff',
},
inputs: ['main'],
outputs: ['main'],
@@ -32,9 +37,44 @@ export class NextCloud implements INodeType {
{
name: 'nextCloudApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'nextCloudOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'The resource to operate on.',
},
{
displayName: 'Resource',
name: 'resource',
@@ -446,7 +486,14 @@ export class NextCloud implements INodeType {
const items = this.getInputData().slice();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('nextCloudApi');
const authenticationMethod = this.getNodeParameter('authentication', 0);
let credentials;
if (authenticationMethod === 'accessToken') {
credentials = this.getCredentials('nextCloudApi');
} else {
credentials = this.getCredentials('nextCloudOAuth2Api');
}
if (credentials === undefined) {
throw new Error('No credentials got returned!');
@@ -562,26 +609,14 @@ export class NextCloud implements INodeType {
webDavUrl = webDavUrl.slice(0, -1);
}
const options: OptionsWithUri = {
auth: {
user: credentials.user as string,
pass: credentials.password as string,
},
headers,
method: requestMethod,
body,
qs: {},
uri: `${credentials.webDavUrl}/${encodeURI(endpoint)}`,
json: false,
};
let encoding = undefined;
if (resource === 'file' && operation === 'download') {
// Return the data as a buffer
options.encoding = null;
encoding = null;
}
try {
responseData = await this.helpers.request(options);
responseData = await nextCloudApiRequest.call(this, requestMethod, endpoint, body, headers, encoding);
} catch (error) {
if (this.continueOnFail() === true) {
returnData.push({ error });

View File

@@ -0,0 +1,66 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function philipsHueApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://api.meethue.com${resource}`,
json: true
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(qs).length === 0) {
delete options.qs;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'philipsHueOAuth2Api', options, { tokenType: 'Bearer' });
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
const errorMessage = error.response.body.error.description;
// Try to return the error prettier
throw new Error(
`Philip Hue error response [${error.statusCode}]: ${errorMessage}`
);
}
throw error;
}
}
export async function getUser(this: IExecuteFunctions | ILoadOptionsFunctions): Promise<any> { // tslint:disable-line:no-any
const { whitelist } = await philipsHueApiRequest.call(this, 'GET', '/bridge/0/config', {}, {});
//check if there is a n8n user
for (const user of Object.keys(whitelist)) {
if (whitelist[user].name === 'n8n') {
return user;
}
}
// n8n user was not fount then create the user
await philipsHueApiRequest.call(this, 'PUT', '/bridge/0/config', { linkbutton: true });
const { success } = await philipsHueApiRequest.call(this, 'POST', '/bridge', { devicetype: 'n8n' });
return success.username;
}

View File

@@ -0,0 +1,345 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const lightOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'light',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a light',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a light',
},
{
name: 'Get All',
value: 'getAll',
description: 'Retrieve all lights',
},
{
name: 'Update',
value: 'update',
description: 'Update an light',
}
],
default: 'update',
description: 'The operation to perform.'
}
] as INodeProperties[];
export const lightFields = [
/* -------------------------------------------------------------------------- */
/* light:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Light ID',
name: 'lightId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'light',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* light:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'light',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'light',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* light:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Light ID',
name: 'lightId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'light',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* light:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Light ID',
name: 'lightId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLights',
},
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'light',
],
},
},
default: '',
},
{
displayName: 'On',
name: 'on',
type: 'boolean',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'light',
],
},
},
default: true,
description: 'On/Off state of the light.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'light',
],
operation: [
'update',
],
},
},
default: {},
options: [
{
displayName: 'Alert Effect',
name: 'alert',
type: 'options',
options: [
{
name: 'None',
value: 'none',
description: 'The light is not performing an alert effect',
},
{
name: 'Select',
value: 'select',
description: 'The light is performing one breathe cycle.',
},
{
name: 'LSelect',
value: 'lselect',
description: 'The light is performing breathe cycles for 15 seconds or until an "alert": "none" command is received',
},
],
default: '',
description: 'The alert effect, is a temporary change to the bulbs state',
},
{
displayName: 'Brightness',
name: 'bri',
type: 'number',
typeOptions: {
minValue: 1,
maxValue: 254,
},
default: 100,
description: 'The brightness value to set the light to.Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum).',
},
{
displayName: 'Brightness Increments',
name: 'bri_inc',
type: 'number',
typeOptions: {
minValue: -254,
maxValue: 254,
},
default: 0,
description: 'Increments or decrements the value of the brightness. This value is ignored if the Brightness attribute is provided.',
},
{
displayName: 'Color Temperature',
name: 'ct',
type: 'number',
default: 0,
description: 'The Mired color temperature of the light. 2012 connected lights are capable of 153 (6500K) to 500 (2000K).',
},
{
displayName: 'Color Temperature Increments',
name: 'ct_inc',
type: 'number',
typeOptions: {
minValue: -65534,
maxValue: 65534,
},
default: 0,
description: 'Increments or decrements the value of the ct. ct_inc is ignored if the ct attribute is provided',
},
{
displayName: 'Coordinates',
name: 'xy',
type: 'string',
default: '',
placeholder: '0.64394,0.33069',
description: `The x and y coordinates of a color in CIE color space.</br>
The first entry is the x coordinate and the second entry is the y coordinate. Both x and y are between 0 and 1`,
},
{
displayName: 'Coordinates Increments',
name: 'xy_inc',
type: 'string',
default: '',
placeholder: '0.5,0.5',
description: `Increments or decrements the value of the xy. This value is ignored if the Coordinates attribute is provided. Any ongoing color transition is stopped. Max value [0.5, 0.5]`,
},
{
displayName: 'Dynamic Effect',
name: 'effect',
type: 'options',
options: [
{
name: 'None',
value: 'none',
},
{
name: 'Color Loop',
value: 'colorloop',
},
],
default: '',
description: 'The dynamic effect of the light.',
},
{
displayName: 'Hue',
name: 'hue',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 65535,
},
default: 0,
description: 'The hue value to set light to.The hue value is a wrapping value between 0 and 65535. Both 0 and 65535 are red, 25500 is green and 46920 is blue.',
},
{
displayName: 'Hue Increments',
name: 'hue_inc',
type: 'number',
typeOptions: {
minValue: -65534,
maxValue: 65534,
},
default: 0,
description: 'Increments or decrements the value of the hue. Hue Increments is ignored if the Hue attribute is provided.',
},
{
displayName: 'Saturation',
name: 'sat',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 254,
},
default: 0,
description: 'Saturation of the light. 254 is the most saturated (colored) and 0 is the least saturated (white).',
},
{
displayName: 'Saturation Increments',
name: 'sat_inc',
type: 'number',
typeOptions: {
minValue: -254,
maxValue: 254,
},
default: 0,
description: 'Increments or decrements the value of the sat. This value is ignored if the Saturation attribute is provided.',
},
{
displayName: 'Transition Time',
name: 'transitiontime',
type: 'number',
typeOptions: {
minVale: 1,
},
default: 4,
description: 'The duration in seconds of the transition from the lights current state to the new state',
},
],
},
] as INodeProperties[];

View File

@@ -0,0 +1,184 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeTypeDescription,
INodeType,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
philipsHueApiRequest,
getUser,
} from './GenericFunctions';
import {
lightOperations,
lightFields,
} from './LightDescription';
export class PhilipsHue implements INodeType {
description: INodeTypeDescription = {
displayName: 'Philips Hue',
name: 'philipsHue',
icon: 'file:philipshue.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Philips Hue API.',
defaults: {
name: 'Philips Hue',
color: '#063c9a',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'philipsHueOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Light',
value: 'light',
},
],
default: 'light',
description: 'The resource to operate on.',
},
...lightOperations,
...lightFields,
],
};
methods = {
loadOptions: {
// Get all the lights to display them to user so that he can
// select them easily
async getLights(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const user = await getUser.call(this);
const lights = await philipsHueApiRequest.call(
this,
'GET',
`/bridge/${user}/lights`,
);
for (const light of Object.keys(lights)) {
const lightName = lights[light].name;
const lightId = light;
returnData.push({
name: lightName,
value: lightId
});
}
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 === 'light') {
if (operation === 'update') {
const lightId = this.getNodeParameter('lightId', i) as string;
const on = this.getNodeParameter('on', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body = {
on,
};
if (additionalFields.transitiontime) {
additionalFields.transitiontime = (additionalFields.transitiontime as number * 100);
}
if (additionalFields.xy) {
additionalFields.xy = (additionalFields.xy as string).split(',').map((e: string) => parseFloat(e));
}
if (additionalFields.xy_inc) {
additionalFields.xy_inc = (additionalFields.xy_inc as string).split(',').map((e: string) => parseFloat(e));
}
Object.assign(body, additionalFields);
const user = await getUser.call(this);
const data = await philipsHueApiRequest.call(
this,
'PUT',
`/bridge/${user}/lights/${lightId}/state`,
body,
);
responseData = {};
for (const response of data) {
Object.assign(responseData, response.success);
}
}
if (operation === 'delete') {
const lightId = this.getNodeParameter('lightId', i) as string;
const user = await getUser.call(this);
responseData = await philipsHueApiRequest.call(this, 'DELETE', `/bridge/${user}/lights/${lightId}`);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const user = await getUser.call(this);
const lights = await philipsHueApiRequest.call(this, 'GET', `/bridge/${user}/lights`);
responseData = Object.values(lights);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'get') {
const lightId = this.getNodeParameter('lightId', i) as string;
const user = await getUser.call(this);
responseData = await philipsHueApiRequest.call(this, 'GET', `/bridge/${user}/lights/${lightId}`);
}
}
}
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: 7.0 KiB

View File

@@ -25,7 +25,6 @@ export interface ICustomProperties {
[key: string]: ICustomInterface;
}
/**
* Make an API request to Pipedrive
*
@@ -36,18 +35,12 @@ export interface ICustomProperties {
* @returns {Promise<any>}
*/
export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, formData?: IDataObject, downloadFile?: boolean): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('pipedriveApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
if (query === undefined) {
query = {};
}
query.api_token = credentials.apiToken;
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
headers: {
Accept: 'application/json',
},
method,
qs: query,
uri: `https://api.pipedrive.com/v1${endpoint}`,
@@ -67,9 +60,28 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
options.formData = formData;
}
if (query === undefined) {
query = {};
}
let responseData;
try {
//@ts-ignore
const responseData = await this.helpers.request(options);
if (authenticationMethod === 'basicAuth' || authenticationMethod === 'apiToken') {
const credentials = this.getCredentials('pipedriveApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
query.api_token = credentials.apiToken;
//@ts-ignore
responseData = await this.helpers.request(options);
} else {
responseData = await this.helpers.requestOAuth2!.call(this, 'pipedriveOAuth2Api', options);
}
if (downloadFile === true) {
return {
@@ -85,7 +97,7 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
additionalData: responseData.additional_data,
data: responseData.data,
};
} catch (error) {
} catch(error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Pipedrive credentials are not valid!');
@@ -93,7 +105,7 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
if (error.response && error.response.body && error.response.body.error) {
// Try to return the error prettier
let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error}`;
let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error.message}`;
if (error.response.body.error_info) {
errorMessage += ` - ${error.response.body.error_info}`;
}
@@ -105,8 +117,6 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
}
}
/**
* Make an API request to paginated Pipedrive endpoint
* and return all results
@@ -124,7 +134,7 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
if (query === undefined) {
query = {};
}
query.limit = 500;
query.limit = 100;
query.start = 0;
const returnData: IDataObject[] = [];
@@ -133,7 +143,12 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
do {
responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData.data);
// the search path returns data diferently
if (responseData.data.items) {
returnData.push.apply(returnData, responseData.data.items);
} else {
returnData.push.apply(returnData, responseData.data);
}
query.start = responseData.additionalData.pagination.next_start;
} while (

View File

@@ -25,7 +25,6 @@ interface CustomProperty {
value: string;
}
/**
* Add the additional fields to the body
*
@@ -63,9 +62,44 @@ export class Pipedrive implements INodeType {
{
name: 'pipedriveApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiToken',
],
},
},
},
{
name: 'pipedriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Token',
value: 'apiToken'
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiToken',
description: 'Method of authentication.',
},
{
displayName: 'Resource',
name: 'resource',
@@ -275,7 +309,7 @@ export class Pipedrive implements INodeType {
{
name: 'Get All',
value: 'getAll',
description: 'Get data of all note',
description: 'Get data of all notes',
},
{
name: 'Update',
@@ -307,7 +341,7 @@ export class Pipedrive implements INodeType {
{
name: 'Delete',
value: 'delete',
description: 'Delete anorganization',
description: 'Delete an organization',
},
{
name: 'Get',
@@ -362,6 +396,11 @@ export class Pipedrive implements INodeType {
value: 'getAll',
description: 'Get data of all persons',
},
{
name: 'Search',
value: 'search',
description: 'Search all persons',
},
{
name: 'Update',
value: 'update',
@@ -2021,6 +2060,7 @@ export class Pipedrive implements INodeType {
show: {
operation: [
'getAll',
'search',
],
},
},
@@ -2035,6 +2075,7 @@ export class Pipedrive implements INodeType {
show: {
operation: [
'getAll',
'search',
],
returnAll: [
false,
@@ -2088,6 +2129,82 @@ export class Pipedrive implements INodeType {
},
],
},
// ----------------------------------
// person:search
// ----------------------------------
{
displayName: 'Term',
name: 'term',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'person',
],
},
},
default: '',
description: 'The search term to look for. Minimum 2 characters (or 1 if using exact_match).',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'search',
],
resource: [
'person',
],
},
},
default: {},
options: [
{
displayName: 'Exact Match',
name: 'exactMatch',
type: 'boolean',
default: false,
description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.',
},
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them.',
},
{
displayName: 'Include Fields',
name: 'includeFields',
type: 'string',
default: '',
description: 'Supports including optional fields in the results which are not provided by default.',
},
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'string',
default: '',
description: 'Will filter Deals by the provided Organization ID.',
},
{
displayName: 'RAW Data',
name: 'rawData',
type: 'boolean',
default: false,
description: `Returns the data exactly in the way it got received from the API.`,
},
],
},
],
};
@@ -2526,6 +2643,39 @@ export class Pipedrive implements INodeType {
endpoint = `/persons`;
} else if (operation === 'search') {
// ----------------------------------
// persons:search
// ----------------------------------
requestMethod = 'GET';
qs.term = this.getNodeParameter('term', i) as string;
returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === false) {
qs.limit = this.getNodeParameter('limit', i) as number;
}
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs.fields = additionalFields.fields as string;
}
if (additionalFields.exactMatch) {
qs.exact_match = additionalFields.exactMatch as boolean;
}
if (additionalFields.organizationId) {
qs.organization_id = parseInt(additionalFields.organizationId as string, 10);
}
if (additionalFields.includeFields) {
qs.include_fields = additionalFields.includeFields as string;
}
endpoint = `/persons/search`;
} else if (operation === 'update') {
// ----------------------------------
// person:update
@@ -2562,7 +2712,9 @@ export class Pipedrive implements INodeType {
let responseData;
if (returnAll === true) {
responseData = await pipedriveApiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else {
if (customProperties !== undefined) {
@@ -2597,6 +2749,19 @@ export class Pipedrive implements INodeType {
responseData.data = [];
}
if (operation === 'search' && responseData.data && responseData.data.items) {
responseData.data = responseData.data.items;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.rawData !== true) {
responseData.data = responseData.data.map((item: { result_score: number, item: object }) => {
return {
result_score: item.result_score,
...item.item,
};
});
}
}
if (Array.isArray(responseData.data)) {
returnData.push.apply(returnData, responseData.data as IDataObject[]);
} else {

View File

@@ -14,8 +14,10 @@ import {
} from './GenericFunctions';
import * as basicAuth from 'basic-auth';
import { Response } from 'express';
import {
Response,
} from 'express';
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
if (message === undefined) {
@@ -179,7 +181,6 @@ export class PipedriveTrigger implements INodeType {
description: 'Type of object to receive notifications about.',
},
],
};
// @ts-ignore (because of request)
@@ -276,8 +277,6 @@ export class PipedriveTrigger implements INodeType {
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const resp = this.getResponseObject();

View File

@@ -0,0 +1,129 @@
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
import pgPromise = require('pg-promise');
import pg = require('pg-promise/typescript/pg-subset');
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
export function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map(item => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}
/**
* Executes the given SQL query on the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {input[]} input The Node's input data
* @returns Promise<Array<object>>
*/
export function pgQuery(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
input: INodeExecutionData[],
): Promise<object[]> {
const queries: string[] = [];
for (let i = 0; i < input.length; i++) {
queries.push(getNodeParam('query', i) as string);
}
return db.any(pgp.helpers.concat(queries));
}
/**
* Inserts the given items into the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {INodeExecutionData[]} items The items to be inserted
* @returns Promise<Array<IDataObject>>
*/
export async function pgInsert(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
items: INodeExecutionData[],
): Promise<IDataObject[][]> {
const table = getNodeParam('table', 0) as string;
const schema = getNodeParam('schema', 0) as string;
let returnFields = (getNodeParam('returnFields', 0) as string).split(',') as string[];
const columnString = getNodeParam('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
const cs = new pgp.helpers.ColumnSet(columns);
const te = new pgp.helpers.TableName({ table, schema });
// Prepare the data to insert and copy it to be returned
const insertItems = getItemCopy(items, columns);
// Generate the multi-row insert query and return the id of new row
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
const query =
pgp.helpers.insert(insertItems, cs, te) +
(returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
// Executing the query to insert the data
const insertData = await db.manyOrNone(query);
return [insertData, insertItems];
}
/**
* Updates the given items in the database.
*
* @param {Function} getNodeParam The getter for the Node's parameters
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {INodeExecutionData[]} items The items to be updated
* @returns Promise<Array<IDataObject>>
*/
export async function pgUpdate(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
items: INodeExecutionData[],
): Promise<IDataObject[]> {
const table = getNodeParam('table', 0) as string;
const updateKey = getNodeParam('updateKey', 0) as string;
const columnString = getNodeParam('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
// Make sure that the updateKey does also get queried
if (!columns.includes(updateKey)) {
columns.unshift(updateKey);
}
// Prepare the data to update and copy it to be returned
const updateItems = getItemCopy(items, columns);
// Generate the multi-row update query
const query =
pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
// Executing the query to update the data
await db.none(query);
return updateItems;
}

View File

@@ -3,36 +3,12 @@ import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
INodeTypeDescription
} from 'n8n-workflow';
import * as pgPromise from 'pg-promise';
/**
* Returns of copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
// Prepare the data to insert and copy it to be returned
let newItem: IDataObject;
return items.map((item) => {
newItem = {};
for (const property of properties) {
if (item.json[property] === undefined) {
newItem[property] = null;
} else {
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
}
}
return newItem;
});
}
import { pgInsert, pgQuery, pgUpdate } from './Postgres.node.functions';
export class Postgres implements INodeType {
description: INodeTypeDescription = {
@@ -52,7 +28,7 @@ export class Postgres implements INodeType {
{
name: 'postgres',
required: true,
}
},
],
properties: [
{
@@ -63,17 +39,17 @@ export class Postgres implements INodeType {
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Executes a SQL query.',
description: 'Execute an SQL query',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database.',
description: 'Insert rows in database',
},
{
name: 'Update',
value: 'update',
description: 'Updates rows in database.',
description: 'Update rows in database',
},
],
default: 'insert',
@@ -92,9 +68,7 @@ export class Postgres implements INodeType {
},
displayOptions: {
show: {
operation: [
'executeQuery'
],
operation: ['executeQuery'],
},
},
default: '',
@@ -103,7 +77,6 @@ export class Postgres implements INodeType {
description: 'The SQL query to execute.',
},
// ----------------------------------
// insert
// ----------------------------------
@@ -113,9 +86,7 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
operation: ['insert'],
},
},
default: 'public',
@@ -128,9 +99,7 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
operation: ['insert'],
},
},
default: '',
@@ -143,14 +112,13 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
operation: ['insert'],
},
},
default: '',
placeholder: 'id,name,description',
description: 'Comma separated list of the properties which should used as columns for the new rows.',
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
{
displayName: 'Return Fields',
@@ -158,16 +126,13 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
operation: ['insert'],
},
},
default: '*',
description: 'Comma separated list of the fields that the operation will return',
},
// ----------------------------------
// update
// ----------------------------------
@@ -177,9 +142,7 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'update'
],
operation: ['update'],
},
},
default: '',
@@ -192,14 +155,13 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'update'
],
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".',
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
@@ -207,22 +169,18 @@ export class Postgres implements INodeType {
type: 'string',
displayOptions: {
show: {
operation: [
'update'
],
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description: 'Comma separated list of the properties which should used as columns for rows to update.',
description:
'Comma separated list of the properties which should used as columns for rows to update.',
},
]
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('postgres');
if (credentials === undefined) {
@@ -238,7 +196,7 @@ export class Postgres implements INodeType {
user: credentials.user as string,
password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: credentials.ssl as string || 'disable',
sslmode: (credentials.ssl as string) || 'disable',
};
const db = pgp(config);
@@ -253,39 +211,15 @@ export class Postgres implements INodeType {
// executeQuery
// ----------------------------------
const queries: string[] = [];
for (let i = 0; i < items.length; i++) {
queries.push(this.getNodeParameter('query', i) as string);
}
const queryResult = await db.any(pgp.helpers.concat(queries));
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const table = this.getNodeParameter('table', 0) as string;
const schema = this.getNodeParameter('schema', 0) as string;
let returnFields = (this.getNodeParameter('returnFields', 0) as string).split(',') as string[];
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
const cs = new pgp.helpers.ColumnSet(columns);
const te = new pgp.helpers.TableName({ table, schema });
// Prepare the data to insert and copy it to be returned
const insertItems = getItemCopy(items, columns);
// Generate the multi-row insert query and return the id of new row
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
const query = pgp.helpers.insert(insertItems, cs, te) + (returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
// Executing the query to insert the data
const insertData = await db.manyOrNone(query);
const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
// Add the id to the data
for (let i = 0; i < insertData.length; i++) {
@@ -293,37 +227,17 @@ export class Postgres implements INodeType {
json: {
...insertData[i],
...insertItems[i],
}
},
});
}
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const table = this.getNodeParameter('table', 0) as string;
const updateKey = this.getNodeParameter('updateKey', 0) as string;
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
// Make sure that the updateKey does also get queried
if (!columns.includes(updateKey)) {
columns.unshift(updateKey);
}
// Prepare the data to update and copy it to be returned
const updateItems = getItemCopy(items, columns);
// Generate the multi-row update query
const query = pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
// Executing the query to update the data
await db.none(query);
returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]);
const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(updateItems);
} else {
await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`);

View File

@@ -0,0 +1,93 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IHookFunctions,
IWebhookFunctions
} from 'n8n-workflow';
export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method : string, endpoint : string, body: any = {}, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('postmarkApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Postmark-Server-Token' : credentials.serverToken
},
method,
body,
uri: 'https://api.postmarkapp.com' + endpoint,
json: true
};
if (body === {}) {
delete options.body;
}
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
} catch (error) {
throw new Error(`Postmark: ${error.statusCode} Message: ${error.message}`);
}
}
// tslint:disable-next-line: no-any
export function convertTriggerObjectToStringArray (webhookObject : any) : string[] {
const triggers = webhookObject.Triggers;
const webhookEvents : string[] = [];
// Translate Webhook trigger settings to string array
if (triggers.Open.Enabled) {
webhookEvents.push('open');
}
if (triggers.Open.PostFirstOpenOnly) {
webhookEvents.push('firstOpen');
}
if (triggers.Click.Enabled) {
webhookEvents.push('click');
}
if (triggers.Delivery.Enabled) {
webhookEvents.push('delivery');
}
if (triggers.Bounce.Enabled) {
webhookEvents.push('bounce');
}
if (triggers.Bounce.IncludeContent) {
webhookEvents.push('includeContent');
}
if (triggers.SpamComplaint.Enabled) {
webhookEvents.push('spamComplaint');
}
if (triggers.SpamComplaint.IncludeContent) {
if (!webhookEvents.includes('IncludeContent')) {
webhookEvents.push('includeContent');
}
}
if (triggers.SubscriptionChange.Enabled) {
webhookEvents.push('subscriptionChange');
}
return webhookEvents;
}
export function eventExists (currentEvents : string[], webhookEvents: string[]) {
for (const currentEvent of currentEvents) {
if (!webhookEvents.includes(currentEvent)) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,256 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
INodeTypeDescription,
INodeType,
IWebhookResponseData,
} from 'n8n-workflow';
import {
convertTriggerObjectToStringArray,
eventExists,
postmarkApiRequest
} from './GenericFunctions';
export class PostmarkTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Postmark Trigger',
name: 'postmarkTrigger',
icon: 'file:postmark.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Postmark events occur.',
defaults: {
name: 'Postmark Trigger',
color: '#fedd00',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'postmarkApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
options: [
{
name: 'Bounce',
value: 'bounce',
description: 'Trigger on bounce.',
},
{
name: 'Click',
value: 'click',
description: 'Trigger on click.',
},
{
name: 'Delivery',
value: 'delivery',
description: 'Trigger on delivery.',
},
{
name: 'Open',
value: 'open',
description: 'Trigger webhook on open.',
},
{
name: 'Spam Complaint',
value: 'spamComplaint',
description: 'Trigger on spam complaint.',
},
{
name: 'Subscription Change',
value: 'subscriptionChange',
description: 'Trigger on subscription change.',
},
],
default: [],
required: true,
description: 'Webhook events that will be enabled for that endpoint.',
},
{
displayName: 'First Open',
name: 'firstOpen',
description: 'Only fires on first open for event "Open".',
type: 'boolean',
default: false,
displayOptions: {
show: {
events: [
'open',
],
},
},
},
{
displayName: 'Include Content',
name: 'includeContent',
description: 'Includes message content for events "Bounce" and "Spam Complaint".',
type: 'boolean',
default: false,
displayOptions: {
show: {
events: [
'bounce',
'spamComplaint',
],
},
},
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const events = this.getNodeParameter('events') as string[];
if (this.getNodeParameter('includeContent') as boolean) {
events.push('includeContent');
}
if (this.getNodeParameter('firstOpen') as boolean) {
events.push('firstOpen');
}
// Get all webhooks
const endpoint = `/webhooks`;
const responseData = await postmarkApiRequest.call(this, 'GET', endpoint, {});
// No webhooks exist
if (responseData.Webhooks.length === 0) {
return false;
}
// If webhooks exist, check if any match current settings
for (const webhook of responseData.Webhooks) {
if (webhook.Url === webhookUrl && eventExists(events, convertTriggerObjectToStringArray(webhook))) {
webhookData.webhookId = webhook.ID;
// webhook identical to current settings. re-assign webhook id to found webhook.
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const endpoint = `/webhooks`;
// tslint:disable-next-line: no-any
const body : any = {
Url: webhookUrl,
Triggers: {
Open:{
Enabled: false,
PostFirstOpenOnly: false
},
Click:{
Enabled: false
},
Delivery:{
Enabled: false
},
Bounce:{
Enabled: false,
IncludeContent: false
},
SpamComplaint:{
Enabled: false,
IncludeContent: false
},
SubscriptionChange: {
Enabled: false
}
}
};
const events = this.getNodeParameter('events') as string[];
if (events.includes('open')) {
body.Triggers.Open.Enabled = true;
body.Triggers.Open.PostFirstOpenOnly = this.getNodeParameter('firstOpen') as boolean;
}
if (events.includes('click')) {
body.Triggers.Click.Enabled = true;
}
if (events.includes('delivery')) {
body.Triggers.Delivery.Enabled = true;
}
if (events.includes('bounce')) {
body.Triggers.Bounce.Enabled = true;
body.Triggers.Bounce.IncludeContent = this.getNodeParameter('includeContent') as boolean;
}
if (events.includes('spamComplaint')) {
body.Triggers.SpamComplaint.Enabled = true;
body.Triggers.SpamComplaint.IncludeContent = this.getNodeParameter('includeContent') as boolean;
}
if (events.includes('subscriptionChange')) {
body.Triggers.SubscriptionChange.Enabled = true;
}
const responseData = await postmarkApiRequest.call(this, 'POST', endpoint, body);
if (responseData.ID === undefined) {
// Required data is missing so was not successful
return false;
}
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = responseData.ID as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/webhooks/${webhookData.webhookId}`;
const body = {};
try {
await postmarkApiRequest.call(this, 'DELETE', endpoint, body);
} catch (e) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
delete webhookData.webhookEvents;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
return {
workflowData: [
this.helpers.returnJsonArray(req.body)
],
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,215 @@
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import * as pgPromise from 'pg-promise';
import { pgQuery } from '../Postgres/Postgres.node.functions';
export class QuestDb implements INodeType {
description: INodeTypeDescription = {
displayName: 'QuestDB',
name: 'questDb',
icon: 'file:questdb.png',
group: ['input'],
version: 1,
description: 'Gets, add and update data in QuestDB.',
defaults: {
name: 'QuestDB',
color: '#2C4A79',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'questDb',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Executes a SQL query.',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database.',
}
],
default: 'insert',
description: 'The operation to perform.',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: [
'executeQuery',
],
},
},
default: '',
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute.',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Schema',
name: 'schema',
type: 'string',
displayOptions: {
show: {
operation: [
'insert',
],
},
},
default: 'public',
required: true,
description: 'Name of the schema the table belongs to',
},
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: [
'insert',
],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to.',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: [
'insert',
],
},
},
default: '',
placeholder: 'id,name,description',
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
{
displayName: 'Return Fields',
name: 'returnFields',
type: 'string',
displayOptions: {
show: {
operation: [
'insert',
],
},
},
default: '*',
description: 'Comma separated list of the fields that the operation will return',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = this.getCredentials('questDb');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const pgp = pgPromise();
const config = {
host: credentials.host as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: (credentials.ssl as string) || 'disable',
};
const db = pgp(config);
let returnItems = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const tableName = this.getNodeParameter('table', 0) as string;
const returnFields = this.getNodeParameter('returnFields', 0) as string;
const queries : string[] = [];
items.map(item => {
const columns = Object.keys(item.json);
const values : string = columns.map((col : string) => {
if (typeof item.json[col] === 'string') {
return `\'${item.json[col]}\'`;
} else {
return item.json[col];
}
}).join(',');
const query = `INSERT INTO ${tableName} (${columns.join(',')}) VALUES (${values});`;
queries.push(query);
});
await db.any(pgp.helpers.concat(queries));
const returnedItems = await db.any(`SELECT ${returnFields} from ${tableName}`);
returnItems = this.helpers.returnJsonArray(returnedItems as IDataObject[]);
} else {
await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`);
}
// Close the connection
await pgp.end();
return this.prepareOutputData(returnItems);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

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