Merge 'master' into 'ConvertKit'
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
361
packages/nodes-base/nodes/Box/Box.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
}
|
||||
354
packages/nodes-base/nodes/Box/BoxTrigger.node.ts
Normal 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)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
599
packages/nodes-base/nodes/Box/FileDescription.ts
Normal 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[];
|
||||
419
packages/nodes-base/nodes/Box/FolderDescription.ts
Normal 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[];
|
||||
79
packages/nodes-base/nodes/Box/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Box/box.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
140
packages/nodes-base/nodes/CircleCi/CircleCi.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
67
packages/nodes-base/nodes/CircleCi/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
229
packages/nodes-base/nodes/CircleCi/PipelineDescription.ts
Normal 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[];
|
||||
BIN
packages/nodes-base/nodes/CircleCi/circleCi.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ export const singletonOperations = [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Gets a Singleton',
|
||||
description: 'Get a singleton',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
288
packages/nodes-base/nodes/CrateDb/CrateDb.node.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/CrateDb/cratedb.png
Normal file
|
After Width: | Height: | Size: 475 B |
329
packages/nodes-base/nodes/CustomerIo/CustomerIoTrigger.node.ts
Normal 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)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
65
packages/nodes-base/nodes/CustomerIo/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
69
packages/nodes-base/nodes/Dropbox/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -99,7 +99,7 @@ export class FlowTrigger implements INodeType {
|
||||
]
|
||||
}
|
||||
},
|
||||
description: `Taks ids separated by ,`,
|
||||
description: `Task ids separated by ,`,
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
434
packages/nodes-base/nodes/Ftp.node.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
80
packages/nodes-base/nodes/HackerNews/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
384
packages/nodes-base/nodes/HackerNews/HackerNews.node.ts
Normal 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)];
|
||||
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/HackerNews/hackernews.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const companyOperations = [
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all company',
|
||||
description: 'Get all companies',
|
||||
},
|
||||
{
|
||||
name: 'Get Recently Created',
|
||||
|
||||
@@ -23,7 +23,7 @@ export const contactOperations = [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a contacts',
|
||||
description: 'Delete a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
|
||||
@@ -23,7 +23,7 @@ export const dealOperations = [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a deals',
|
||||
description: 'Delete a deal',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ticketOperations = [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a tickets',
|
||||
description: 'Delete a ticket',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
454
packages/nodes-base/nodes/Jira/JiraTrigger.node.ts
Normal 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)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
54
packages/nodes-base/nodes/Medium/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
577
packages/nodes-base/nodes/Medium/Medium.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Medium/medium.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
144
packages/nodes-base/nodes/Microsoft/Sql/GenericFunctions.ts
Normal 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(',')})`;
|
||||
}
|
||||
394
packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
export interface ITables {
|
||||
[key: string]: {
|
||||
[key: string]: IDataObject[];
|
||||
};
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Microsoft/Sql/mssql.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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',
|
||||
|
||||
63
packages/nodes-base/nodes/NextCloud/GenericFunctions.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
66
packages/nodes-base/nodes/PhilipsHue/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
345
packages/nodes-base/nodes/PhilipsHue/LightDescription.ts
Normal 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 bulb’s 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 light’s current state to the new state',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as INodeProperties[];
|
||||
184
packages/nodes-base/nodes/PhilipsHue/PhilipsHue.node.ts
Normal 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)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/PhilipsHue/philipshue.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
129
packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts
Normal 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;
|
||||
}
|
||||
@@ -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!`);
|
||||
|
||||
93
packages/nodes-base/nodes/Postmark/GenericFunctions.ts
Normal 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;
|
||||
}
|
||||
256
packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts
Normal 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)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Postmark/postmark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
215
packages/nodes-base/nodes/QuestDb/QuestDb.node.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/QuestDb/questdb.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |