Merge branch 'Master' into 'Pipedrive-OAuth2-support'

This commit is contained in:
ricardo
2020-07-23 16:51:05 -04:00
parent c1b4c570fd
commit b187a8fd7d
271 changed files with 17019 additions and 2796 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,246 @@
import { IExecuteFunctions } from 'n8n-core';
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
import * as pgPromise from 'pg-promise';
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/Postgres.node.functions';
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: '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',
},
// ----------------------------------
// 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 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!`);
}
// Close the connection
await pgp.end();
return this.prepareOutputData(returnItems);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

View File

@@ -243,9 +243,10 @@ export class DateTime implements INodeType {
if (currentDate === undefined) {
continue;
}
if (!moment(currentDate as string | number).isValid()) {
if (options.fromFormat === undefined && !moment(currentDate as string | number).isValid()) {
throw new Error('The date input format could not be recognized. Please set the "From Format" field');
}
if (Number.isInteger(currentDate as unknown as number)) {
newDate = moment.unix(currentDate as unknown as number);
} else {

View File

@@ -37,9 +37,44 @@ export class Drift implements INodeType {
{
name: 'driftApi',
required: true,
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'driftOAuth2Api',
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',

View File

@@ -12,25 +12,15 @@ import {
} from 'n8n-workflow';
export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('driftApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const endpoint = 'https://driftapi.com';
let options: OptionsWithUri = {
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
},
headers: {},
method,
body,
qs: query,
uri: uri || `${endpoint}${resource}`,
uri: uri || `https://driftapi.com${resource}`,
json: true
};
if (!Object.keys(body).length) {
delete options.form;
}
@@ -38,11 +28,27 @@ export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunction
delete options.qs;
}
options = Object.assign({}, options, option);
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('driftApi');
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, 'driftOAuth2Api', options);
}
} catch (error) {
if (error.response) {
const errorMessage = error.message || (error.response.body && error.response.body.message );
if (error.response && error.response.body && error.response.body.error) {
const errorMessage = error.response.body.error.message;
throw new Error(`Drift error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;

View File

@@ -2,6 +2,7 @@ import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@@ -9,8 +10,9 @@ import {
INodeType,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import {
OptionsWithUri
} from 'request';
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: '#0061FF',
},
inputs: ['main'],
outputs: ['main'],
@@ -454,6 +456,7 @@ export class Dropbox implements INodeType {
let requestMethod = '';
let body: IDataObject | Buffer;
let isJson = false;
let query: IDataObject = {};
let headers: IDataObject;
@@ -470,8 +473,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,9 +487,10 @@ 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';
@@ -594,8 +599,8 @@ export class Dropbox implements INodeType {
const options: OptionsWithUri = {
headers,
method: requestMethod,
qs: {},
uri: endpoint,
qs: query,
json: isJson,
};

View File

@@ -35,7 +35,25 @@ export class EventbriteTrigger implements INodeType {
{
name: 'eventbriteApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'privateKey',
],
},
},
},
{
name: 'eventbriteOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -46,6 +64,23 @@ export class EventbriteTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Private Key',
value: 'privateKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'privateKey',
description: 'The resource to operate on.',
},
{
displayName: 'Organization',
name: 'organization',
@@ -149,7 +184,6 @@ export class EventbriteTrigger implements INodeType {
description: 'By default does the webhook-data only contain the URL to receive<br />the object data manually. If this option gets activated it<br />will resolve the data automatically.',
},
],
};
methods = {
@@ -192,23 +226,39 @@ export class EventbriteTrigger implements INodeType {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
return false;
const webhookUrl = this.getNodeWebhookUrl('default');
const organisation = this.getNodeParameter('organization') as string;
const actions = this.getNodeParameter('actions') as string[];
const endpoint = `/organizations/${organisation}/webhooks/`;
const { webhooks } = await eventbriteApiRequest.call(this, 'GET', endpoint);
const check = (currentActions: string[], webhookActions: string[]) => {
for (const currentAction of currentActions) {
if (!webhookActions.includes(currentAction)) {
return false;
}
}
return true;
};
for (const webhook of webhooks) {
if (webhook.endpoint_url === webhookUrl && check(actions, webhook.actions)) {
webhookData.webhookId = webhook.id;
return true;
}
}
const endpoint = `/webhooks/${webhookData.webhookId}/`;
try {
await eventbriteApiRequest.call(this, 'GET', endpoint);
} catch (e) {
return false;
}
return true;
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const organisation = this.getNodeParameter('organization') as string;
const event = this.getNodeParameter('event') as string;
const actions = this.getNodeParameter('actions') as string[];
const endpoint = `/webhooks/`;
const endpoint = `/organizations/${organisation}/webhooks/`;
const body: IDataObject = {
endpoint_url: webhookUrl,
actions: actions.join(','),

View File

@@ -1,4 +1,7 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
@@ -6,16 +9,14 @@ import {
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import { IDataObject } from 'n8n-workflow';
import {
IDataObject,
} from 'n8n-workflow';
export async function eventbriteApiRequest(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('eventbriteApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: { 'Authorization': `Bearer ${credentials.apiKey}`},
headers: {},
method,
qs,
body,
@@ -27,14 +28,26 @@ export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFuncti
delete options.body;
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'privateKey') {
const credentials = this.getCredentials('eventbriteApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
return await this.helpers.request!(options);
} else {
return await this.helpers.requestOAuth2!.call(this, 'eventbriteOAuth2Api', options);
}
} catch (error) {
let errorMessage = error.message;
if (error.response.body && error.response.body.error_description) {
errorMessage = error.response.body.error_description;
}
throw new Error('Eventbrite Error: ' + errorMessage);
}
}

View File

@@ -137,6 +137,13 @@ export class FacebookGraphApi implements INodeType {
placeholder: 'videos',
required: false,
},
{
displayName: 'Ignore SSL Issues',
name: 'allowUnauthorizedCerts',
type: 'boolean',
default: false,
description: 'Still download the response even if SSL certificate validation is not possible. (Not recommended)',
},
{
displayName: 'Send Binary Data',
name: 'sendBinaryData',
@@ -301,6 +308,7 @@ export class FacebookGraphApi implements INodeType {
qs: {
access_token: graphApiCredentials!.accessToken,
},
rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean,
};
if (options !== undefined) {

View File

@@ -17,14 +17,14 @@ import {
export class Github implements INodeType {
description: INodeTypeDescription = {
displayName: 'Github',
displayName: 'GitHub',
name: 'github',
icon: 'file:github.png',
group: ['input'],
version: 1,
description: 'Retrieve data from Github API.',
description: 'Retrieve data from GitHub API.',
defaults: {
name: 'Github',
name: 'GitHub',
color: '#665533',
},
inputs: ['main'],
@@ -178,7 +178,7 @@ export class Github implements INodeType {
{
name: 'Get',
value: 'get',
description: 'Get the data of a single issues',
description: 'Get the data of a single issue',
},
],
default: 'create',
@@ -220,7 +220,7 @@ export class Github implements INodeType {
{
name: 'List Popular Paths',
value: 'listPopularPaths',
description: 'Get the data of a file in repositoryGet the top 10 popular content paths over the last 14 days.',
description: 'Get the top 10 popular content paths over the last 14 days.',
},
{
name: 'List Referrers',
@@ -244,11 +244,6 @@ export class Github implements INodeType {
},
},
options: [
{
name: 'Get Emails',
value: 'getEmails',
description: 'Returns the email addresses of a user',
},
{
name: 'Get Repositories',
value: 'getRepositories',
@@ -463,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: '',
@@ -496,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: '',
@@ -1019,28 +1014,28 @@ export class Github implements INodeType {
name: 'assignee',
type: 'string',
default: '',
description: 'Return only issuse which are assigned to a specific user.',
description: 'Return only issues which are assigned to a specific user.',
},
{
displayName: 'Creator',
name: 'creator',
type: 'string',
default: '',
description: 'Return only issuse which were created by a specific user.',
description: 'Return only issues which were created by a specific user.',
},
{
displayName: 'Mentioned',
name: 'mentioned',
type: 'string',
default: '',
description: 'Return only issuse in which a specific user was mentioned.',
description: 'Return only issues in which a specific user was mentioned.',
},
{
displayName: 'Labels',
name: 'labels',
type: 'string',
default: '',
description: 'Return only issuse with the given labels. Multiple lables can be separated by comma.',
description: 'Return only issues with the given labels. Multiple lables can be separated by comma.',
},
{
displayName: 'Updated Since',

View File

@@ -34,7 +34,25 @@ export class GithubTrigger implements INodeType {
{
name: 'githubApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'githubOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -45,6 +63,23 @@ export class GithubTrigger 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',

View File

@@ -1,11 +1,13 @@
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
/**
* Make an API request to Gitlab
@@ -17,24 +19,43 @@ import {
* @returns {Promise<any>}
*/
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, 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 = {
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
};
if (query === undefined) {
delete options.qs;
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
return await this.helpers.request(options);
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

View File

@@ -13,7 +13,6 @@ import {
gitlabApiRequest,
} from './GenericFunctions';
export class Gitlab implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gitlab',
@@ -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',
@@ -793,10 +827,26 @@ export class Gitlab implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('gitlabApi');
let credentials;
if (credentials === undefined) {
throw new Error('No credentials got returned!');
const authenticationMethod = this.getNodeParameter('authentication', 0);
try {
if (authenticationMethod === 'accessToken') {
credentials = this.getCredentials('gitlabApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
} else {
credentials = this.getCredentials('gitlabOAuth2Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
}
} catch (error) {
throw new Error(error);
}
// Operations which overwrite the returned data

View File

@@ -14,7 +14,6 @@ import {
gitlabApiRequest,
} from './GenericFunctions';
export class GitlabTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Gitlab Trigger',
@@ -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',
@@ -135,7 +169,10 @@ export class GitlabTrigger implements INodeType {
// Webhook got created before so check if it still exists
const owner = this.getNodeParameter('owner') as string;
const repository = this.getNodeParameter('repository') as string;
const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`;
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`;
try {
await gitlabApiRequest.call(this, 'GET', endpoint, {});
@@ -175,15 +212,22 @@ export class GitlabTrigger implements INodeType {
events[`${e}_events`] = true;
}
const endpoint = `/projects/${owner}%2F${repository}/hooks`;
// gitlab set the push_events to true when the field it's not sent.
// set it to false when it's not picked by the user.
if (events['push_events'] === undefined) {
events['push_events'] = false;
}
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
const endpoint = `/projects/${path}/hooks`;
const body = {
url: webhookUrl,
events,
...events,
enable_ssl_verification: false,
};
let responseData;
try {
responseData = await gitlabApiRequest.call(this, 'POST', endpoint, body);
@@ -208,7 +252,10 @@ export class GitlabTrigger implements INodeType {
if (webhookData.webhookId !== undefined) {
const owner = this.getNodeParameter('owner') as string;
const repository = this.getNodeParameter('repository') as string;
const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`;
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`;
const body = {};
try {

View File

@@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const eventOperations = [
{
@@ -37,37 +39,36 @@ export const eventOperations = [
name: 'Update',
value: 'update',
description: 'Update an event',
},
}
],
default: 'create',
description: 'The operation to perform.',
},
description: 'The operation to perform.'
}
] as INodeProperties[];
export const eventFields = [
/* -------------------------------------------------------------------------- */
/* event:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* event:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Calendar',
name: 'calendar',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCalendars',
loadOptionsMethod: 'getCalendars'
},
required: true,
displayOptions: {
show: {
operation: [
'create',
'create'
],
resource: [
'event',
'event'
],
},
},
default: '',
default: ''
},
{
displayName: 'Start',
@@ -85,7 +86,7 @@ export const eventFields = [
},
},
default: '',
description: 'Start time of the event.',
description: 'Start time of the event.'
},
{
displayName: 'End',
@@ -103,7 +104,7 @@ export const eventFields = [
},
},
default: '',
description: 'End time of the event.',
description: 'End time of the event.'
},
{
displayName: 'Use Default Reminders',
@@ -119,7 +120,7 @@ export const eventFields = [
],
},
},
default: true,
default: true
},
{
displayName: 'Additional Fields',
@@ -153,7 +154,7 @@ export const eventFields = [
},
],
default: 'no',
description: 'Wheater the event is all day or not',
description: 'Wheater the event is all day or not'
},
{
displayName: 'Attendees',
@@ -176,6 +177,15 @@ export const eventFields = [
default: '',
description: 'The color of the event.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Guests Can Invite Others',
name: 'guestsCanInviteOthers',
@@ -239,7 +249,7 @@ export const eventFields = [
{
name: 'Yearly',
value: 'yearly',
},
}
],
default: '',
},
@@ -254,9 +264,9 @@ export const eventFields = [
name: 'repeatHowManyTimes',
type: 'number',
typeOptions: {
minValue: 1,
minValue: 1
},
default: 1,
default: 1
},
{
displayName: 'Send Updates',
@@ -266,7 +276,7 @@ export const eventFields = [
{
name: 'All',
value: 'all',
description: ' Notifications are sent to all guests',
description: 'Notifications are sent to all guests'
},
{
name: 'External Only',
@@ -276,8 +286,8 @@ export const eventFields = [
{
name: 'None',
value: 'none',
description: ' No notifications are sent. This value should only be used for migration use case',
},
description: 'No notifications are sent. This value should only be used for migration use case',
}
],
description: 'Whether to send notifications about the creation of the new event',
default: '',
@@ -303,7 +313,7 @@ export const eventFields = [
name: 'Busy',
value: 'opaque',
description: ' The event does block time on the calendar.',
},
}
],
default: 'opaque',
description: 'Whether the event blocks time on the calendar',
@@ -316,7 +326,7 @@ export const eventFields = [
loadOptionsMethod: 'getTimezones',
},
default: '',
description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.'
description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.',
},
{
displayName: 'Visibility',
@@ -331,7 +341,7 @@ export const eventFields = [
{
name: 'Default',
value: 'default',
description: ' Uses the default visibility for events on the calendar.',
description: 'Uses the default visibility for events on the calendar.',
},
{
name: 'Private',
@@ -345,7 +355,7 @@ export const eventFields = [
},
],
default: 'default',
description: 'Visibility of the event.',
description: 'Visibility of the event.'
},
],
},
@@ -356,7 +366,7 @@ export const eventFields = [
default: '',
placeholder: 'Add Reminder',
typeOptions: {
multipleValues: true,
multipleValues: true
},
required: false,
displayOptions: {
@@ -404,13 +414,13 @@ export const eventFields = [
default: 0,
},
],
}
},
],
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`,
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`
},
/* -------------------------------------------------------------------------- */
/* event:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* event:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Calendar',
name: 'calendar',
@@ -429,7 +439,7 @@ export const eventFields = [
],
},
},
default: '',
default: ''
},
{
displayName: 'Event ID',
@@ -473,7 +483,7 @@ export const eventFields = [
{
name: 'All',
value: 'all',
description: ' Notifications are sent to all guests',
description: 'Notifications are sent to all guests',
},
{
name: 'External Only',
@@ -483,17 +493,17 @@ export const eventFields = [
{
name: 'None',
value: 'none',
description: ' No notifications are sent. This value should only be used for migration use case',
},
description: 'No notifications are sent. This value should only be used for migration use case',
}
],
description: 'Whether to send notifications about the creation of the new event',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* event:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* event:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Calendar',
name: 'calendar',
@@ -512,7 +522,7 @@ export const eventFields = [
],
},
},
default: '',
default: ''
},
{
displayName: 'Event ID',
@@ -565,12 +575,12 @@ export const eventFields = [
},
default: '',
description: `Time zone used in the response. The default is the time zone of the calendar.`,
},
],
}
]
},
/* -------------------------------------------------------------------------- */
/* event:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* event:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Calendar',
name: 'calendar',
@@ -589,7 +599,7 @@ export const eventFields = [
],
},
},
default: '',
default: ''
},
{
displayName: 'Return All',
@@ -678,7 +688,7 @@ export const eventFields = [
name: 'Updated',
value: 'updated',
description: 'Order by last modification time (ascending).',
},
}
],
default: '',
description: 'The order of the events returned in the result.',
@@ -743,18 +753,18 @@ export const eventFields = [
default: '',
description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by.<b/r>
When specified, entries deleted since this time will always be included regardless of showDeleted`,
},
],
}
]
},
/* -------------------------------------------------------------------------- */
/* event:update */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* event:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Calendar',
name: 'calendar',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCalendars',
loadOptionsMethod: 'getCalendars'
},
required: true,
displayOptions: {
@@ -800,7 +810,7 @@ export const eventFields = [
],
},
},
default: true,
default: true
},
{
displayName: 'Update Fields',
@@ -831,7 +841,7 @@ export const eventFields = [
{
name: 'No',
value: 'no',
},
}
],
default: 'no',
description: 'Wheater the event is all day or not',
@@ -857,6 +867,15 @@ export const eventFields = [
default: '',
description: 'The color of the event.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'End',
name: 'end',
@@ -927,7 +946,7 @@ export const eventFields = [
{
name: 'Yearly',
value: 'yearly',
},
}
],
default: '',
},
@@ -971,8 +990,8 @@ export const eventFields = [
{
name: 'None',
value: 'none',
description: ' No notifications are sent. This value should only be used for migration use case',
},
description: 'No notifications are sent. This value should only be used for migration use case',
}
],
description: 'Whether to send notifications about the creation of the new event',
default: '',
@@ -1011,7 +1030,7 @@ export const eventFields = [
loadOptionsMethod: 'getTimezones',
},
default: '',
description: 'The timezone the event will have set. By default events are schedule on n8n timezone '
description: 'The timezone the event will have set. By default events are schedule on n8n timezone',
},
{
displayName: 'Visibility',
@@ -1026,7 +1045,7 @@ export const eventFields = [
{
name: 'Default',
value: 'default',
description: ' Uses the default visibility for events on the calendar.',
description: 'Uses the default visibility for events on the calendar.',
},
{
name: 'Public',
@@ -1037,7 +1056,7 @@ export const eventFields = [
name: 'Private',
value: 'private',
description: 'The event is private and only event attendees may view event details.',
},
}
],
default: 'default',
description: 'Visibility of the event.',
@@ -1051,7 +1070,7 @@ export const eventFields = [
default: '',
placeholder: 'Add Reminder',
typeOptions: {
multipleValues: true,
multipleValues: true
},
required: false,
displayOptions: {
@@ -1084,7 +1103,7 @@ export const eventFields = [
{
name: 'Popup',
value: 'popup',
},
}
],
default: '',
},
@@ -1099,8 +1118,8 @@ export const eventFields = [
default: 0,
},
],
}
},
],
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`,
},
}
] as INodeProperties[];

View File

@@ -1,4 +1,6 @@
import { IDataObject } from "n8n-workflow";
import {
IDataObject,
} from 'n8n-workflow';
export interface IReminder {
useDefault?: boolean;

View File

@@ -33,9 +33,15 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
if (error.response && error.response.body && error.response.body.error) {
let errors = error.response.body.error.errors;
errors = errors.map((e: IDataObject) => e.message);
// Try to return the error prettier
throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`);
throw new Error(
`Google Calendar error response [${error.statusCode}]: ${errors.join('|')}`
);
}
throw error;
}

View File

@@ -46,7 +46,7 @@ export class GoogleCalendar implements INodeType {
{
name: 'googleCalendarOAuth2Api',
required: true,
},
}
],
properties: [
{
@@ -60,7 +60,7 @@ export class GoogleCalendar implements INodeType {
},
],
default: 'event',
description: 'The resource to operate on.',
description: 'The resource to operate on.'
},
...eventOperations,
...eventFields,
@@ -71,55 +71,70 @@ export class GoogleCalendar implements INodeType {
loadOptions: {
// Get all the calendars to display them to user so that he can
// select them easily
async getCalendars(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
async getCalendars(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const calendars = await googleApiRequestAllItems.call(this, 'items', 'GET', '/calendar/v3/users/me/calendarList');
const calendars = await googleApiRequestAllItems.call(
this,
'items',
'GET',
'/calendar/v3/users/me/calendarList'
);
for (const calendar of calendars) {
const calendarName = calendar.summary;
const calendarId = calendar.id;
returnData.push({
name: calendarName,
value: calendarId,
value: calendarId
});
}
return returnData;
},
// Get all the colors to display them to user so that he can
// select them easily
async getColors(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
async getColors(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors');
for (const key of Object.keys(calendar)) {
const colorName = calendar[key].background;
const { event } = await googleApiRequest.call(
this,
'GET',
'/calendar/v3/colors'
);
for (const key of Object.keys(event)) {
const colorName = `Background: ${event[key].background} - Foreground: ${event[key].foreground}`;
const colorId = key;
returnData.push({
name: `${colorName} - ${colorId}`,
value: colorId,
name: `${colorName}`,
value: colorId
});
}
return returnData;
},
// Get all the timezones to display them to user so that he can
// select them easily
async getTimezones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
async getTimezones(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
value: timezoneId
});
}
return returnData;
},
},
}
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = (items.length as unknown) as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
@@ -131,8 +146,14 @@ export class GoogleCalendar implements INodeType {
const calendarId = this.getNodeParameter('calendar', i) as string;
const start = this.getNodeParameter('start', i) as string;
const end = this.getNodeParameter('end', i) as string;
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const useDefaultReminders = this.getNodeParameter(
'useDefaultReminders',
i
) as boolean;
const additionalFields = this.getNodeParameter(
'additionalFields',
i
) as IDataObject;
if (additionalFields.maxAttendees) {
qs.maxAttendees = additionalFields.maxAttendees as number;
}
@@ -145,17 +166,19 @@ export class GoogleCalendar implements INodeType {
const body: IEvent = {
start: {
dateTime: start,
timeZone: additionalFields.timeZone || this.getTimezone(),
timeZone: additionalFields.timeZone || this.getTimezone()
},
end: {
dateTime: end,
timeZone: additionalFields.timeZone || this.getTimezone(),
timeZone: additionalFields.timeZone || this.getTimezone()
}
};
if (additionalFields.attendees) {
body.attendees = (additionalFields.attendees as string[]).map(attendee => {
return { email: attendee };
});
body.attendees = (additionalFields.attendees as string[]).map(
attendee => {
return { email: attendee };
}
);
}
if (additionalFields.color) {
body.colorId = additionalFields.color as string;
@@ -188,9 +211,12 @@ export class GoogleCalendar implements INodeType {
body.visibility = additionalFields.visibility as string;
}
if (!useDefaultReminders) {
const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[];
const reminders = (this.getNodeParameter(
'remindersUi',
i
) as IDataObject).remindersValues as IDataObject[];
body.reminders = {
useDefault: false,
useDefault: false
};
if (reminders) {
body.reminders.overrides = reminders;
@@ -198,32 +224,54 @@ export class GoogleCalendar implements INodeType {
}
if (additionalFields.allday) {
body.start = {
date: moment(start).utc().format('YYYY-MM-DD'),
date: moment(start)
.utc()
.format('YYYY-MM-DD')
};
body.end = {
date: moment(end).utc().format('YYYY-MM-DD'),
date: moment(end)
.utc()
.format('YYYY-MM-DD')
};
}
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
body.recurrence = [];
if (additionalFields.repeatHowManyTimes
&& additionalFields.repeatUntil) {
throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`);
if (
additionalFields.repeatHowManyTimes &&
additionalFields.repeatUntil
) {
throw new Error(
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`
);
}
if (additionalFields.repeatFrecuency) {
body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`);
body.recurrence?.push(
`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`
);
}
if (additionalFields.repeatHowManyTimes) {
body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`);
body.recurrence?.push(
`COUNT=${additionalFields.repeatHowManyTimes};`
);
}
if (additionalFields.repeatUntil) {
body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`);
body.recurrence?.push(
`UNTIL=${moment(additionalFields.repeatUntil as string)
.utc()
.format('YYYYMMDDTHHmmss')}Z`
);
}
if (body.recurrence.length !== 0) {
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
}
responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs);
responseData = await googleApiRequest.call(
this,
'POST',
`/calendar/v3/calendars/${calendarId}/events`,
body,
qs
);
}
//https://developers.google.com/calendar/v3/reference/events/delete
if (operation === 'delete') {
@@ -233,8 +281,13 @@ export class GoogleCalendar implements INodeType {
if (options.sendUpdates) {
qs.sendUpdates = options.sendUpdates as number;
}
responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {});
responseData = { success: true };
responseData = await googleApiRequest.call(
this,
'DELETE',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
{}
);
responseData = { success: true };
}
//https://developers.google.com/calendar/v3/reference/events/get
if (operation === 'get') {
@@ -247,7 +300,13 @@ export class GoogleCalendar implements INodeType {
if (options.timeZone) {
qs.timeZone = options.timeZone as string;
}
responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs);
responseData = await googleApiRequest.call(
this,
'GET',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
{},
qs
);
}
//https://developers.google.com/calendar/v3/reference/events/list
if (operation === 'getAll') {
@@ -288,10 +347,23 @@ export class GoogleCalendar implements INodeType {
qs.updatedMin = options.updatedMin as string;
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs);
responseData = await googleApiRequestAllItems.call(
this,
'items',
'GET',
`/calendar/v3/calendars/${calendarId}/events`,
{},
qs
);
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs);
responseData = await googleApiRequest.call(
this,
'GET',
`/calendar/v3/calendars/${calendarId}/events`,
{},
qs
);
responseData = responseData.items;
}
}
@@ -299,8 +371,14 @@ export class GoogleCalendar implements INodeType {
if (operation === 'update') {
const calendarId = this.getNodeParameter('calendar', i) as string;
const eventId = this.getNodeParameter('eventId', i) as string;
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const useDefaultReminders = this.getNodeParameter(
'useDefaultReminders',
i
) as boolean;
const updateFields = this.getNodeParameter(
'updateFields',
i
) as IDataObject;
if (updateFields.maxAttendees) {
qs.maxAttendees = updateFields.maxAttendees as number;
}
@@ -314,19 +392,21 @@ export class GoogleCalendar implements INodeType {
if (updateFields.start) {
body.start = {
dateTime: updateFields.start,
timeZone: updateFields.timeZone || this.getTimezone(),
timeZone: updateFields.timeZone || this.getTimezone()
};
}
if (updateFields.end) {
body.end = {
dateTime: updateFields.end,
timeZone: updateFields.timeZone || this.getTimezone(),
timeZone: updateFields.timeZone || this.getTimezone()
};
}
if (updateFields.attendees) {
body.attendees = (updateFields.attendees as string[]).map(attendee => {
return { email: attendee };
});
body.attendees = (updateFields.attendees as string[]).map(
attendee => {
return { email: attendee };
}
);
}
if (updateFields.color) {
body.colorId = updateFields.color as string;
@@ -359,46 +439,64 @@ export class GoogleCalendar implements INodeType {
body.visibility = updateFields.visibility as string;
}
if (!useDefaultReminders) {
const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[];
const reminders = (this.getNodeParameter(
'remindersUi',
i
) as IDataObject).remindersValues as IDataObject[];
body.reminders = {
useDefault: false,
useDefault: false
};
if (reminders) {
body.reminders.overrides = reminders;
}
}
if (updateFields.allday
&& updateFields.start
&& updateFields.end) {
if (updateFields.allday && updateFields.start && updateFields.end) {
body.start = {
date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'),
date: moment(updateFields.start as string)
.utc()
.format('YYYY-MM-DD')
};
body.end = {
date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'),
date: moment(updateFields.end as string)
.utc()
.format('YYYY-MM-DD')
};
}
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
body.recurrence = [];
if (updateFields.repeatHowManyTimes
&& updateFields.repeatUntil) {
throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`);
if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) {
throw new Error(
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`
);
}
if (updateFields.repeatFrecuency) {
body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`);
body.recurrence?.push(
`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`
);
}
if (updateFields.repeatHowManyTimes) {
body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`);
}
if (updateFields.repeatUntil) {
body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`);
body.recurrence?.push(
`UNTIL=${moment(updateFields.repeatUntil as string)
.utc()
.format('YYYYMMDDTHHmmss')}Z`
);
}
if (body.recurrence.length !== 0) {
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
} else {
delete body.recurrence;
}
responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs);
responseData = await googleApiRequest.call(
this,
'PATCH',
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
body,
qs
);
}
}
}

View File

@@ -0,0 +1,142 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
import * as moment from 'moment-timezone';
import * as jwt from 'jsonwebtoken';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string;
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://www.googleapis.com${resource}`,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
if (authenticationMethod === 'serviceAccount') {
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const { access_token } = await getAccessToken.call(this, credentials as IDataObject);
options.headers!.Authorization = `Bearer ${access_token}`;
//@ts-ignore
return await this.helpers.request(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'googleDriveOAuth2Api', options);
}
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
let errorMessages;
if (error.response.body.error.errors) {
// Try to return the error prettier
errorMessages = error.response.body.error.errors;
errorMessages = errorMessages.map((errorItem: IDataObject) => errorItem.message);
errorMessages = errorMessages.join('|');
} else if (error.response.body.error.message) {
errorMessages = error.response.body.error.message;
}
throw new Error(`Google Drive error response [${error.statusCode}]: ${errorMessages}`);
}
throw error;
}
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.maxResults = 100;
do {
responseData = await googleApiRequest.call(this, method, endpoint, body, query);
query.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['nextPageToken'] !== undefined &&
responseData['nextPageToken'] !== ''
);
return returnData;
}
function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise<IDataObject> {
//https://developers.google.com/identity/protocols/oauth2/service-account#httprest
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.photos.readonly',
];
const now = moment().unix();
const signature = jwt.sign(
{
'iss': credentials.email as string,
'sub': credentials.email as string,
'scope': scopes.join(' '),
'aud': `https://oauth2.googleapis.com/token`,
'iat': now,
'exp': now + 3600,
},
credentials.privateKey as string,
{
algorithm: 'RS256',
header: {
'kid': credentials.privateKey as string,
'typ': 'JWT',
'alg': 'RS256',
},
}
);
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
form: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: signature,
},
uri: 'https://oauth2.googleapis.com/token',
json: true
};
//@ts-ignore
return this.helpers.request(options);
}

View File

@@ -1,10 +1,8 @@
import { google } from 'googleapis';
const { Readable } = require('stream');
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@@ -12,8 +10,9 @@ import {
INodeType,
} from 'n8n-workflow';
import { getAuthenticationClient } from '../GoogleApi';
import {
googleApiRequest,
} from './GenericFunctions';
export class GoogleDrive implements INodeType {
description: INodeTypeDescription = {
@@ -34,9 +33,43 @@ export class GoogleDrive implements INodeType {
{
name: 'googleApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'serviceAccount',
],
},
},
},
{
name: 'googleDriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Service Account',
value: 'serviceAccount',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'serviceAccount',
},
{
displayName: 'Resource',
name: 'resource',
@@ -764,7 +797,7 @@ export class GoogleDrive implements INodeType {
{
name: 'domain',
value: 'domain',
description:"All files shared to the user's domain that are searchable",
description: 'All files shared to the user\'s domain that are searchable',
},
{
name: 'drive',
@@ -813,26 +846,6 @@ export class GoogleDrive implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const scopes = [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/drive.appdata',
'https://www.googleapis.com/auth/drive.photos.readonly',
];
const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes);
const drive = google.drive({
version: 'v3',
// @ts-ignore
auth: client,
});
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
@@ -857,22 +870,20 @@ export class GoogleDrive implements INodeType {
const fileId = this.getNodeParameter('fileId', i) as string;
const copyOptions = {
fileId,
const body: IDataObject = {
fields: queryFields,
requestBody: {} as IDataObject,
};
const optionProperties = ['name', 'parents'];
for (const propertyName of optionProperties) {
if (options[propertyName] !== undefined) {
copyOptions.requestBody[propertyName] = options[propertyName];
body[propertyName] = options[propertyName];
}
}
const response = await drive.files.copy(copyOptions);
const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/copy`, body);
returnData.push(response.data as IDataObject);
returnData.push(response as IDataObject);
} else if (operation === 'download') {
// ----------------------------------
@@ -881,15 +892,13 @@ export class GoogleDrive implements INodeType {
const fileId = this.getNodeParameter('fileId', i) as string;
const response = await drive.files.get(
{
fileId,
alt: 'media',
},
{
responseType: 'arraybuffer',
},
);
const requestOptions = {
resolveWithFullResponse: true,
encoding: null,
json: false,
};
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions);
let mimeType: string | undefined;
if (response.headers['content-type']) {
@@ -912,7 +921,7 @@ export class GoogleDrive implements INodeType {
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const data = Buffer.from(response.data as string);
const data = Buffer.from(response.body as string);
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType);
@@ -936,7 +945,7 @@ export class GoogleDrive implements INodeType {
queryCorpora = options.corpora as string;
}
let driveId : string | undefined;
let driveId: string | undefined;
driveId = options.driveId as string;
if (driveId === '') {
driveId = undefined;
@@ -988,20 +997,19 @@ export class GoogleDrive implements INodeType {
const pageSize = this.getNodeParameter('limit', i) as number;
const res = await drive.files.list({
const qs = {
pageSize,
orderBy: 'modifiedTime',
fields: `nextPageToken, files(${queryFields})`,
spaces: querySpaces,
corpora: queryCorpora,
driveId,
q: queryString,
includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''), // Actually depracated,
supportsAllDrives: (queryCorpora !== '' || driveId !== ''), // see https://developers.google.com/drive/api/v3/reference/files/list
// However until June 2020 still needs to be set, to avoid API errors.
});
includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''),
supportsAllDrives: (queryCorpora !== '' || driveId !== ''),
};
const files = res!.data.files;
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files`, {}, qs);
const files = response!.files;
return [this.helpers.returnJsonArray(files as IDataObject[])];
@@ -1044,29 +1052,35 @@ export class GoogleDrive implements INodeType {
const name = this.getNodeParameter('name', i) as string;
const parents = this.getNodeParameter('parents', i) as string[];
const response = await drive.files.create({
requestBody: {
name,
originalFilename,
parents,
},
let qs: IDataObject = {
fields: queryFields,
media: {
mimeType,
body: ((buffer: Buffer) => {
const readableInstanceStream = new Readable({
read() {
this.push(buffer);
this.push(null);
}
});
uploadType: 'media',
};
return readableInstanceStream;
})(body),
const requestOptions = {
headers: {
'Content-Type': mimeType,
'Content-Length': body.byteLength,
},
});
encoding: null,
json: false,
};
returnData.push(response.data as IDataObject);
let response = await googleApiRequest.call(this, 'POST', `/upload/drive/v3/files`, body, qs, undefined, requestOptions);
body = {
mimeType,
name,
originalFilename,
};
qs = {
addParents: parents.join(','),
};
response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${JSON.parse(response).id}`, body, qs);
returnData.push(response as IDataObject);
}
} else if (resource === 'folder') {
@@ -1077,19 +1091,19 @@ export class GoogleDrive implements INodeType {
const name = this.getNodeParameter('name', i) as string;
const fileMetadata = {
const body = {
name,
mimeType: 'application/vnd.google-apps.folder',
parents: options.parents || [],
};
const response = await drive.files.create({
// @ts-ignore
resource: fileMetadata,
const qs = {
fields: queryFields,
});
};
returnData.push(response.data as IDataObject);
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs);
returnData.push(response as IDataObject);
}
}
if (['file', 'folder'].includes(resource)) {
@@ -1100,9 +1114,7 @@ export class GoogleDrive implements INodeType {
const fileId = this.getNodeParameter('fileId', i) as string;
await drive.files.delete({
fileId,
});
const response = await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`);
// If we are still here it did succeed
returnData.push({

View File

@@ -1,23 +0,0 @@
import { JWT } from 'google-auth-library';
import { google } from 'googleapis';
/**
* Returns the authentication client needed to access spreadsheet
*/
export async function getAuthenticationClient(email: string, privateKey: string, scopes: string[]): Promise <JWT> {
const client = new google.auth.JWT(
email,
undefined,
privateKey,
scopes,
undefined
);
// TODO: Check later if this or the above should be cached
await client.authorize();
// @ts-ignore
return client;
}

View File

@@ -622,7 +622,7 @@ export class GoogleSheets implements INodeType {
// ----------------------------------
// append
// ----------------------------------
const keyRow = this.getNodeParameter('keyRow', 0) as number;
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const items = this.getInputData();
@@ -670,7 +670,7 @@ export class GoogleSheets implements INodeType {
sheetId: range.sheetId,
dimension: deletePropertyToDimensions[propertyName] as string,
startIndex: range.startIndex,
endIndex: range.startIndex + range.amount,
endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10),
}
}
});
@@ -693,8 +693,8 @@ export class GoogleSheets implements INodeType {
return [];
}
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
const keyRow = this.getNodeParameter('keyRow', 0) as number;
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const items = this.getInputData();
@@ -735,8 +735,8 @@ export class GoogleSheets implements INodeType {
}
];
} else {
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
const keyRow = this.getNodeParameter('keyRow', 0) as number;
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
}
@@ -769,8 +769,8 @@ export class GoogleSheets implements INodeType {
const data = await sheet.batchUpdate(updateData, valueInputMode);
} else {
const keyName = this.getNodeParameter('key', 0) as string;
const keyRow = this.getNodeParameter('keyRow', 0) as number;
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const setData: IDataObject[] = [];
items.forEach((item) => {

View File

@@ -0,0 +1,92 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: string,
resource: string,
body: IDataObject = {},
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://www.googleapis.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;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(
this,
'googleTasksOAuth2Api',
options
);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
let errors = error.response.body.error.errors;
errors = errors.map((e: IDataObject) => e.message);
// Try to return the error prettier
throw new Error(
`Google Tasks error response [${error.statusCode}]: ${errors.join('|')}`
);
}
throw error;
}
}
export async function googleApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: string,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {}
): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.maxResults = 100;
do {
responseData = await googleApiRequest.call(
this,
method,
endpoint,
body,
query
);
query.pageToken = responseData['nextPageToken'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['nextPageToken'] !== undefined &&
responseData['nextPageToken'] !== ''
);
return returnData;
}

View File

@@ -0,0 +1,279 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
googleApiRequest,
googleApiRequestAllItems,
} from './GenericFunctions';
import {
taskOperations,
taskFields,
} from './TaskDescription';
export class GoogleTasks implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Tasks',
name: 'googleTasks',
icon: 'file:googleTasks.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Google Tasks API.',
defaults: {
name: 'Google Tasks',
color: '#3E87E4'
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleTasksOAuth2Api',
required: true
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Task',
value: 'task'
}
],
default: 'task',
description: 'The resource to operate on.'
},
...taskOperations,
...taskFields
]
};
methods = {
loadOptions: {
// Get all the tasklists to display them to user so that he can select them easily
async getTasks(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tasks = await googleApiRequestAllItems.call(
this,
'items',
'GET',
'/tasks/v1/users/@me/lists'
);
for (const task of tasks) {
const taskName = task.title;
const taskId = task.id;
returnData.push({
name: taskName,
value: taskId
});
}
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;
let body: IDataObject = {};
for (let i = 0; i < length; i++) {
if (resource === 'task') {
if (operation === 'create') {
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
) as IDataObject;
if (additionalFields.parent) {
qs.parent = additionalFields.parent as string;
}
if (additionalFields.previous) {
qs.previous = additionalFields.previous as string;
}
if (additionalFields.status) {
body.status = additionalFields.status as string;
}
if (additionalFields.notes) {
body.notes = additionalFields.notes as string;
}
if (additionalFields.dueDate) {
body.dueDate = additionalFields.dueDate as string;
}
if (additionalFields.completed) {
body.completed = additionalFields.completed as string;
}
if (additionalFields.deleted) {
body.deleted = additionalFields.deleted as boolean;
}
responseData = await googleApiRequest.call(
this,
'POST',
`/tasks/v1/lists/${taskId}/tasks`,
body,
qs
);
}
if (operation === 'delete') {
//https://developers.google.com/tasks/v1/reference/tasks/delete
const taskListId = this.getNodeParameter('task', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
responseData = await googleApiRequest.call(
this,
'DELETE',
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
{}
);
responseData = { success: true };
}
if (operation === 'get') {
//https://developers.google.com/tasks/v1/reference/tasks/get
const taskListId = this.getNodeParameter('task', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
responseData = await googleApiRequest.call(
this,
'GET',
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
{},
qs
);
}
if (operation === 'getAll') {
//https://developers.google.com/tasks/v1/reference/tasks/list
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const taskListId = this.getNodeParameter('task', i) as string;
const options = this.getNodeParameter(
'additionalFields',
i
) as IDataObject;
if (options.completedMax) {
qs.completedMax = options.completedMax as string;
}
if (options.completedMin) {
qs.completedMin = options.completedMin as string;
}
if (options.dueMin) {
qs.dueMin = options.dueMin as string;
}
if (options.dueMax) {
qs.dueMax = options.dueMax as string;
}
if (options.showCompleted) {
qs.showCompleted = options.showCompleted as boolean;
}
if (options.showDeleted) {
qs.showDeleted = options.showDeleted as boolean;
}
if (options.showHidden) {
qs.showHidden = options.showHidden as boolean;
}
if (options.updatedMin) {
qs.updatedMin = options.updatedMin as string;
}
if (returnAll) {
responseData = await googleApiRequestAllItems.call(
this,
'items',
'GET',
`/tasks/v1/lists/${taskListId}/tasks`,
{},
qs
);
} else {
qs.maxResults = this.getNodeParameter('limit', i) as number;
responseData = await googleApiRequest.call(
this,
'GET',
`/tasks/v1/lists/${taskListId}/tasks`,
{},
qs
);
responseData = responseData.items;
}
}
if (operation === 'update') {
body = {};
//https://developers.google.com/tasks/v1/reference/tasks/patch
const taskListId = this.getNodeParameter('task', i) as string;
const taskId = this.getNodeParameter('taskId', i) as string;
const updateFields = this.getNodeParameter(
'updateFields',
i
) as IDataObject;
if (updateFields.previous) {
qs.previous = updateFields.previous as string;
}
if (updateFields.status) {
body.status = updateFields.status as string;
}
if (updateFields.notes) {
body.notes = updateFields.notes as string;
}
if (updateFields.title) {
body.title = updateFields.title as string;
}
if (updateFields.dueDate) {
body.dueDate = updateFields.dueDate as string;
}
if (updateFields.completed) {
body.completed = updateFields.completed as string;
}
if (updateFields.deleted) {
body.deleted = updateFields.deleted as boolean;
}
responseData = await googleApiRequest.call(
this,
'PATCH',
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
body,
qs
);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View File

@@ -0,0 +1,493 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const taskOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'task',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Add a task to tasklist',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a task',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve a task',
},
{
name: 'Get All',
value: 'getAll',
description: 'Retrieve all tasks from a tasklist',
},
{
name: 'Update',
value: 'update',
description: 'Update a task',
}
],
default: 'create',
description: 'The operation to perform.',
}
] as INodeProperties[];
export const taskFields = [
/* -------------------------------------------------------------------------- */
/* task:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'TaskList',
name: 'task',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTasks',
},
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'task',
],
},
},
default: '',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the task.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'task',
],
}
},
options: [
{
displayName: 'Completion Date',
name: 'completed',
type: 'dateTime',
default: '',
description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`,
},
{
displayName: 'Deleted',
name: 'deleted',
type: 'boolean',
default: false,
description: 'Flag indicating whether the task has been deleted.',
},
{
displayName: 'Due Date',
name: 'dueDate',
type: 'dateTime',
default: '',
description: 'Due date of the task.',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Additional Notes.',
},
{
displayName: 'Parent',
name: 'parent',
type: 'string',
default: '',
description: 'Parent task identifier. If the task is created at the top level, this parameter is omitted.',
},
{
displayName: 'Previous',
name: 'previous',
type: 'string',
default: '',
description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Needs Action',
value: 'needsAction',
},
{
name: 'Completed',
value: 'completed',
}
],
default: '',
description: 'Current status of the task.',
},
],
},
/* -------------------------------------------------------------------------- */
/* task:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'TaskList',
name: 'task',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTasks',
},
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'task',
],
},
},
default: '',
},
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'task',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* task:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'TaskList',
name: 'task',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTasks',
},
required: true,
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'task',
],
}
},
default: '',
},
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'task',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* task:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'TaskList',
name: 'task',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTasks',
},
required: true,
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
},
},
default: '',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
},
},
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: [
'task',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100
},
default: 20,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
},
},
options: [
{
displayName: 'Completed Max',
name: 'completedMax',
type: 'dateTime',
default: '',
description: 'Upper bound for a task completion date (as a RFC 3339 timestamp) to filter by.',
},
{
displayName: 'Completed Min',
name: 'completedMin',
type: 'dateTime',
default: '',
description: 'Lower bound for a task completion date (as a RFC 3339 timestamp) to filter by.',
},
{
displayName: 'Due Min',
name: 'dueMin',
type: 'dateTime',
default: '',
description: 'Lower bound for a task due date (as a RFC 3339 timestamp) to filter by.',
},
{
displayName: 'Due Max',
name: 'dueMax',
type: 'dateTime',
default: '',
description: 'Upper bound for a task due date (as a RFC 3339 timestamp) to filter by.',
},
{
displayName: 'Show Completed',
name: 'showCompleted',
type: 'boolean',
default: true,
description: 'Flag indicating whether completed tasks are returned in the result',
},
{
displayName: 'Show Deleted',
name: 'showDeleted',
type: 'boolean',
default: false,
description: 'Flag indicating whether deleted tasks are returned in the result',
},
{
displayName: 'Show Hidden',
name: 'showHidden',
type: 'boolean',
default: false,
description: 'Flag indicating whether hidden tasks are returned in the result',
},
{
displayName: 'Updated Min',
name: 'updatedMin',
type: 'dateTime',
default: '',
description: 'Lower bound for a task last modification time (as a RFC 3339 timestamp) to filter by.',
},
]
},
/* -------------------------------------------------------------------------- */
/* task:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'TaskList',
name: 'task',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTasks',
},
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'task',
],
},
},
default: '',
},
{
displayName: 'Task ID',
name: 'taskId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'task',
],
},
},
default: '',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Update Field',
default: {},
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'task',
],
}
},
options: [
{
displayName: 'Completion Date',
name: 'completed',
type: 'dateTime',
default: '',
description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`,
},
{
displayName: 'Deleted',
name: 'deleted',
type: 'boolean',
default: false,
description: 'Flag indicating whether the task has been deleted.',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Additional Notes.',
},
{
displayName: 'Previous',
name: 'previous',
type: 'string',
default: '',
description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Needs Update',
value: 'needsAction',
},
{
name: 'Completed',
value: 'completed',
}
],
default: '',
description: 'Current status of the task.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the task.',
},
],
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -802,7 +802,7 @@ export class HttpRequest implements INodeType {
if (oAuth2Api !== undefined) {
//@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
} else {
response = await this.helpers.request(requestOptions);
}

View File

@@ -15,11 +15,12 @@ import {
export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise<any> { // tslint:disable-line:no-any
const node = this.getNode();
const credentialName = Object.keys(node.credentials!)[0];
const credentials = this.getCredentials(credentialName);
let authenticationMethod = this.getNodeParameter('authentication', 0);
if (this.getNode().type.includes('Trigger')) {
authenticationMethod = 'developerApi';
}
query!.hapikey = credentials!.apiKey as string;
const options: OptionsWithUri = {
method,
qs: query,
@@ -28,18 +29,42 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions
json: true,
useQuerystring: true,
};
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'apiKey') {
const credentials = this.getCredentials('hubspotApi');
options.qs.hapikey = credentials!.apiKey as string;
return await this.helpers.request!(options);
} else if (authenticationMethod === 'developerApi') {
const credentials = this.getCredentials('hubspotDeveloperApi');
options.qs.hapikey = credentials!.apiKey as string;
return await this.helpers.request!(options);
} else {
// @ts-ignore
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, 'Bearer');
}
} catch (error) {
if (error.response && error.response.body && error.response.body.errors) {
// Try to return the error prettier
let errorMessages = error.response.body.errors;
let errorMessages;
if (errorMessages[0].message) {
// @ts-ignore
errorMessages = errorMessages.map(errorItem => errorItem.message);
if (error.response && error.response.body) {
if (error.response.body.message) {
errorMessages = [error.response.body.message];
} else if (error.response.body.errors) {
// Try to return the error prettier
errorMessages = error.response.body.errors;
if (errorMessages[0].message) {
// @ts-ignore
errorMessages = errorMessages.map(errorItem => errorItem.message);
}
}
throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessages.join('|')}`);
}

View File

@@ -73,9 +73,44 @@ export class Hubspot implements INodeType {
{
name: 'hubspotApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'hubspotOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Key',
value: 'apiKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiKey',
description: 'The method of authentication.',
},
{
displayName: 'Resource',
name: 'resource',

View File

@@ -246,7 +246,13 @@ export class HubspotTrigger implements INodeType {
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const credentials = this.getCredentials('hubspotDeveloperApi');
const credentials = this.getCredentials('hubspotDeveloperApi') as IDataObject;
if (credentials === undefined) {
throw new Error('No credentials found!');
}
const req = this.getRequestObject();
const bodyData = req.body;
const headerData = this.getHeaderData();
@@ -254,12 +260,18 @@ export class HubspotTrigger implements INodeType {
if (headerData['x-hubspot-signature'] === undefined) {
return {};
}
const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`;
const signature = createHash('sha256').update(hash).digest('hex');
//@ts-ignore
if (signature !== headerData['x-hubspot-signature']) {
return {};
// check signare if client secret is defined
if (credentials.clientSecret !== '') {
const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`;
const signature = createHash('sha256').update(hash).digest('hex');
//@ts-ignore
if (signature !== headerData['x-hubspot-signature']) {
return {};
}
}
for (let i = 0; i < bodyData.length; i++) {
const subscriptionType = bodyData[i].subscriptionType as string;
if (subscriptionType.includes('contact')) {

View File

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

View File

@@ -1,5 +1,5 @@
import {
OptionsWithUri,
OptionsWithUrl,
} from 'request';
import {
@@ -14,37 +14,53 @@ import {
} from 'n8n-workflow';
export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {} ,headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('mailchimpApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const headerWithAuthentication = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` });
if (!(credentials.apiKey as string).includes('-')) {
throw new Error('The API key is not valid!');
}
const datacenter = (credentials.apiKey as string).split('-').pop();
const authenticationMethod = this.getNodeParameter('authentication', 0) as string;
const host = 'api.mailchimp.com/3.0';
const options: OptionsWithUri = {
headers: headerWithAuthentication,
const options: OptionsWithUrl = {
headers: {
'Accept': 'application/json'
},
method,
qs,
uri: `https://${datacenter}.${host}${endpoint}`,
body,
url: ``,
json: true,
};
if (Object.keys(body).length !== 0) {
options.body = body;
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'apiKey') {
const credentials = this.getCredentials('mailchimpApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.headers = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` });
if (!(credentials.apiKey as string).includes('-')) {
throw new Error('The API key is not valid!');
}
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;
const { api_endpoint } = await getMetadata.call(this, credentials.oauthTokenData as IDataObject);
options.url = `${api_endpoint}/3.0${endpoint}`;
//@ts-ignore
return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, 'Bearer');
}
} catch (error) {
if (error.response.body && error.response.body.detail) {
if (error.respose && error.response.body && error.response.body.detail) {
throw new Error(`Mailchimp Error response [${error.statusCode}]: ${error.response.body.detail}`);
}
throw error;
@@ -80,3 +96,17 @@ export function validateJSON(json: string | undefined): any { // tslint:disable-
}
return result;
}
function getMetadata(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, oauthTokenData: IDataObject) {
const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject;
const options: OptionsWithUrl = {
headers: {
'Accept': 'application/json',
'Authorization': `OAuth ${oauthTokenData.access_token}`,
},
method: 'GET',
url: credentials.metadataUrl as string,
json: true,
};
return this.helpers.request!(options);
}

View File

@@ -47,6 +47,7 @@ interface ICreateMemberBody {
timestamp_opt?: string;
tags?: string[];
merge_fields?: IDataObject;
interests?: IDataObject;
}
export class Mailchimp implements INodeType {
@@ -69,14 +70,53 @@ export class Mailchimp implements INodeType {
{
name: 'mailchimpApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'mailchimpOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Key',
value: 'apiKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiKey',
description: 'Method of authentication.',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'List Group',
value: 'listGroup',
},
{
name: 'Member',
value: 'member',
@@ -159,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 */
/* -------------------------------------------------------------------------- */
@@ -221,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: '',
@@ -252,7 +309,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource:[
@@ -289,12 +345,10 @@ export class Mailchimp implements INodeType {
{
name: 'HTML',
value: 'html',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
@@ -461,7 +515,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
@@ -484,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:[
@@ -737,12 +869,10 @@ export class Mailchimp implements INodeType {
{
name: 'HTML',
value: 'html',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
@@ -756,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: '',
@@ -839,7 +964,6 @@ export class Mailchimp implements INodeType {
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource:[
@@ -876,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',
@@ -989,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: '',
@@ -1084,7 +1259,6 @@ export class Mailchimp implements INodeType {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
@@ -1107,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:[
@@ -1215,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.',
},
],
};
@@ -1226,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;
@@ -1254,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;
},
}
};
@@ -1267,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') {
@@ -1328,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);
}
@@ -1469,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);
}
@@ -1536,6 +1884,7 @@ export class Mailchimp implements INodeType {
responseData = { success: true };
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {

View File

@@ -33,7 +33,25 @@ export class MailchimpTrigger implements INodeType {
{
name: 'mailchimpApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'mailchimpOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -50,6 +68,23 @@ export class MailchimpTrigger 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: 'List',
name: 'list',

View File

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

View File

@@ -226,6 +226,94 @@ export const contactFields = [
},
},
options: [
{
displayName: 'Address',
name: 'addressUi',
placeholder: 'Address',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'addressValues',
displayName: 'Address',
values: [
{
displayName: 'Address Line 1',
name: 'address1',
type: 'string',
default: '',
},
{
displayName: 'Address Line 2',
name: 'address2',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'Zip Code',
name: 'zipCode',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'B2B or B2C',
name: 'b2bOrb2c',
type: 'options',
options: [
{
name: 'B2B',
value: 'B2B',
},
{
name: 'B2C',
value: 'B2C',
},
],
default: '',
},
{
displayName: 'CRM ID',
name: 'crmId',
type: 'string',
default: '',
},
{
displayName: 'Fax',
name: 'fax',
type: 'string',
default: '',
},
{
displayName: 'Has Purchased',
name: 'hasPurchased',
type: 'boolean',
default: false,
},
{
displayName: 'IP Address',
name: 'ipAddress',
@@ -240,6 +328,12 @@ export const contactFields = [
default: '',
description: 'Date/time in UTC;',
},
{
displayName: 'Mobile',
name: 'mobile',
type: 'string',
default: '',
},
{
displayName: 'Owner ID',
name: 'ownerId',
@@ -247,6 +341,112 @@ export const contactFields = [
default: '',
description: 'ID of a Mautic user to assign this contact to',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
},
{
displayName: 'Prospect or Customer',
name: 'prospectOrCustomer',
type: 'options',
options: [
{
name: 'Prospect',
value: 'Prospect',
},
{
name: 'Customer',
value: 'Customer',
},
],
default: '',
},
{
displayName: 'Sandbox',
name: 'sandbox',
type: 'boolean',
default: false,
},
{
displayName: 'Stage',
name: 'stage',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getStages',
},
default: '',
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: '',
},
{
displayName: 'Social Media',
name: 'socialMediaUi',
placeholder: 'Social Media',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'socialMediaValues',
displayName: 'Social Media',
values: [
{
displayName: 'Facebook',
name: 'facebook',
type: 'string',
default: '',
},
{
displayName: 'Foursquare',
name: 'foursquare',
type: 'string',
default: '',
},
{
displayName: 'Instagram',
name: 'instagram',
type: 'string',
default: '',
},
{
displayName: 'LinkedIn',
name: 'linkedIn',
type: 'string',
default: '',
},
{
displayName: 'Skype',
name: 'skype',
type: 'string',
default: '',
},
{
displayName: 'Twitter',
name: 'twitter',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
},
],
},
@@ -318,6 +518,103 @@ export const contactFields = [
default: '',
description: 'Contact parameters',
},
{
displayName: 'Address',
name: 'addressUi',
placeholder: 'Address',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: {},
options: [
{
name: 'addressValues',
displayName: 'Address',
values: [
{
displayName: 'Address Line 1',
name: 'address1',
type: 'string',
default: '',
},
{
displayName: 'Address Line 2',
name: 'address2',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
{
displayName: 'Zip Code',
name: 'zipCode',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'B2B or B2C',
name: 'b2bOrb2c',
type: 'options',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
options: [
{
name: 'B2B',
value: 'B2B',
},
{
name: 'B2C',
value: 'B2C',
},
],
default: '',
},
{
displayName: 'CRM ID',
name: 'crmId',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
},
{
displayName: 'Email',
name: 'email',
@@ -332,6 +629,19 @@ export const contactFields = [
default: '',
description: 'Email address of the contact.',
},
{
displayName: 'Fax',
name: 'fax',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
},
{
displayName: 'First Name',
name: 'firstName',
@@ -346,6 +656,47 @@ export const contactFields = [
default: '',
description: 'First Name',
},
{
displayName: 'Has Purchased',
name: 'hasPurchased',
type: 'boolean',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: false,
},
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
description: 'IP address to associate with the contact',
},
{
displayName: 'Last Active',
name: 'lastActive',
type: 'dateTime',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
description: 'Date/time in UTC;',
},
{
displayName: 'Last Name',
name: 'lastName',
@@ -360,6 +711,60 @@ export const contactFields = [
default: '',
description: 'LastName',
},
{
displayName: 'Mobile',
name: 'mobile',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
},
{
displayName: 'Owner ID',
name: 'ownerId',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
description: 'ID of a Mautic user to assign this contact to',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
},
{
displayName: 'Position',
name: 'position',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
description: 'Position',
},
{
displayName: 'Primary Company',
name: 'company',
@@ -378,9 +783,9 @@ export const contactFields = [
description: 'Primary company',
},
{
displayName: 'Position',
name: 'position',
type: 'string',
displayName: 'Prospect or Customer',
name: 'prospectOrCustomer',
type: 'options',
displayOptions: {
show: {
'/jsonParameters': [
@@ -388,8 +793,62 @@ export const contactFields = [
],
},
},
options: [
{
name: 'Prospect',
value: 'Prospect',
},
{
name: 'Customer',
value: 'Customer',
},
],
default: '',
},
{
displayName: 'Sandbox',
name: 'sandbox',
type: 'boolean',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: false,
},
{
displayName: 'Stage',
name: 'stage',
type: 'options',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
typeOptions: {
loadOptionsMethod: 'getStages',
},
default: '',
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: '',
description: 'Position',
},
{
displayName: 'Title',
@@ -405,27 +864,94 @@ export const contactFields = [
default: '',
description: 'Title',
},
{
displayName: 'Social Media',
name: 'socialMediaUi',
placeholder: 'Social Media',
type: 'fixedCollection',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'socialMediaValues',
displayName: 'Social Media',
values: [
{
displayName: 'Facebook',
name: 'facebook',
type: 'string',
default: '',
},
{
displayName: 'Foursquare',
name: 'foursquare',
type: 'string',
default: '',
},
{
displayName: 'Instagram',
name: 'instagram',
type: 'string',
default: '',
},
{
displayName: 'LinkedIn',
name: 'linkedIn',
type: 'string',
default: '',
},
{
displayName: 'Skype',
name: 'skype',
type: 'string',
default: '',
},
{
displayName: 'Twitter',
name: 'twitter',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Website',
name: 'website',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
},
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
displayOptions: {
show: {
'/jsonParameters': [
false,
],
},
},
default: '',
description: 'IP address to associate with the contact',
},
{
displayName: 'Last Active',
name: 'lastActive',
type: 'dateTime',
default: '',
description: 'Date/time in UTC;',
},
{
displayName: 'Owner ID',
name: 'ownerId',
type: 'string',
default: '',
description: 'ID of a Mautic user to assign this contact to',
},
],
},

View File

@@ -10,7 +10,6 @@ import {
import {
IDataObject,
} from 'n8n-workflow';
import { errors } from 'imap-simple';
interface OMauticErrorResponse {
errors: Array<{
@@ -19,7 +18,7 @@ interface OMauticErrorResponse {
}>;
}
function getErrors(error: OMauticErrorResponse): string {
export function getErrors(error: OMauticErrorResponse): string {
const returnErrors: string[] = [];
for (const errorItem of error.errors) {
@@ -31,23 +30,40 @@ function getErrors(error: OMauticErrorResponse): string {
export async function mauticApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('mauticApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials') as string;
const options: OptionsWithUri = {
headers: { Authorization: `Basic ${base64Key}` },
headers: {},
method,
qs: query,
uri: uri || `${credentials.url}/api${endpoint}`,
uri: uri || `/api${endpoint}`,
body,
json: true
};
try {
const returnData = await this.helpers.request!(options);
if (returnData.error) {
try {
let returnData;
if (authenticationMethod === 'credentials') {
const credentials = this.getCredentials('mauticApi') as IDataObject;
const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
options.headers!.Authorization = `Basic ${base64Key}`;
options.uri = `${credentials.url}${options.uri}`;
//@ts-ignore
returnData = await this.helpers.request(options);
} else {
const credentials = this.getCredentials('mauticOAuth2Api') as IDataObject;
options.uri = `${credentials.url}${options.uri}`;
//@ts-ignore
returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options);
}
if (returnData.errors) {
// They seem to to sometimes return 200 status but still error.
throw new Error(getErrors(returnData));
}

View File

@@ -1,5 +1,3 @@
import { snakeCase } from 'change-case';
import {
IExecuteFunctions,
} from 'n8n-core';
@@ -15,12 +13,18 @@ import {
mauticApiRequest,
mauticApiRequestAllItems,
validateJSON,
getErrors,
} from './GenericFunctions';
import {
contactFields,
contactOperations,
} from './ContactDescription';
import {
snakeCase,
} from 'change-case';
export class Mautic implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mautic',
@@ -40,9 +44,43 @@ export class Mautic implements INodeType {
{
name: 'mauticApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'credentials',
],
},
},
},
{
name: 'mauticOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Credentials',
value: 'credentials',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'credentials',
},
{
displayName: 'Resource',
name: 'resource',
@@ -77,6 +115,32 @@ export class Mautic implements INodeType {
}
return returnData;
},
// Get all the available tags to display them to user so that he can
// select them easily
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tags = await mauticApiRequestAllItems.call(this, 'tags', 'GET', '/tags');
for (const tag of tags) {
returnData.push({
name: tag.tag,
value: tag.tag,
});
}
return returnData;
},
// Get all the available stages to display them to user so that he can
// select them easily
async getStages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const stages = await mauticApiRequestAllItems.call(this, 'stages', 'GET', '/stages');
for (const stage of stages) {
returnData.push({
name: stage.name,
value: stage.id,
});
}
return returnData;
},
},
};
@@ -124,6 +188,62 @@ export class Mautic implements INodeType {
if (additionalFields.ownerId) {
body.ownerId = additionalFields.ownerId as string;
}
if (additionalFields.addressUi) {
const addressValues = (additionalFields.addressUi as IDataObject).addressValues as IDataObject;
if (addressValues) {
body.address1 = addressValues.address1 as string;
body.address2 = addressValues.address2 as string;
body.city = addressValues.city as string;
body.state = addressValues.state as string;
body.country = addressValues.country as string;
body.zipcode = addressValues.zipCode as string;
}
}
if (additionalFields.socialMediaUi) {
const socialMediaValues = (additionalFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject;
if (socialMediaValues) {
body.facebook = socialMediaValues.facebook as string;
body.foursquare = socialMediaValues.foursquare as string;
body.instagram = socialMediaValues.instagram as string;
body.linkedin = socialMediaValues.linkedIn as string;
body.skype = socialMediaValues.skype as string;
body.twitter = socialMediaValues.twitter as string;
}
}
if (additionalFields.b2bOrb2c) {
body.b2b_or_b2c = additionalFields.b2bOrb2c as string;
}
if (additionalFields.crmId) {
body.crm_id = additionalFields.crmId as string;
}
if (additionalFields.fax) {
body.fax = additionalFields.fax as string;
}
if (additionalFields.hasPurchased) {
body.haspurchased = additionalFields.hasPurchased as boolean;
}
if (additionalFields.mobile) {
body.mobile = additionalFields.mobile as string;
}
if (additionalFields.phone) {
body.phone = additionalFields.phone as string;
}
if (additionalFields.prospectOrCustomer) {
body.prospect_or_customer = additionalFields.prospectOrCustomer as string;
}
if (additionalFields.sandbox) {
body.sandbox = additionalFields.sandbox as boolean;
}
if (additionalFields.stage) {
body.stage = additionalFields.stage as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string;
}
if (additionalFields.website) {
body.website = additionalFields.website as string;
}
responseData = await mauticApiRequest.call(this, 'POST', '/contacts/new', body);
responseData = responseData.contact;
}
@@ -167,6 +287,61 @@ export class Mautic implements INodeType {
if (updateFields.ownerId) {
body.ownerId = updateFields.ownerId as string;
}
if (updateFields.addressUi) {
const addressValues = (updateFields.addressUi as IDataObject).addressValues as IDataObject;
if (addressValues) {
body.address1 = addressValues.address1 as string;
body.address2 = addressValues.address2 as string;
body.city = addressValues.city as string;
body.state = addressValues.state as string;
body.country = addressValues.country as string;
body.zipcode = addressValues.zipCode as string;
}
}
if (updateFields.socialMediaUi) {
const socialMediaValues = (updateFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject;
if (socialMediaValues) {
body.facebook = socialMediaValues.facebook as string;
body.foursquare = socialMediaValues.foursquare as string;
body.instagram = socialMediaValues.instagram as string;
body.linkedin = socialMediaValues.linkedIn as string;
body.skype = socialMediaValues.skype as string;
body.twitter = socialMediaValues.twitter as string;
}
}
if (updateFields.b2bOrb2c) {
body.b2b_or_b2c = updateFields.b2bOrb2c as string;
}
if (updateFields.crmId) {
body.crm_id = updateFields.crmId as string;
}
if (updateFields.fax) {
body.fax = updateFields.fax as string;
}
if (updateFields.hasPurchased) {
body.haspurchased = updateFields.hasPurchased as boolean;
}
if (updateFields.mobile) {
body.mobile = updateFields.mobile as string;
}
if (updateFields.phone) {
body.phone = updateFields.phone as string;
}
if (updateFields.prospectOrCustomer) {
body.prospect_or_customer = updateFields.prospectOrCustomer as string;
}
if (updateFields.sandbox) {
body.sandbox = updateFields.sandbox as boolean;
}
if (updateFields.stage) {
body.stage = updateFields.stage as string;
}
if (updateFields.tags) {
body.tags = updateFields.tags as string;
}
if (updateFields.website) {
body.website = updateFields.website as string;
}
responseData = await mauticApiRequest.call(this, 'PATCH', `/contacts/${contactId}/edit`, body);
responseData = responseData.contact;
}
@@ -193,6 +368,9 @@ export class Mautic implements INodeType {
qs.limit = this.getNodeParameter('limit', i) as number;
qs.start = 0;
responseData = await mauticApiRequest.call(this, 'GET', '/contacts', {}, qs);
if (responseData.errors) {
throw new Error(getErrors(responseData));
}
responseData = responseData.contacts;
responseData = Object.values(responseData);
}

View File

@@ -38,7 +38,25 @@ export class MauticTrigger implements INodeType {
{
name: 'mauticApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'credentials',
],
},
},
},
{
name: 'mauticOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -49,6 +67,22 @@ export class MauticTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Credentials',
value: 'credentials',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'credentials',
},
{
displayName: 'Events',
name: 'events',

View File

@@ -0,0 +1,64 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
/**
* Make an API request to Message Bird
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function messageBirdApiRequest(
this: IHookFunctions | IExecuteFunctions,
method: string,
resource: string,
body: IDataObject,
query: IDataObject = {},
): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('messageBirdApi');
if (credentials === undefined) {
throw new Error('No credentials returned!');
}
const options: OptionsWithUri = {
headers: {
Accept: 'application/json',
Authorization: `AccessKey ${credentials.accessKey}`,
},
method,
qs: query,
body,
uri: `https://rest.messagebird.com${resource}`,
json: true,
};
try {
return await this.helpers.request(options);
} catch (error) {
if (error.statusCode === 401) {
throw new Error('The Message Bird credentials are not valid!');
}
if (error.response && error.response.body && error.response.body.errors) {
// Try to return the error prettier
const errorMessage = error.response.body.errors.map((e: IDataObject) => e.description);
throw new Error(`MessageBird Error response [${error.statusCode}]: ${errorMessage.join('|')}`);
}
// If that data does not exist for some reason return the actual error
throw error;
}
}

View File

@@ -0,0 +1,364 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import {
messageBirdApiRequest,
} from './GenericFunctions';
export class MessageBird implements INodeType {
description: INodeTypeDescription = {
displayName: 'MessageBird',
name: 'messageBird',
icon: 'file:messagebird.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sending SMS',
defaults: {
name: 'MessageBird',
color: '#2481d7',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'messageBirdApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'SMS',
value: 'sms',
},
],
default: 'sms',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'sms',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send text messages (SMS)',
},
],
default: 'send',
description: 'The operation to perform.',
},
// ----------------------------------
// sms:send
// ----------------------------------
{
displayName: 'From',
name: 'originator',
type: 'string',
default: '',
placeholder: '14155238886',
required: true,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'The number from which to send the message.',
},
{
displayName: 'To',
name: 'recipients',
type: 'string',
default: '',
placeholder: '14155238886/+14155238886',
required: true,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'All recipients separated by commas.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
description: 'The message to be send.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Fields',
default: {},
options: [
{
displayName: 'Created Date-time',
name: 'createdDatetime',
type: 'dateTime',
default: '',
description: 'The date and time of the creation of the message in RFC3339 format (Y-m-dTH:i:sP).',
},
{
displayName: 'Datacoding',
name: 'datacoding',
type: 'options',
options: [
{
name: 'Auto',
value: 'auto',
},
{
name: 'Plain',
value: 'plain',
},
{
name: 'Unicode',
value: 'unicode',
},
],
default: '',
description: 'Using unicode will limit the maximum number of characters to 70 instead of 160.',
},
{
displayName: 'Gateway',
name: 'gateway',
type: 'number',
default: '',
description: 'The SMS route that is used to send the message.',
},
{
displayName: 'Group IDs',
name: 'groupIds',
placeholder: '1,2',
type: 'string',
default: '',
description: 'Group IDs separated by commas, If provided recipients can be omitted.',
},
{
displayName: 'Message Type',
name: 'mclass',
type: 'options',
placeholder: 'Permissible values from 0-3',
options: [
{
name: 'Flash',
value: 1,
},
{
name: 'Normal',
value: 0,
},
],
default: 1,
description: 'Indicated the message type. 1 is a normal message, 0 is a flash message.',
},
{
displayName: 'Reference',
name: 'reference',
type: 'string',
default: '',
description: 'A client reference.',
},
{
displayName: 'Report Url',
name: 'reportUrl',
type: 'string',
default: '',
description: 'The status report URL to be used on a per-message basis.<br /> Reference is required for a status report webhook to be sent.',
},
{
displayName: 'Scheduled Date-time',
name: 'scheduledDatetime',
type: 'dateTime',
default: '',
description: 'The scheduled date and time of the message in RFC3339 format (Y-m-dTH:i:sP).',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Binary',
value: 'binary',
},
{
name: 'Flash',
value: 'flash',
},
{
name: 'SMS',
value: 'sms',
},
],
default: '',
description: 'The type of message.<br /> Values can be: sms, binary, or flash.',
},
{
displayName: 'Type Details',
name: 'typeDetails',
type: 'string',
default: '',
description: 'A hash with extra information.<br /> Is only used when a binary message is sent.',
},
{
displayName: 'Validity',
name: 'validity',
type: 'number',
default: 1,
typeOptions: {
minValue: 1,
},
description: 'The amount of seconds that the message is valid.',
},
],
},
],
};
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 requestMethod;
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 === 'sms') {
//https://developers.messagebird.com/api/sms-messaging/#sms-api
if (operation === 'send') {
// ----------------------------------
// sms:send
// ----------------------------------
requestMethod = 'POST';
const originator = this.getNodeParameter('originator', i) as string;
const body = this.getNodeParameter('message', i) as string;
bodyRequest = {
recipients: [],
originator,
body
};
const additionalFields = this.getNodeParameter(
'additionalFields',
i
) as IDataObject;
if (additionalFields.groupIds) {
bodyRequest.groupIds = additionalFields.groupIds as string;
}
if (additionalFields.type) {
bodyRequest.type = additionalFields.type as string;
}
if (additionalFields.reference) {
bodyRequest.reference = additionalFields.reference as string;
}
if (additionalFields.reportUrl) {
bodyRequest.reportUrl = additionalFields.reportUrl as string;
}
if (additionalFields.validity) {
bodyRequest.validity = additionalFields.reportUrl as number;
}
if (additionalFields.gateway) {
bodyRequest.gateway = additionalFields.gateway as string;
}
if (additionalFields.typeDetails) {
bodyRequest.typeDetails = additionalFields.typeDetails as string;
}
if (additionalFields.datacoding) {
bodyRequest.datacoding = additionalFields.datacoding as string;
}
if (additionalFields.mclass) {
bodyRequest.mclass = additionalFields.mclass as number;
}
if (additionalFields.scheduledDatetime) {
bodyRequest.scheduledDatetime = additionalFields.scheduledDatetime as string;
}
if (additionalFields.createdDatetime) {
bodyRequest.createdDatetime = additionalFields.createdDatetime as string;
}
const receivers = this.getNodeParameter('recipients', i) as string;
bodyRequest.recipients = receivers.split(',').map(item => {
return parseInt(item, 10);
});
} else {
throw new Error(`The operation "${operation}" is not known!`);
}
} else {
throw new Error(`The resource "${resource}" is not known!`);
}
const responseData = await messageBirdApiRequest.call(
this,
requestMethod,
'/messages',
bodyRequest,
qs
);
returnData.push(responseData as IDataObject);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -15,6 +15,21 @@ export const boardItemOperations = [
},
},
options: [
{
name: 'Add Update',
value: 'addUpdate',
description: `Add an update to an item.`,
},
{
name: 'Change Column Value',
value: 'changeColumnValue',
description: 'Change a column value for a board item',
},
{
name: 'Change Multiple Column Values',
value: 'changeMultipleColumnValues',
description: 'Change multiple column values for a board item',
},
{
name: 'Create',
value: 'create',
@@ -48,6 +63,192 @@ export const boardItemOperations = [
export const boardItemFields = [
/* -------------------------------------------------------------------------- */
/* boardItem:addUpdate */
/* -------------------------------------------------------------------------- */
{
displayName: 'Item ID',
name: 'itemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'addUpdate',
],
},
},
description: 'The unique identifier of the item to add update to.',
},
{
displayName: 'Update Text',
name: 'value',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'addUpdate',
],
},
},
description: 'The update text to add.',
},
/* -------------------------------------------------------------------------- */
/* boardItem:changeColumnValue */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeColumnValue',
],
},
},
description: 'The unique identifier of the board.',
},
{
displayName: 'Item ID',
name: 'itemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeColumnValue',
],
},
},
description: 'The unique identifier of the item to to change column of.',
},
{
displayName: 'Column ID',
name: 'columnId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: [
'boardId'
],
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeColumnValue',
],
},
},
description: `The column's unique identifier.`,
},
{
displayName: 'Value',
name: 'value',
type: 'json',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeColumnValue',
],
},
},
description: 'The column value in JSON format. Documentation can be found <a href="https://monday.com/developers/v2#mutations-section-columns-change-column-value" target="_blank">here</a>.',
},
/* -------------------------------------------------------------------------- */
/* boardItem:changeMultipleColumnValues */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeMultipleColumnValues',
],
},
},
description: 'The unique identifier of the board.',
},
{
displayName: 'Item ID',
name: 'itemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeMultipleColumnValues',
],
},
},
description: `Item's ID`
},
{
displayName: 'Column Values',
name: 'columnValues',
type: 'json',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'changeMultipleColumnValues',
],
},
},
description: 'The column fields and values in JSON format. Documentation can be found <a href="https://monday.com/developers/v2#mutations-section-columns-change-multiple-column-values" target="_blank">here</a>.',
typeOptions: {
alwaysOpenEditWindow: true,
},
},
/* -------------------------------------------------------------------------- */
/* boardItem:create */
/* -------------------------------------------------------------------------- */

View File

@@ -455,6 +455,84 @@ export class MondayCom implements INodeType {
}
}
if (resource === 'boardItem') {
if (operation === 'addUpdate') {
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
const value = this.getNodeParameter('value', i) as string;
const body: IGraphqlBody = {
query:
`mutation ($itemId: Int!, $value: String!) {
create_update (item_id: $itemId, body: $value) {
id
}
}`,
variables: {
itemId,
value,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.create_update;
}
if (operation === 'changeColumnValue') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
const columnId = this.getNodeParameter('columnId', i) as string;
const value = this.getNodeParameter('value', i) as string;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $itemId: Int!, $columnId: String!, $value: JSON!) {
change_column_value (board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) {
id
}
}`,
variables: {
boardId,
itemId,
columnId,
},
};
try {
JSON.parse(value);
} catch (e) {
throw new Error('Custom Values must be a valid JSON');
}
body.variables.value = JSON.stringify(JSON.parse(value));
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.change_column_value;
}
if (operation === 'changeMultipleColumnValues') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
const columnValues = this.getNodeParameter('columnValues', i) as string;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $itemId: Int!, $columnValues: JSON!) {
change_multiple_column_values (board_id: $boardId, item_id: $itemId, column_values: $columnValues) {
id
}
}`,
variables: {
boardId,
itemId,
},
};
try {
JSON.parse(columnValues);
} catch (e) {
throw new Error('Custom Values must be a valid JSON');
}
body.variables.columnValues = JSON.stringify(JSON.parse(columnValues));
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.change_multiple_column_values;
}
if (operation === 'create') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const groupId = this.getNodeParameter('groupId', i) as string;

View File

@@ -32,7 +32,18 @@ export class MongoDb implements INodeType {
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'find') {
if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const { deletedCount } = await mdb
.collection(this.getNodeParameter('collection', 0) as string)
.deleteMany(JSON.parse(this.getNodeParameter('query', 0) as string));
returnItems = this.helpers.returnJsonArray([{ deletedCount }]);
} else if (operation === 'find') {
// ----------------------------------
// find
// ----------------------------------

View File

@@ -28,6 +28,11 @@ export const nodeDescription: INodeTypeDescription = {
name: 'operation',
type: 'options',
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete documents.'
},
{
name: 'Find',
value: 'find',
@@ -57,13 +62,36 @@ export const nodeDescription: INodeTypeDescription = {
description: 'MongoDB Collection'
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Delete Query (JSON format)',
name: 'query',
type: 'json',
typeOptions: {
rows: 5
},
displayOptions: {
show: {
operation: [
'delete'
],
},
},
default: '{}',
placeholder: `{ "birth": { "$gt": "1950-01-01" } }`,
required: true,
description: 'MongoDB Delete query.'
},
// ----------------------------------
// find
// ----------------------------------
{
displayName: 'Query (JSON format)',
name: 'query',
type: 'string',
type: 'json',
typeOptions: {
rows: 5
},

View File

@@ -68,7 +68,7 @@ export class Msg91 implements INodeType {
description: 'The operation to perform.',
},
{
displayName: 'From',
displayName: 'Sender ID',
name: 'from',
type: 'string',
default: '',

View File

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

View File

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

View File

@@ -213,20 +213,20 @@ export class OpenWeatherMap implements INodeType {
// Set base data
qs = {
APPID: credentials.accessToken,
units: this.getNodeParameter('format', 0) as string
units: this.getNodeParameter('format', i) as string
};
// Get the location
locationSelection = this.getNodeParameter('locationSelection', 0) as string;
locationSelection = this.getNodeParameter('locationSelection', i) as string;
if (locationSelection === 'cityName') {
qs.q = this.getNodeParameter('cityName', 0) as string;
qs.q = this.getNodeParameter('cityName', i) as string;
} else if (locationSelection === 'cityId') {
qs.id = this.getNodeParameter('cityId', 0) as number;
qs.id = this.getNodeParameter('cityId', i) as number;
} else if (locationSelection === 'coordinates') {
qs.lat = this.getNodeParameter('latitude', 0) as string;
qs.lon = this.getNodeParameter('longitude', 0) as string;
qs.lat = this.getNodeParameter('latitude', i) as string;
qs.lon = this.getNodeParameter('longitude', i) as string;
} else if (locationSelection === 'zipCode') {
qs.zip = this.getNodeParameter('zipCode', 0) as string;
qs.zip = this.getNodeParameter('zipCode', i) as string;
} else {
throw new Error(`The locationSelection "${locationSelection}" is not known!`);
}

View File

@@ -19,16 +19,11 @@ import {
export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('pagerDutyApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
headers: {
Accept: 'application/vnd.pagerduty+json;version=2',
Authorization: `Token token=${credentials.apiToken}`,
Accept: 'application/vnd.pagerduty+json;version=2'
},
method,
body,
@@ -39,15 +34,30 @@ export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunc
arrayFormat: 'brackets',
},
};
if (!Object.keys(body).length) {
delete options.form;
}
if (!Object.keys(query).length) {
delete options.qs;
}
options.headers = Object.assign({}, options.headers, headers);
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'apiToken') {
const credentials = this.getCredentials('pagerDutyApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
options.headers!['Authorization'] = `Token token=${credentials.apiToken}`;
return await this.helpers.request!(options);
} else {
return await this.helpers.requestOAuth2!.call(this, 'pagerDutyOAuth2Api', options);
}
} catch (error) {
if (error.response && error.response.body && error.response.body.error && error.response.body.error.errors) {
// Try to return the error prettier

View File

@@ -66,9 +66,43 @@ export class PagerDuty implements INodeType {
{
name: 'pagerDutyApi',
required: true,
displayOptions: {
show: {
authentication: [
'apiToken',
],
},
},
},
{
name: 'pagerDutyOAuth2Api',
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',
},
{
displayName: 'Resource',
name: 'resource',

View File

@@ -5,10 +5,16 @@ import {
import {
IDataObject,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
<<<<<<< HEAD
=======
>>>>>>> master
export interface ICustomInterface {
name: string;
@@ -32,10 +38,27 @@ export interface ICustomProperties {
* @param {object} body
* @returns {Promise<any>}
*/
<<<<<<< HEAD
export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, formData?: IDataObject, downloadFile?: boolean): Promise<any> { // tslint:disable-line:no-any
const authenticationMethod = this.getNodeParameter('authentication', 0);
=======
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;
>>>>>>> master
const options: OptionsWithUri = {
headers: {
Accept: 'application/json',
},
method,
qs: query,
uri: `https://api.pipedrive.com/v1${endpoint}`,
@@ -62,7 +85,8 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
let responseData;
try {
if (authenticationMethod === 'basicAuth') {
<<<<<<< HEAD
if (authenticationMethod === 'basicAuth' || authenticationMethod === 'apiToken') {
const credentials = this.getCredentials('pipedriveApi');
if (credentials === undefined) {
@@ -76,6 +100,10 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
} else {
responseData = await this.helpers.requestOAuth2!.call(this, 'pipedriveOAuth2Api', options);
}
=======
//@ts-ignore
const responseData = await this.helpers.request(options);
>>>>>>> master
if (downloadFile === true) {
return {
@@ -99,7 +127,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}`;
}
@@ -128,7 +156,7 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
if (query === undefined) {
query = {};
}
query.limit = 500;
query.limit = 100;
query.start = 0;
const returnData: IDataObject[] = [];
@@ -137,7 +165,12 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
do {
responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData.data);
// the search path returns data diferently
if (responseData.data.items) {
returnData.push.apply(returnData, responseData.data.items);
} else {
returnData.push.apply(returnData, responseData.data);
}
query.start = responseData.additionalData.pagination.next_start;
} while (

View File

@@ -1,12 +1,14 @@
import {
BINARY_ENCODING,
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
INodePropertyOptions,
} from 'n8n-workflow';
import {
@@ -23,7 +25,6 @@ interface CustomProperty {
value: string;
}
/**
* Add the additional fields to the body
*
@@ -64,7 +65,7 @@ export class Pipedrive implements INodeType {
displayOptions: {
show: {
authentication: [
'basicAuth',
'apiToken',
],
},
},
@@ -88,19 +89,15 @@ export class Pipedrive implements INodeType {
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth'
name: 'API Token',
value: 'apiToken'
},
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'None',
value: 'none',
},
],
default: 'basicAuth',
default: 'apiToken',
description: 'Method of authentication.',
},
{
@@ -399,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',
@@ -2058,6 +2060,7 @@ export class Pipedrive implements INodeType {
show: {
operation: [
'getAll',
'search',
],
},
},
@@ -2072,6 +2075,7 @@ export class Pipedrive implements INodeType {
show: {
operation: [
'getAll',
'search',
],
returnAll: [
false,
@@ -2086,9 +2090,143 @@ export class Pipedrive implements INodeType {
description: 'How many results to return.',
},
// ----------------------------------
// person:getAll
// ----------------------------------
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'person',
],
},
},
default: {},
options: [
{
displayName: 'Filter ID',
name: 'filterId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getFilters',
},
default: '',
description: 'ID of the filter to use.',
},
{
displayName: 'First Char',
name: 'firstChar',
type: 'string',
default: '',
description: 'If supplied, only persons whose name starts with the specified letter will be returned ',
},
],
},
// ----------------------------------
// person:search
// ----------------------------------
{
displayName: 'Term',
name: 'term',
type: 'string',
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.`,
},
],
},
],
};
methods = {
loadOptions: {
// Get all the filters to display them to user so that he can
// select them easily
async getFilters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { data } = await pipedriveApiRequest.call(this, 'GET', '/filters', {}, { type: 'people' });
for (const filter of data) {
const filterName = filter.name;
const filterId = filter.id;
returnData.push({
name: filterName,
value: filterId,
});
}
return returnData;
},
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
@@ -2492,8 +2630,51 @@ export class Pipedrive implements INodeType {
qs.limit = this.getNodeParameter('limit', i) as number;
}
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.filterId) {
qs.filter_id = additionalFields.filterId as string;
}
if (additionalFields.firstChar) {
qs.first_char = additionalFields.firstChar as string;
}
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
@@ -2530,7 +2711,9 @@ export class Pipedrive implements INodeType {
let responseData;
if (returnAll === true) {
responseData = await pipedriveApiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else {
if (customProperties !== undefined) {
@@ -2538,6 +2721,13 @@ export class Pipedrive implements INodeType {
}
responseData = await pipedriveApiRequest.call(this, requestMethod, endpoint, body, qs, formData, downloadFile);
<<<<<<< HEAD
if (responseData.data === null) {
responseData.data = [];
}
=======
>>>>>>> master
}
if (resource === 'file' && operation === 'download') {
@@ -2559,6 +2749,24 @@ export class Pipedrive implements INodeType {
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData.data);
} else {
if (responseData.data === null) {
responseData.data = [];
}
if (operation === 'search' && responseData.data && responseData.data.items) {
responseData.data = responseData.data.items;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.rawData !== true) {
responseData.data = responseData.data.map((item: { result_score: number, item: object }) => {
return {
result_score: item.result_score,
...item.item,
};
});
}
}
if (Array.isArray(responseData.data)) {
returnData.push.apply(returnData, responseData.data as IDataObject[]);
} else {

View File

@@ -14,8 +14,10 @@ import {
} from './GenericFunctions';
import * as basicAuth from 'basic-auth';
import { Response } from 'express';
import {
Response,
} from 'express';
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
if (message === undefined) {
@@ -52,6 +54,10 @@ export class PipedriveTrigger implements INodeType {
{
name: 'pipedriveApi',
required: true,
},
{
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
authentication: [
@@ -60,18 +66,6 @@ export class PipedriveTrigger implements INodeType {
},
},
},
{
name: 'pipedriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
],
webhooks: [
{
@@ -91,17 +85,13 @@ export class PipedriveTrigger implements INodeType {
name: 'Basic Auth',
value: 'basicAuth'
},
{
name: 'OAuth2',
value: 'oAuth2',
},
{
name: 'None',
value: 'none',
value: 'none'
},
],
default: 'basicAuth',
description: 'Method of authentication.',
default: 'none',
description: 'If authentication should be activated for the webhook (makes it more scure).',
},
{
displayName: 'Action',
@@ -191,7 +181,6 @@ export class PipedriveTrigger implements INodeType {
description: 'Type of object to receive notifications about.',
},
],
};
// @ts-ignore (because of request)
@@ -288,8 +277,6 @@ export class PipedriveTrigger implements INodeType {
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
const resp = this.getResponseObject();

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,246 @@
import { IExecuteFunctions } from 'n8n-core';
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
import * as pgPromise from 'pg-promise';
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/Postgres.node.functions';
import { table } from 'console';
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',
},
// ----------------------------------
// 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('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;
let queries : string[] = [];
items.map(item => {
let columns = Object.keys(item.json);
let values : string = columns.map((col : string) => {
if (typeof item.json[col] === 'string') {
return `\'${item.json[col]}\'`;
} else {
return item.json[col];
}
}).join(',');
let query = `INSERT INTO ${tableName} (${columns.join(',')}) VALUES (${values});`;
queries.push(query);
});
await db.any(pgp.helpers.concat(queries));
let returnedItems = await db.any(`SELECT ${returnFields} from ${tableName}`);
returnItems = this.helpers.returnJsonArray(returnedItems as IDataObject[]);
} else {
await pgp.end();
throw new Error(`The operation "${operation}" is not supported!`);
}
// Close the connection
await pgp.end();
return this.prepareOutputData(returnItems);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -402,6 +402,7 @@ export class Redis implements INodeType {
} else if (type === 'hash') {
const clientHset = util.promisify(client.hset).bind(client);
for (const key of Object.keys(value)) {
// @ts-ignore
await clientHset(keyName, key, (value as IDataObject)[key]!.toString());
}
} else if (type === 'list') {

View File

@@ -48,15 +48,15 @@ interface IPostMessageBody {
export class Rocketchat implements INodeType {
description: INodeTypeDescription = {
displayName: 'Rocketchat',
displayName: 'RocketChat',
name: 'rocketchat',
icon: 'file:rocketchat.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume Rocketchat API',
description: 'Consume RocketChat API',
defaults: {
name: 'Rocketchat',
name: 'RocketChat',
color: '#c02428',
},
inputs: ['main'],

View File

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

View File

@@ -0,0 +1,52 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an API request to SIGNL4
*
* @param {IHookFunctions | IExecuteFunctions} this
* @param {object} message
* @returns {Promise<any>}
*/
export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
let options: OptionsWithUri = {
headers: {
'Accept': '*/*',
},
method,
body,
qs: query,
uri: uri || ``,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(query).length) {
delete options.qs;
}
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response && error.response.body && error.response.body.details) {
throw new Error(`SIGNL4 error response [${error.statusCode}]: ${error.response.body.details}`);
}
throw error;
}
}

View File

@@ -0,0 +1,325 @@
import {
IExecuteFunctions,
BINARY_ENCODING,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IBinaryKeyData,
} from 'n8n-workflow';
import {
SIGNL4ApiRequest,
} from './GenericFunctions';
export class Signl4 implements INodeType {
description: INodeTypeDescription = {
displayName: 'SIGNL4',
name: 'signl4',
icon: 'file:signl4.png',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume SIGNL4 API.',
defaults: {
name: 'SIGNL4',
color: '#53afe8',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'signl4Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Alert',
value: 'alert',
},
],
default: 'alert',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'alert',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send an alert.',
},
],
default: 'send',
description: 'The operation to perform.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
required: false,
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'alert',
],
},
},
description: 'A more detailed description for the alert.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'alert',
],
},
},
default: {},
options: [
{
displayName: 'Alerting Scenario',
name: 'alertingScenario',
type: 'options',
options: [
{
name: 'Single ACK',
value: 'single_ack',
description: 'In case only one person needs to confirm this Signl.'
},
{
name: 'Multi ACK',
value: 'multi_ack',
description: 'in case this alert must be confirmed by the number of people who are on duty at the time this Singl is raised',
},
],
default: 'single_ack',
required: false,
},
{
displayName: 'Attachments',
name: 'attachmentsUi',
placeholder: 'Add Attachments',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
options: [
{
name: 'attachmentsBinary',
displayName: 'Attachments Binary',
values: [
{
displayName: 'Property Name',
name: 'property',
type: 'string',
placeholder: 'data',
default: '',
description: 'Name of the binary properties which contain data which should be added as attachment',
},
],
},
],
default: {},
},
{
displayName: 'External ID',
name: 'externalId',
type: 'string',
default: '',
description: `If the event originates from a record in a 3rd party system, use this parameter to pass <br/>
the unique ID of that record. That ID will be communicated in outbound webhook notifications from SIGNL4,<br/>
which is great for correlation/synchronization of that record with the alert.`,
},
{
displayName: 'Filtering',
name: 'filtering',
type: 'boolean',
default: 'false',
description: `Specify a boolean value of true or false to apply event filtering for this event, or not. <br/>
If set to true, the event will only trigger a notification to the team, if it contains at least one keyword <br/>
from one of your services and system categories (i.e. it is whitelisted)`,
},
{
displayName: 'Location',
name: 'locationFieldsUi',
type: 'fixedCollection',
placeholder: 'Add Location',
default: {},
description: 'Transmit location information (\'latitude, longitude\') with your event and display a map in the mobile app.',
options: [
{
name: 'locationFieldsValues',
displayName: 'Location',
values: [
{
displayName: 'Latitude',
name: 'latitude',
type: 'string',
required: true,
description: 'The location latitude.',
default: '',
},
{
displayName: 'Longitude',
name: 'longitude',
type: 'string',
required: true,
description: 'The location longitude.',
default: '',
},
],
}
],
},
{
displayName: 'Service',
name: 'service',
type: 'string',
default: '',
description: 'Assigns the alert to the service/system category with the specified name.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
},
],
},
],
};
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 === 'alert') {
//https://connect.signl4.com/webhook/docs/index.html
if (operation === 'send') {
const message = this.getNodeParameter('message', i) as string;
const additionalFields = this.getNodeParameter('additionalFields',i) as IDataObject;
const data: IDataObject = {
message,
};
if (additionalFields.alertingScenario) {
data['X-S4-AlertingScenario'] = additionalFields.alertingScenario as string;
}
if (additionalFields.externalId) {
data['X-S4-ExternalID'] = additionalFields.externalId as string;
}
if (additionalFields.filtering) {
data['X-S4-Filtering'] = (additionalFields.filtering as boolean).toString();
}
if (additionalFields.locationFieldsUi) {
const locationUi = (additionalFields.locationFieldsUi as IDataObject).locationFieldsValues as IDataObject;
if (locationUi) {
data['X-S4-Location'] = `${locationUi.latitude},${locationUi.longitude}`;
}
}
if (additionalFields.service) {
data['X-S4-Service'] = additionalFields.service as string;
}
if (additionalFields.title) {
data['title'] = additionalFields.title as string;
}
const attachments = additionalFields.attachmentsUi as IDataObject;
if (attachments) {
if (attachments.attachmentsBinary && items[i].binary) {
const propertyName = (attachments.attachmentsBinary as IDataObject).property as string;
const binaryProperty = (items[i].binary as IBinaryKeyData)[propertyName];
if (binaryProperty) {
const supportedFileExtension = ['png', 'jpg', 'txt'];
if (!supportedFileExtension.includes(binaryProperty.fileExtension as string)) {
throw new Error(`Invalid extension, just ${supportedFileExtension.join(',')} are supported}`);
}
data['file'] = {
value: Buffer.from(binaryProperty.data, BINARY_ENCODING),
options: {
filename: binaryProperty.fileName,
contentType: binaryProperty.mimeType,
},
};
} else {
throw new Error(`Binary property ${propertyName} does not exist on input`);
}
}
}
const credentials = this.getCredentials('signl4Api');
const endpoint = `https://connect.signl4.com/webhook/${credentials?.teamSecret}`;
responseData = await SIGNL4ApiRequest.call(
this,
'POST',
'',
{},
{},
endpoint,
{
formData: {
...data,
},
},
);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -91,7 +91,7 @@ export const messageFields = [
],
},
},
description: 'Post the message as authenticated user instead of bot.',
description: 'Post the message as authenticated user instead of bot. Works only with user token.',
},
{
displayName: 'User Name',
@@ -111,7 +111,7 @@ export const messageFields = [
],
},
},
description: 'Set the bot\'s user name.',
description: 'Set the bot\'s user name. This field will be ignored if you are using a user token.',
},
{
displayName: 'JSON parameters',
@@ -504,7 +504,7 @@ export const messageFields = [
],
},
},
description: 'Pass true to update the message as the authed user. Bot users in this context are considered authed users.',
description: 'Pass true to update the message as the authed user. Works only with user token.',
},
{
displayName: 'Update Fields',

View File

@@ -289,7 +289,7 @@ export class Slack implements INodeType {
if (operation === 'get') {
const channel = this.getNodeParameter('channelId', i) as string;
qs.channel = channel,
responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs);
responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs);
responseData = responseData.channel;
}
//https://api.slack.com/methods/conversations.list
@@ -452,11 +452,12 @@ export class Slack implements INodeType {
}
if (body.as_user === false) {
body.username = this.getNodeParameter('username', i) as string;
delete body.as_user;
}
if (!jsonParameters) {
const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[];
const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[];
const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[];
// The node does save the fields data differently than the API
// expects so fix the data befre we send the request
@@ -482,7 +483,7 @@ export class Slack implements INodeType {
block.block_id = blockUi.blockId as string;
block.type = blockUi.type as string;
if (block.type === 'actions') {
const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[];
const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[];
if (elementsUi) {
for (const elementUi of elementsUi) {
const element: Element = {};
@@ -498,7 +499,7 @@ export class Slack implements INodeType {
text: elementUi.text as string,
type: 'plain_text',
emoji: elementUi.emoji as boolean,
};
};
if (elementUi.url) {
element.url = elementUi.url as string;
}
@@ -508,13 +509,13 @@ export class Slack implements INodeType {
if (elementUi.style !== 'default') {
element.style = elementUi.style as string;
}
const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject;
if (confirmUi) {
const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject;
if (confirmUi) {
const confirm: Confirm = {};
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
const style = confirmUi.style as string;
if (titleUi) {
confirm.title = {
@@ -548,13 +549,13 @@ export class Slack implements INodeType {
confirm.style = style as string;
}
element.confirm = confirm;
}
elements.push(element);
}
elements.push(element);
}
block.elements = elements;
}
} else if (block.type === 'section') {
const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject;
const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject;
if (textUi) {
const text: Text = {};
if (textUi.type === 'plainText') {
@@ -569,7 +570,7 @@ export class Slack implements INodeType {
} else {
throw new Error('Property text must be defined');
}
const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[];
const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[];
if (fieldsUi) {
const fields: Text[] = [];
for (const fieldUi of fieldsUi) {
@@ -589,7 +590,7 @@ export class Slack implements INodeType {
block.fields = fields;
}
}
const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject;
const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject;
if (accessoryUi) {
const accessory: Element = {};
if (accessoryUi.type === 'button') {
@@ -608,46 +609,46 @@ export class Slack implements INodeType {
if (accessoryUi.style !== 'default') {
accessory.style = accessoryUi.style as string;
}
const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject;
const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject;
if (confirmUi) {
const confirm: Confirm = {};
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
const style = confirmUi.style as string;
if (titleUi) {
confirm.title = {
type: 'plain_text',
text: titleUi.text as string,
emoji: titleUi.emoji as boolean,
};
}
if (textUi) {
confirm.text = {
type: 'plain_text',
text: textUi.text as string,
emoji: textUi.emoji as boolean,
};
}
if (confirmTextUi) {
confirm.confirm = {
type: 'plain_text',
text: confirmTextUi.text as string,
emoji: confirmTextUi.emoji as boolean,
};
}
if (denyUi) {
confirm.deny = {
type: 'plain_text',
text: denyUi.text as string,
emoji: denyUi.emoji as boolean,
};
}
if (style !== 'default') {
confirm.style = style as string;
}
accessory.confirm = confirm;
const confirm: Confirm = {};
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
const style = confirmUi.style as string;
if (titleUi) {
confirm.title = {
type: 'plain_text',
text: titleUi.text as string,
emoji: titleUi.emoji as boolean,
};
}
if (textUi) {
confirm.text = {
type: 'plain_text',
text: textUi.text as string,
emoji: textUi.emoji as boolean,
};
}
if (confirmTextUi) {
confirm.confirm = {
type: 'plain_text',
text: confirmTextUi.text as string,
emoji: confirmTextUi.emoji as boolean,
};
}
if (denyUi) {
confirm.deny = {
type: 'plain_text',
text: denyUi.text as string,
emoji: denyUi.emoji as boolean,
};
}
if (style !== 'default') {
confirm.style = style as string;
}
accessory.confirm = confirm;
}
}
block.accessory = accessory;
@@ -691,10 +692,6 @@ export class Slack implements INodeType {
ts,
};
if (authentication === 'accessToken') {
body.as_user = this.getNodeParameter('as_user', i) as boolean;
}
// The node does save the fields data differently than the API
// expects so fix the data befre we send the request
for (const attachment of attachments) {
@@ -790,8 +787,8 @@ export class Slack implements INodeType {
if (binaryData) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
if (items[i].binary === undefined
//@ts-ignore
|| items[i].binary[binaryPropertyName] === undefined) {
//@ts-ignore
|| items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
body.file = {
@@ -804,7 +801,7 @@ export class Slack implements INodeType {
contentType: items[i].binary[binaryPropertyName].mimeType,
}
};
responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body });
responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body });
responseData = responseData.file;
} else {
const fileContent = this.getNodeParameter('fileContent', i) as string;

View File

@@ -0,0 +1,84 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
/**
* Make an API request to Spotify
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions,
method: string, endpoint: string, body: object, query?: object, uri?: string): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
method,
headers: {
'User-Agent': 'n8n',
'Content-Type': 'text/plain',
'Accept': ' application/json',
},
body,
qs: query,
uri: uri || `https://api.spotify.com/v1${endpoint}`,
json: true
};
try {
const credentials = this.getCredentials('spotifyOAuth2Api');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
if (Object.keys(body).length === 0) {
delete options.body;
}
return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options);
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Spotify credentials are not valid!');
}
if (error.error && error.error.error && error.error.error.message) {
// Try to return the error prettier
throw new Error(`Spotify error response [${error.error.error.status}]: ${error.error.error.message}`);
}
// If that data does not exist for some reason return the actual error
throw error;
}
}
export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteFunctions,
propertyName: string, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
do {
responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri);
returnData.push.apply(returnData, responseData[propertyName]);
uri = responseData.next;
} while (
responseData['next'] !== null
);
return returnData;
}

View File

@@ -0,0 +1,816 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
spotifyApiRequest,
spotifyApiRequestAllItems,
} from './GenericFunctions';
export class Spotify implements INodeType {
description: INodeTypeDescription = {
displayName: 'Spotify',
name: 'spotify',
icon: 'file:spotify.png',
group: ['input'],
version: 1,
description: 'Access public song data via the Spotify API.',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
defaults: {
name: 'Spotify',
color: '#1DB954',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'spotifyOAuth2Api',
required: true,
},
],
properties: [
// ----------------------------------------------------------
// Resource to Operate on
// Player, Album, Artisits, Playlists, Tracks
// ----------------------------------------------------------
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Album',
value: 'album',
},
{
name: 'Artist',
value: 'artist',
},
{
name: 'Player',
value: 'player',
},
{
name: 'Playlist',
value: 'playlist',
},
{
name: 'Track',
value: 'track',
},
],
default: 'player',
description: 'The resource to operate on.',
},
// --------------------------------------------------------------------------------------------------------
// Player Operations
// Pause, Play, Get Recently Played, Get Currently Playing, Next Song, Previous Song, Add to Queue
// --------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'player',
],
},
},
options: [
{
name: 'Add Song to Queue',
value: 'addSongToQueue',
description: 'Add a song to your queue.'
},
{
name: 'Currently Playing',
value: 'currentlyPlaying',
description: 'Get your currently playing track.'
},
{
name: 'Next Song',
value: 'nextSong',
description: 'Skip to your next track.'
},
{
name: 'Pause',
value: 'pause',
description: 'Pause your music.',
},
{
name: 'Previous Song',
value: 'previousSong',
description: 'Skip to your previous song.'
},
{
name: 'Recently Played',
value: 'recentlyPlayed',
description: 'Get your recently played tracks.'
},
{
name: 'Start Music',
value: 'startMusic',
description: 'Start playing a playlist, artist, or album.'
},
],
default: 'addSongToQueue',
description: 'The operation to perform.',
},
{
displayName: 'Resource ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'player'
],
operation: [
'startMusic',
],
},
},
placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp',
description: `Enter a playlist, artist, or album URI or ID.`,
},
{
displayName: 'Track ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'player'
],
operation: [
'addSongToQueue',
],
},
},
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
description: `Enter a track URI or ID.`,
},
// -----------------------------------------------
// Album Operations
// Get an Album, Get an Album's Tracks
// -----------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'album',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get an album by URI or ID.',
},
{
name: `Get Tracks`,
value: 'getTracks',
description: `Get an album's tracks by URI or ID.`,
},
],
default: 'get',
description: 'The operation to perform.',
},
{
displayName: 'Album ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'album',
],
},
},
placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp',
description: `The album's Spotify URI or ID.`,
},
// -------------------------------------------------------------------------------------------------------------
// Artist Operations
// Get an Artist, Get an Artist's Related Artists, Get an Artist's Top Tracks, Get an Artist's Albums
// -------------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'artist',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get an artist by URI or ID.',
},
{
name: `Get Albums`,
value: 'getAlbums',
description: `Get an artist's albums by URI or ID.`,
},
{
name: `Get Related Artists`,
value: 'getRelatedArtists',
description: `Get an artist's related artists by URI or ID.`,
},
{
name: `Get Top Tracks`,
value: 'getTopTracks',
description: `Get an artist's top tracks by URI or ID.`,
},
],
default: 'get',
description: 'The operation to perform.',
},
{
displayName: 'Artist ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'artist',
],
},
},
placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U',
description: `The artist's Spotify URI or ID.`,
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: 'US',
required: true,
displayOptions: {
show: {
resource: [
'artist'
],
operation: [
'getTopTracks',
],
},
},
placeholder: 'US',
description: `Top tracks in which country? Enter the postal abbriviation.`,
},
// -------------------------------------------------------------------------------------------------------------
// Playlist Operations
// Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists
// -------------------------------------------------------------------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'playlist',
],
},
},
options: [
{
name: 'Add an Item',
value: 'add',
description: 'Add tracks from a playlist by track and playlist URI or ID.',
},
{
name: 'Get',
value: 'get',
description: 'Get a playlist by URI or ID.',
},
{
name: 'Get Tracks',
value: 'getTracks',
description: `Get a playlist's tracks by URI or ID.`,
},
{
name: `Get the User's Playlists`,
value: 'getUserPlaylists',
description: `Get a user's playlists.`,
},
{
name: 'Remove an Item',
value: 'delete',
description: 'Remove tracks from a playlist by track and playlist URI or ID.',
},
],
default: 'add',
description: 'The operation to perform.',
},
{
displayName: 'Playlist ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'playlist',
],
operation: [
'add',
'delete',
'get',
'getTracks',
],
},
},
placeholder: 'spotify:playlist:37i9dQZF1DWUhI3iC1khPH',
description: `The playlist's Spotify URI or its ID.`,
},
{
displayName: 'Track ID',
name: 'trackID',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'playlist',
],
operation: [
'add',
'delete',
],
},
},
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
description: `The track's Spotify URI or its ID. The track to add/delete from the playlist.`,
},
// -----------------------------------------------------
// Track Operations
// Get a Track, Get a Track's Audio Features
// -----------------------------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'track',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a track by its URI or ID.',
},
{
name: 'Get Audio Features',
value: 'getAudioFeatures',
description: 'Get audio features for a track by URI or ID.',
},
],
default: 'track',
description: 'The operation to perform.',
},
{
displayName: 'Track ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'track',
],
},
},
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
description: `The track's Spotify URI or ID.`,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
required: true,
displayOptions: {
show: {
resource: [
'album',
'artist',
'playlist',
],
operation: [
'getTracks',
'getAlbums',
'getUserPlaylists',
],
},
},
description: `The number of items to return.`,
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
required: true,
displayOptions: {
show: {
resource: [
'album',
'artist',
'playlist'
],
operation: [
'getTracks',
'getAlbums',
'getUserPlaylists',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
description: `The number of items to return.`,
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
required: true,
displayOptions: {
show: {
resource: [
'player',
],
operation: [
'recentlyPlayed',
],
},
},
typeOptions: {
minValue: 1,
maxValue: 50,
},
description: `The number of items to return.`,
},
]
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// Get all of the incoming input data to loop through
const items = this.getInputData();
const returnData: IDataObject[] = [];
// For Post
let body: IDataObject;
// For Query string
let qs: IDataObject;
let requestMethod: string;
let endpoint: string;
let returnAll: boolean;
let propertyName = '';
let responseData;
const operation = this.getNodeParameter('operation', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string;
// Set initial values
requestMethod = 'GET';
endpoint = '';
body = {};
qs = {};
returnAll = false;
for(let i = 0; i < items.length; i++) {
// -----------------------------
// Player Operations
// -----------------------------
if( resource === 'player' ) {
if(operation === 'pause') {
requestMethod = 'PUT';
endpoint = `/me/player/pause`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
} else if(operation === 'recentlyPlayed') {
requestMethod = 'GET';
endpoint = `/me/player/recently-played`;
const limit = this.getNodeParameter('limit', i) as number;
qs = {
limit,
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.items;
} else if(operation === 'currentlyPlaying') {
requestMethod = 'GET';
endpoint = `/me/player/currently-playing`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
} else if(operation === 'nextSong') {
requestMethod = 'POST';
endpoint = `/me/player/next`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
} else if(operation === 'previousSong') {
requestMethod = 'POST';
endpoint = `/me/player/previous`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
} else if(operation === 'startMusic') {
requestMethod = 'PUT';
endpoint = `/me/player/play`;
const id = this.getNodeParameter('id', i) as string;
body.context_uri = id;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
} else if(operation === 'addSongToQueue') {
requestMethod = 'POST';
endpoint = `/me/player/queue`;
const id = this.getNodeParameter('id', i) as string;
qs = {
uri: id
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
}
// -----------------------------
// Album Operations
// -----------------------------
} else if( resource === 'album') {
const uri = this.getNodeParameter('id', i) as string;
const id = uri.replace('spotify:album:', '');
requestMethod = 'GET';
if(operation === 'get') {
endpoint = `/albums/${id}`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
} else if(operation === 'getTracks') {
endpoint = `/albums/${id}/tracks`;
propertyName = 'tracks';
returnAll = this.getNodeParameter('returnAll', i) as boolean;
propertyName = 'items';
if(!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
qs = {
limit,
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.items;
}
}
// -----------------------------
// Artist Operations
// -----------------------------
} else if( resource === 'artist') {
const uri = this.getNodeParameter('id', i) as string;
const id = uri.replace('spotify:artist:', '');
if(operation === 'getAlbums') {
endpoint = `/artists/${id}/albums`;
returnAll = this.getNodeParameter('returnAll', i) as boolean;
propertyName = 'items';
if(!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
qs = {
limit,
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.items;
}
} else if(operation === 'getRelatedArtists') {
endpoint = `/artists/${id}/related-artists`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.artists;
} else if(operation === 'getTopTracks'){
const country = this.getNodeParameter('country', i) as string;
qs = {
country,
};
endpoint = `/artists/${id}/top-tracks`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.tracks;
} else if (operation === 'get') {
requestMethod = 'GET';
endpoint = `/artists/${id}`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
}
// -----------------------------
// Playlist Operations
// -----------------------------
} else if( resource === 'playlist') {
if(['delete', 'get', 'getTracks', 'add'].includes(operation)) {
const uri = this.getNodeParameter('id', i) as string;
const id = uri.replace('spotify:playlist:', '');
if(operation === 'delete') {
requestMethod = 'DELETE';
const trackId = this.getNodeParameter('trackID', i) as string;
body.tracks = [
{
uri: `${trackId}`,
positions: [ 0 ],
},
];
endpoint = `/playlists/${id}/tracks`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = { success: true };
} else if(operation === 'get') {
requestMethod = 'GET';
endpoint = `/playlists/${id}`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
} else if(operation === 'getTracks') {
requestMethod = 'GET';
endpoint = `/playlists/${id}/tracks`;
returnAll = this.getNodeParameter('returnAll', i) as boolean;
propertyName = 'items';
if(!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
qs = {
'limit': limit
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.items;
}
} else if(operation === 'add') {
requestMethod = 'POST';
const trackId = this.getNodeParameter('trackID', i) as string;
qs = {
uris: trackId
};
endpoint = `/playlists/${id}/tracks`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
}
} else if(operation === 'getUserPlaylists') {
requestMethod = 'GET';
endpoint = '/me/playlists';
returnAll = this.getNodeParameter('returnAll', i) as boolean;
propertyName = 'items';
if(!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
qs = {
limit,
};
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.items;
}
}
// -----------------------------
// Track Operations
// -----------------------------
} else if( resource === 'track') {
const uri = this.getNodeParameter('id', i) as string;
const id = uri.replace('spotify:track:', '');
requestMethod = 'GET';
if(operation === 'getAudioFeatures') {
endpoint = `/audio-features/${id}`;
} else if(operation === 'get') {
endpoint = `/tracks/${id}`;
}
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
}
if(returnAll) {
responseData = await spotifyApiRequestAllItems.call(this, propertyName, requestMethod, 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)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -14,19 +14,13 @@ import {
} from 'n8n-workflow';
export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('surveyMonkeyApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
const endpoint = 'https://api.surveymonkey.com/v3';
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': `bearer ${credentials.accessToken}`,
},
method,
body,
@@ -34,6 +28,7 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF
uri: uri || `${endpoint}${resource}`,
json: true
};
if (!Object.keys(body).length) {
delete options.body;
}
@@ -41,8 +36,22 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF
delete options.qs;
}
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
if ( authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('surveyMonkeyApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
// @ts-ignore
options.headers['Authorization'] = `bearer ${credentials.accessToken}`;
return await this.helpers.request!(options);
} else {
return await this.helpers.requestOAuth2?.call(this, 'surveyMonkeyOAuth2Api', options);
}
} catch (error) {
const errorMessage = error.response.body.error.message;
if (errorMessage !== undefined) {

View File

@@ -49,6 +49,24 @@ export class SurveyMonkeyTrigger implements INodeType {
{
name: 'surveyMonkeyApi',
required: true,
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'surveyMonkeyOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
@@ -66,6 +84,23 @@ export class SurveyMonkeyTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'Method of authentication.',
},
{
displayName: 'Type',
name: 'objectType',
@@ -453,11 +488,18 @@ export class SurveyMonkeyTrigger implements INodeType {
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const event = this.getNodeParameter('event') as string;
const objectType = this.getNodeParameter('objectType') as string;
const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject;
const authenticationMethod = this.getNodeParameter('authentication') as string;
let credentials : IDataObject;
const headerData = this.getHeaderData() as IDataObject;
const req = this.getRequestObject();
const webhookName = this.getWebhookName();
if (authenticationMethod === 'accessToken') {
credentials = this.getCredentials('surveyMonkeyApi') as IDataObject;
} else {
credentials = this.getCredentials('surveyMonkeyOAuth2Api') as IDataObject;
}
if (webhookName === 'setup') {
// It is a create webhook confirmation request
return {};

View File

@@ -29,7 +29,7 @@ export const attachmentOperations = [
{
name: 'Get',
value: 'get',
description: 'Get the data of an attachments',
description: 'Get the data of an attachment',
},
{
name: 'Get All',

View File

@@ -44,17 +44,17 @@ export const checklistOperations = [
{
name: 'Get Checklist Items',
value: 'getCheckItem',
description: 'Get a specific Checklist on a card',
description: 'Get a specific checklist on a card',
},
{
name: 'Get Completed Checklist Items',
value: 'completedCheckItems',
description: 'Get the completed Checklist items on a card',
description: 'Get the completed checklist items on a card',
},
{
name: 'Update Checklist Item',
value: 'updateCheckItem',
description: 'Update an item in a checklist on a card.',
description: 'Update an item in a checklist on a card',
},
],
default: 'getAll',

View File

@@ -39,7 +39,7 @@ export const labelOperations = [
{
name: 'Get All',
value: 'getAll',
description: 'Returns all label for the board',
description: 'Returns all labels for the board',
},
{
name: 'Remove From Card',

View File

@@ -219,7 +219,7 @@ export const tweetFields = [
description: 'The entities node will not be included when set to false',
},
{
displayName: 'Lang',
displayName: 'Language',
name: 'lang',
type: 'options',
typeOptions: {

View File

@@ -133,7 +133,15 @@ export class Twitter implements INodeType {
let attachmentBody = {};
let response: IDataObject = {};
if (binaryData[binaryPropertyName].mimeType.includes('image')) {
const isAnimatedWebp = (Buffer.from(binaryData[binaryPropertyName].data, 'base64').toString().indexOf('ANMF') !== -1);
const isImage = binaryData[binaryPropertyName].mimeType.includes('image');
if (isImage && isAnimatedWebp) {
throw new Error('Animated .webp images are not supported use .gif instead');
}
if (isImage) {
const attachmentBody = {
media_data: binaryData[binaryPropertyName].data,

View File

@@ -45,18 +45,10 @@ export interface ITypeformAnswerField {
* @returns {Promise<any>}
*/
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('typeformApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
query = query || {};
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
headers: {
'Authorization': `bearer ${credentials.accessToken}`,
},
headers: {},
method,
body,
qs: query,
@@ -64,8 +56,23 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
json: true,
};
query = query || {};
try {
return await this.helpers.request!(options);
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('typeformApi');
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, 'typeformOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error

View File

@@ -37,7 +37,25 @@ export class TypeformTrigger implements INodeType {
{
name: 'typeformApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'typeformOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -48,6 +66,23 @@ export class TypeformTrigger 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: 'Form',
name: 'formId',

View File

@@ -113,7 +113,9 @@ export class Uplead implements INodeType {
if (Array.isArray(responseData.data)) {
returnData.push.apply(returnData, responseData.data as IDataObject[]);
} else {
returnData.push(responseData.data as IDataObject);
if (responseData.data !== null) {
returnData.push(responseData.data as IDataObject);
}
}
}
return [this.helpers.returnJsonArray(returnData)];

View File

@@ -1,4 +1,7 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
@@ -6,17 +9,16 @@ import {
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import { IDataObject } from 'n8n-workflow';
import {
IDataObject,
} from 'n8n-workflow';
export async function webflowApiRequest(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('webflowApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const authenticationMethod = this.getNodeParameter('authentication', 0);
let options: OptionsWithUri = {
headers: {
authorization: `Bearer ${credentials.accessToken}`,
'accept-version': '1.0.0',
},
method,
@@ -31,14 +33,22 @@ export async function webflowApiRequest(this: IHookFunctions | IExecuteFunctions
}
try {
return await this.helpers.request!(options);
} catch (error) {
if (authenticationMethod === 'accessToken') {
const credentials = this.getCredentials('webflowApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let errorMessage = error.message;
if (error.response.body && error.response.body.err) {
errorMessage = error.response.body.err;
options.headers!['authorization'] = `Bearer ${credentials.accessToken}`;
return await this.helpers.request!(options);
} else {
return await this.helpers.requestOAuth2!.call(this, 'webflowOAuth2Api', options);
}
throw new Error('Webflow Error: ' + errorMessage);
} catch (error) {
if (error.response.body.err) {
throw new Error(`Webflow Error: [${error.statusCode}]: ${error.response.body.err}`);
}
return error;
}
}

View File

@@ -34,7 +34,25 @@ export class WebflowTrigger implements INodeType {
{
name: 'webflowApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
{
name: 'webflowOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
webhooks: [
{
@@ -45,6 +63,23 @@ export class WebflowTrigger implements INodeType {
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
description: 'Method of authentication.',
},
{
displayName: 'Site',
name: 'site',

View File

@@ -77,6 +77,7 @@ export class Webhook implements INodeType {
{
name: 'default',
httpMethod: '={{$parameter["httpMethod"]}}',
isFullPath: true,
responseCode: '={{$parameter["responseCode"]}}',
responseMode: '={{$parameter["responseMode"]}}',
responseData: '={{$parameter["responseData"]}}',
@@ -133,7 +134,7 @@ export class Webhook implements INodeType {
default: '',
placeholder: 'webhook',
required: true,
description: 'The path to listen to. Slashes("/") in the path are not allowed.',
description: 'The path to listen to.',
},
{
displayName: 'Response Code',

View File

@@ -0,0 +1,838 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const contactOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'contact',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'create a contact',
},
{
name: 'Get',
value: 'get',
description: 'Get a contact',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all contacts',
},
{
name: 'Update',
value: 'update',
description: 'Update a contact',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const contactFields = [
/* -------------------------------------------------------------------------- */
/* contact:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
required: true,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
description: 'Full name of contact/organisation',
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Account Number',
name: 'accountNumber',
type: 'string',
default: '',
description: 'A user defined account number',
},
// {
// displayName: 'Addresses',
// name: 'addressesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Address',
// options: [
// {
// name: 'addressesValues',
// displayName: 'Address',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'PO Box',
// value: 'POBOX',
// },
// {
// name: 'Street',
// value: 'STREET',
// },
// ],
// default: '',
// },
// {
// displayName: 'Line 1',
// name: 'line1',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Line 2',
// name: 'line2',
// type: 'string',
// default: '',
// },
// {
// displayName: 'City',
// name: 'city',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Region',
// name: 'region',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Postal Code',
// name: 'postalCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country',
// name: 'country',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Attention To',
// name: 'attentionTo',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Bank Account Details',
name: 'bankAccountDetails',
type: 'string',
default: '',
description: 'Bank account number of contact',
},
{
displayName: 'Contact Number',
name: 'contactNumber',
type: 'string',
default: '',
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
},
{
displayName: 'Contact Status',
name: 'contactStatus',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
description: 'The Contact is active and can be used in transactions',
},
{
name: 'Archived',
value: 'ARCHIVED',
description: 'The Contact is archived and can no longer be used in transactions',
},
{
name: 'GDPR Request',
value: 'GDPRREQUEST',
description: 'The Contact is the subject of a GDPR erasure request',
},
],
default: '',
description: 'Current status of a contact - see contact status types',
},
{
displayName: 'Default Currency',
name: 'defaultCurrency',
type: 'string',
default: '',
description: 'Default currency for raising invoices against contact',
},
{
displayName: 'Email',
name: 'emailAddress',
type: 'string',
default: '',
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of contact person (max length = 255)',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of contact person (max length = 255)',
},
// {
// displayName: 'Phones',
// name: 'phonesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Phone',
// options: [
// {
// name: 'phonesValues',
// displayName: 'Phones',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'Default',
// value: 'DEFAULT',
// },
// {
// name: 'DDI',
// value: 'DDI',
// },
// {
// name: 'Mobile',
// value: 'MOBILE',
// },
// {
// name: 'Fax',
// value: 'FAX',
// },
// ],
// default: '',
// },
// {
// displayName: 'Number',
// name: 'phoneNumber',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Area Code',
// name: 'phoneAreaCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country Code',
// name: 'phoneCountryCode',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Purchase Default Account Code',
name: 'purchasesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default purchases account code for contacts',
},
{
displayName: 'Sales Default Account Code',
name: 'salesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default sales account code for contacts',
},
{
displayName: 'Skype',
name: 'skypeUserName',
type: 'string',
default: '',
description: 'Skype user name of contact',
},
{
displayName: 'Tax Number',
name: 'taxNumber',
type: 'string',
default: '',
description: 'Tax number of contact',
},
{
displayName: 'Xero Network Key',
name: 'xeroNetworkKey',
type: 'string',
default: '',
description: 'Store XeroNetworkKey for contacts',
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
required: true,
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
},
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* contact:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Include Archived',
name: 'includeArchived',
type: 'boolean',
default: false,
description: `Contacts with a status of ARCHIVED will be included in the response`,
},
{
displayName: 'Order By',
name: 'orderBy',
type: 'string',
placeholder: 'contactID',
default: '',
description: 'Order by any element returned',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Asc',
value: 'ASC',
},
{
name: 'Desc',
value: 'DESC',
},
],
default: '',
description: 'Sort order',
},
{
displayName: 'Where',
name: 'where',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")',
default: '',
description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. <a href="https://developer.xero.com/documentation/api/requests-and-responses#get-modified" target="_blank">Examples Here</a>`,
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTenants',
},
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
required: true,
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Account Number',
name: 'accountNumber',
type: 'string',
default: '',
description: 'A user defined account number',
},
// {
// displayName: 'Addresses',
// name: 'addressesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Address',
// options: [
// {
// name: 'addressesValues',
// displayName: 'Address',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'PO Box',
// value: 'POBOX',
// },
// {
// name: 'Street',
// value: 'STREET',
// },
// ],
// default: '',
// },
// {
// displayName: 'Line 1',
// name: 'line1',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Line 2',
// name: 'line2',
// type: 'string',
// default: '',
// },
// {
// displayName: 'City',
// name: 'city',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Region',
// name: 'region',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Postal Code',
// name: 'postalCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country',
// name: 'country',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Attention To',
// name: 'attentionTo',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Bank Account Details',
name: 'bankAccountDetails',
type: 'string',
default: '',
description: 'Bank account number of contact',
},
{
displayName: 'Contact Number',
name: 'contactNumber',
type: 'string',
default: '',
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
},
{
displayName: 'Contact Status',
name: 'contactStatus',
type: 'options',
options: [
{
name: 'Active',
value: 'ACTIVE',
description: 'The Contact is active and can be used in transactions',
},
{
name: 'Archived',
value: 'ARCHIVED',
description: 'The Contact is archived and can no longer be used in transactions',
},
{
name: 'GDPR Request',
value: 'GDPRREQUEST',
description: 'The Contact is the subject of a GDPR erasure request',
},
],
default: '',
description: 'Current status of a contact - see contact status types',
},
{
displayName: 'Default Currency',
name: 'defaultCurrency',
type: 'string',
default: '',
description: 'Default currency for raising invoices against contact',
},
{
displayName: 'Email',
name: 'emailAddress',
type: 'string',
default: '',
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of contact person (max length = 255)',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of contact person (max length = 255)',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Full name of contact/organisation',
},
// {
// displayName: 'Phones',
// name: 'phonesUi',
// type: 'fixedCollection',
// typeOptions: {
// multipleValues: true,
// },
// default: '',
// placeholder: 'Add Phone',
// options: [
// {
// name: 'phonesValues',
// displayName: 'Phones',
// values: [
// {
// displayName: 'Type',
// name: 'type',
// type: 'options',
// options: [
// {
// name: 'Default',
// value: 'DEFAULT',
// },
// {
// name: 'DDI',
// value: 'DDI',
// },
// {
// name: 'Mobile',
// value: 'MOBILE',
// },
// {
// name: 'Fax',
// value: 'FAX',
// },
// ],
// default: '',
// },
// {
// displayName: 'Number',
// name: 'phoneNumber',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Area Code',
// name: 'phoneAreaCode',
// type: 'string',
// default: '',
// },
// {
// displayName: 'Country Code',
// name: 'phoneCountryCode',
// type: 'string',
// default: '',
// },
// ],
// },
// ],
// },
{
displayName: 'Purchase Default Account Code',
name: 'purchasesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default purchases account code for contacts',
},
{
displayName: 'Sales Default Account Code',
name: 'salesDefaultAccountCode',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getAccountCodes',
},
default: '',
description: 'The default sales account code for contacts',
},
{
displayName: 'Skype',
name: 'skypeUserName',
type: 'string',
default: '',
description: 'Skype user name of contact',
},
{
displayName: 'Tax Number',
name: 'taxNumber',
type: 'string',
default: '',
description: 'Tax number of contact',
},
{
displayName: 'Xero Network Key',
name: 'xeroNetworkKey',
type: 'string',
default: '',
description: 'Store XeroNetworkKey for contacts',
},
],
},
] as INodeProperties[];

View File

@@ -0,0 +1,76 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function xeroApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | 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.xero.com/api.xro/2.0${resource}`,
json: true
};
try {
if (body.organizationId) {
options.headers = { ...options.headers, 'Xero-tenant-id': body.organizationId };
delete body.organizationId;
}
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'xeroOAuth2Api', options);
} catch (error) {
let errorMessage;
if (error.response && error.response.body && error.response.body.Message) {
errorMessage = error.response.body.Message;
if (error.response.body.Elements) {
const elementErrors = [];
for (const element of error.response.body.Elements) {
elementErrors.push(element.ValidationErrors.map((error: IDataObject) => error.Message).join('|'));
}
errorMessage = elementErrors.join('-');
}
// Try to return the error prettier
throw new Error(`Xero error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function xeroApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
do {
responseData = await xeroApiRequest.call(this, method, endpoint, body, query);
query.page++;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData[propertyName].length !== 0
);
return returnData;
}

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