Merge branch 'master' into save-changes-warning

This commit is contained in:
Rupenieks 2020-09-09 13:41:50 +02:00
commit 6961050a42
65 changed files with 8107 additions and 167 deletions

View file

@ -30,7 +30,7 @@ n8n is split up in different modules which are all in a single mono repository.
The most important directories:
- [/docker/image](/docker/image) - Dockerfiles to create n8n containers
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers
- [/docker/compose](/docker/compose) - Examples Docker Setups
- [/packages](/packages) - The different n8n modules
- [/packages/cli](/packages/cli) - CLI code to run front- & backend
@ -57,11 +57,16 @@ dependencies are installed and the packages get linked correctly. Here a short g
The packages which n8n uses depend on a few build tools:
Linux:
Debian/Ubuntu:
```
apt-get install -y build-essential python
```
CentOS:
```
yum install gcc gcc-c++ make
```
Windows:
```
npm install -g windows-build-tools

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.79.3",
"version": "0.80.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -100,9 +100,9 @@
"lodash.get": "^4.4.2",
"mongodb": "^3.5.5",
"mysql2": "^2.0.1",
"n8n-core": "~0.43.0",
"n8n-core": "~0.44.0",
"n8n-editor-ui": "~0.55.0",
"n8n-nodes-base": "~0.74.1",
"n8n-nodes-base": "~0.75.0",
"n8n-workflow": "~0.39.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",

View file

@ -38,7 +38,12 @@ class CredentialsOverwritesClass {
}
const returnData = JSON.parse(JSON.stringify(data));
Object.assign(returnData, overwrites);
// Overwrite only if there is currently no data set
for (const key of Object.keys(overwrites)) {
if ([null, undefined, ''].includes(returnData[key])) {
returnData[key] = overwrites[key];
}
}
return returnData;
}

View file

@ -298,7 +298,7 @@ class App {
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
(req as ICustomRequest).parsedUrl = parseUrl(req);
// @ts-ignore
req.rawBody = new Buffer('', 'base64');
req.rawBody = Buffer.from('', 'base64');
next();
});

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.43.0",
"version": "0.44.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.9.0",
"version": "0.10.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -58,8 +58,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "^0.43.0",
"n8n-workflow": "^0.33.0",
"n8n-core": "^0.44.0",
"n8n-workflow": "^0.39.0",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",
"tmp-promise": "^2.0.2",

View file

@ -63,8 +63,15 @@ export async function createCustomTsconfig () {
export async function buildFiles (options?: IBuildOptions): Promise<string> {
options = options || {};
// Get the path of the TypeScript cli of this project
const tscPath = join(__dirname, '../../node_modules/.bin/tsc');
let typescriptPath;
// Check for OS to designate correct tsc path
if (process.platform === 'win32') {
typescriptPath = '../../node_modules/TypeScript/lib/tsc';
} else {
typescriptPath = '../../node_modules/.bin/tsc';
}
const tscPath = join(__dirname, typescriptPath);
const tsconfigData = await createCustomTsconfig();

View file

@ -10,11 +10,26 @@ export class CustomerIoApi implements ICredentialType {
documentationUrl = 'customerIo';
properties = [
{
displayName: 'App API Key',
name: 'apiKey',
displayName: 'Tracking API Key',
name: 'trackingApiKey',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Required for tracking API.',
required: true,
},
{
displayName: 'Tracking Site ID',
name: 'trackingSiteId',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Required for tracking API.',
},
{
displayName: 'App API Key',
name: 'appApiKey',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Required for App API.',
},
];
}

View file

@ -0,0 +1,59 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class Mqtt implements ICredentialType {
name = 'mqtt';
displayName = 'MQTT';
properties = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'Protocol',
name: 'protocol',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'mqtt',
value: 'mqtt',
},
{
name: 'ws',
value: 'ws',
},
],
default: 'mqtt',
},
{
displayName: 'Host',
name: 'host',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Port',
name: 'port',
type: 'number' as NodePropertyTypes,
default: 1883,
},
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
},
];
}

View file

@ -36,11 +36,11 @@ export class PagerDutyOAuth2Api implements ICredentialType {
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
description: 'Method of authentication.',
},
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
description: 'Method of authentication.',
},
];
}

View file

@ -0,0 +1,45 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class S3 implements ICredentialType {
name = 's3';
displayName = 'S3';
properties = [
{
displayName: 'S3 endpoint',
name: 'endpoint',
type: 'string' as NodePropertyTypes,
default: ''
},
{
displayName: 'Region',
name: 'region',
type: 'string' as NodePropertyTypes,
default: 'us-east-1',
},
{
displayName: 'Access Key Id',
name: 'accessKeyId',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Secret Access Key',
name: 'secretAccessKey',
type: 'string' as NodePropertyTypes,
default: '',
typeOptions: {
password: true,
},
},
{
displayName: 'Force path style',
name: 'forcePathStyle',
type: 'boolean' as NodePropertyTypes,
default: false
},
];
}

View file

@ -37,5 +37,12 @@ export class SalesforceOAuth2Api implements ICredentialType {
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
description: 'Method of authentication.',
},
];
}

View file

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SentryIoApi implements ICredentialType {
name = 'sentryIoApi';
displayName = 'Sentry.io API';
properties = [
{
displayName: 'Token',
name: 'token',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,46 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class SentryIoOAuth2Api implements ICredentialType {
name = 'sentryIoOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Sentry.io OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://sentry.io/oauth/authorize/',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://sentry.io/oauth/token/',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'event:admin event:read org:read project:read project:releases team:read event:write org:admin project:write team:write project:admin team:admin',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,17 @@ import {
ILoadOptionsFunctions,
} from 'n8n-core';
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
import {
get,
} from 'lodash';
/**
* Make an API request to Asana
@ -16,7 +25,7 @@ import { OptionsWithUri } from 'request';
* @param {object} body
* @returns {Promise<any>}
*/
export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object, uri?: string | undefined): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('asanaApi');
if (credentials === undefined) {
@ -30,7 +39,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
method,
body: { data: body },
qs: query,
uri: `https://app.asana.com/api/1.0/${endpoint}`,
uri: uri || `https://app.asana.com/api/1.0${endpoint}`,
json: true,
};
@ -54,3 +63,22 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions |
throw error;
}
}
export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
query.limit = 100;
do {
responseData = await asanaApiRequest.call(this, method, endpoint, body, query, uri);
uri = get(responseData, 'next_page.uri');
returnData.push.apply(returnData, responseData['data']);
} while (
responseData['next_page'] !== null
);
return returnData;
}

View file

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

View file

@ -36,7 +36,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = {headers: headers || {}, host: endpoint, method, path: `${path}?${queryToString(query).replace(/\+/g, '%2B')}`, body};
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`, secretAccessKey: `${credentials.secretAccessKey}`});
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()});
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -99,7 +99,7 @@ export class ClickUp implements INodeType {
{
name: 'clickUpApi',
required: true,
}
},
],
properties: [
{
@ -296,7 +296,7 @@ export class ClickUp implements INodeType {
const { tags } = await clickupApiRequest.call(this, 'GET', `/space/${spaceId}/tag`);
for (const tag of tags) {
const tagName = tag.name;
const tagId = tag.id;
const tagId = tag.name;
returnData.push({
name: tagName,
value: tagId,
@ -320,6 +320,23 @@ export class ClickUp implements INodeType {
}
return returnData;
},
// Get all the custom fields to display them to user so that he can
// select them easily
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const listId = this.getCurrentNodeParameter('list') as string;
const returnData: INodePropertyOptions[] = [];
const { fields } = await clickupApiRequest.call(this, 'GET', `/list/${listId}/field`);
for (const field of fields) {
const fieldName = field.name;
const fieldId = field.id;
returnData.push({
name: fieldName,
value: fieldId,
});
}
return returnData;
},
},
};
@ -846,6 +863,22 @@ export class ClickUp implements INodeType {
if (filters.dateUpdatedLt) {
qs.date_updated_lt = new Date(filters.dateUpdatedLt as string).getTime();
}
if (filters.customFieldsUi) {
const customFieldsValues = (filters.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
if (customFieldsValues) {
const customFields: IDataObject[] = [];
for (const customFieldValue of customFieldsValues) {
customFields.push({
field_id: customFieldValue.fieldId,
operator: (customFieldValue.operator === 'equal') ? '=' : customFieldValue.operator,
value: customFieldValue.value as string,
});
}
qs.custom_fields = JSON.stringify(customFields);
}
}
const listId = this.getNodeParameter('list', i) as string;
if (returnAll === true) {
responseData = await clickupApiRequestAllItems.call(this, 'tasks', 'GET', `/list/${listId}/task`, {}, qs);
@ -855,6 +888,19 @@ export class ClickUp implements INodeType {
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'member') {
const taskId = this.getNodeParameter('id', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === true) {
responseData = await clickupApiRequest.call(this, 'GET', `/task/${taskId}/member`, {}, qs);
responseData = responseData.members;
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await clickupApiRequest.call(this, 'GET', `/task/${taskId}/member`, {}, qs);
responseData = responseData.members;
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'setCustomField') {
const taskId = this.getNodeParameter('task', i) as string;
const fieldId = this.getNodeParameter('field', i) as string;
@ -984,6 +1030,19 @@ export class ClickUp implements INodeType {
responseData = await clickupApiRequest.call(this, 'POST', `/folder/${folderId}/list`, body);
}
}
if (operation === 'member') {
const listId = this.getNodeParameter('id', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === true) {
responseData = await clickupApiRequest.call(this, 'GET', `/list/${listId}/member`, {}, qs);
responseData = responseData.members;
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await clickupApiRequest.call(this, 'GET', `/list/${listId}/member`, {}, qs);
responseData = responseData.members;
responseData = responseData.splice(0, qs.limit);
}
}
if (operation === 'customFields') {
const listId = this.getNodeParameter('list', i) as string;
responseData = await clickupApiRequest.call(this, 'GET', `/list/${listId}/field`);

View file

@ -40,6 +40,11 @@ export const listOperations = [
value: 'getAll',
description: 'Get all lists',
},
{
name: 'Member',
value: 'member',
description: 'Get list members',
},
{
name: 'Update',
value: 'update',
@ -229,6 +234,68 @@ export const listFields = [
],
},
/* -------------------------------------------------------------------------- */
/* list:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'List ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'list',
],
operation: [
'member',
],
},
},
description: 'Task ID',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'list',
],
operation: [
'member',
],
},
},
default: true,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'list',
],
operation: [
'member',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* list:customFields */
/* -------------------------------------------------------------------------- */
{

View file

@ -31,12 +31,17 @@ export const taskOperations = [
description: 'Get a task',
},
{
name: 'Get all',
name: 'Get All',
value: 'getAll',
description: 'Get all tasks',
},
{
name: 'Set custom field',
name: 'Member',
value: 'member',
description: 'Get task members',
},
{
name: 'Set Custom Field',
value: 'setCustomField',
description: 'Set a custom field',
},
@ -95,7 +100,7 @@ export const taskFields = [
loadOptionsMethod: 'getSpaces',
loadOptionsDependsOn: [
'team',
]
],
},
required: true,
},
@ -190,7 +195,7 @@ export const taskFields = [
loadOptionsMethod: 'getLists',
loadOptionsDependsOn: [
'folder',
]
],
},
required: true,
},
@ -239,7 +244,6 @@ export const taskFields = [
typeOptions: {
loadOptionsMethod: 'getAssignees',
},
default: [],
},
{
@ -302,6 +306,12 @@ export const taskFields = [
description: 'Integer mapping as 1 : Urgent, 2 : High, 3 : Normal, 4 : Low',
default: 3,
},
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Start Date Time',
name: 'startDateTime',
@ -457,6 +467,12 @@ export const taskFields = [
default: '',
description: 'status'
},
{
displayName: 'Start Date',
name: 'startDate',
type: 'dateTime',
default: '',
},
{
displayName: 'Start Date Time',
name: 'startDateTime',
@ -631,7 +647,7 @@ export const taskFields = [
loadOptionsMethod: 'getLists',
loadOptionsDependsOn: [
'folder',
]
],
},
required: true,
},
@ -712,6 +728,91 @@ export const taskFields = [
default: [],
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'Filter by custom fields ',
default: {},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field ID',
name: 'fieldId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
default: '',
description: 'The ID of the field to add custom field to.',
},
{
displayName: 'Operator',
name: 'operator',
type: 'options',
options: [
{
name: 'Equal',
value: 'equal',
},
{
name: '<',
value: '<',
},
{
name: '<=',
value: '<=',
},
{
name: '>',
value: '>',
},
{
name: '>=',
value: '>=',
},
{
name: '!=',
value: '!=',
},
{
name: 'Is Null',
value: 'IS NULL',
},
{
name: 'Is Not Null',
value: 'IS NOT NULL',
},
],
default: 'equal',
description: 'The value to set on custom field.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
displayOptions: {
hide: {
operator: [
'IS NULL',
'IS NOT NULL',
],
},
},
default: '',
description: 'The value to set on custom field.',
},
],
},
],
},
{
displayName: 'Date Created Greater Than',
name: 'dateCreatedGt',
@ -841,6 +942,68 @@ export const taskFields = [
description: 'task ID',
},
/* -------------------------------------------------------------------------- */
/* task:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'Task ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'task',
],
operation: [
'member',
],
},
},
description: 'Task ID',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'task',
],
operation: [
'member',
],
},
},
default: true,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'task',
],
operation: [
'member',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* task:setCustomField */
/* -------------------------------------------------------------------------- */
{

View file

@ -118,6 +118,7 @@ export class ClockifyTrigger implements INodeType {
if (Array.isArray(result) && result.length !== 0) {
result = [this.helpers.returnJsonArray(result)];
return result;
}
return null;
}

View file

@ -0,0 +1,199 @@
import { INodeProperties } from 'n8n-workflow';
export const campaignOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'campaign',
],
},
},
options: [
{
name: 'Get',
value: 'get',
},
{
name: 'Get All',
value: 'getAll',
},
{
name: 'Get Metrics',
value: 'getMetrics',
},
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const campaignFields = [
/* -------------------------------------------------------------------------- */
/* campaign:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'number',
required: true,
default: 0,
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'get',
]
},
},
description: 'The unique identifier for the campaign',
},
/* -------------------------------------------------------------------------- */
/* campaign:getMetrics */
/* -------------------------------------------------------------------------- */
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'number',
required: true,
default: 0,
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getMetrics',
]
},
},
description: 'The unique identifier for the campaign',
},
{
displayName: 'Period',
name: 'period',
type: 'options',
default: 'days',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getMetrics',
]
},
},
description: 'Specify metric period',
options: [
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Months',
value: 'months',
},
]
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getMetrics',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getMetrics'
],
jsonParameters: [
false,
],
},
},
options: [
{
displayName: 'Steps',
name: 'steps',
type: 'number',
default: 0,
description: 'Integer specifying how many steps to return. Defaults to the maximum number of timeperiods available, or 12 when using the months period. Maximum timeperiods available are 24 hours, 45 days, 12 weeks and 120 months',
typeOptions: {
minValue: 0,
maxValue: 120,
}
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: 'empty',
description: 'Specify metric type',
options: [
{
name: 'Empty',
value: 'empty',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Push',
value: 'push',
},
{
name: 'Slack',
value: 'slack',
},
{
name: 'twilio',
value: 'twilio',
},
{
name: 'Urban Airship',
value: 'urbanAirship',
},
{
name: 'Webhook',
value: 'webhook',
},
]
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,189 @@
import { INodeProperties } from 'n8n-workflow';
export const customerOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'customer',
],
},
},
options: [
{
name: 'Create/Update',
value: 'upsert',
description: 'Create/Update a customer.',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a customer.',
},
],
default: 'upsert',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const customerFields = [
/* -------------------------------------------------------------------------- */
/* customer:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
name: 'id',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'customer',
],
operation: [
'delete',
]
},
},
description: 'The unique identifier for the customer.',
},
/* -------------------------------------------------------------------------- */
/* customer:upsert */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
name: 'id',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'customer',
],
operation: [
'upsert',
]
},
},
description: 'The unique identifier for the customer.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'customer',
],
operation: [
'upsert',
],
},
},
},
{
displayName: ' Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'customer',
],
operation: [
'upsert',
],
jsonParameters: [
true,
],
},
},
description: 'Object of values to set as described <a href="https://github.com/agilecrm/rest-api#1-companys---companies-api" target="_blank">here</a>.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'customer',
],
operation: [
'upsert',
],
jsonParameters: [
false,
],
},
},
options: [
{
displayName: 'Custom Properties',
name: 'customProperties',
type: 'fixedCollection',
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Property',
name: 'customProperty',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
required: true,
default: '',
description: 'Property name.',
placeholder: 'Plan',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
default: '',
description: 'Property value.',
placeholder: 'Basic',
},
],
},
]
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'The email address of the user.',
},
{
displayName: 'Created at',
name: 'createdAt',
type: 'dateTime',
default: '',
description: 'The UNIX timestamp from when the user was created.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,327 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
} from 'n8n-workflow';
import { customerIoApiRequest, validateJSON } from './GenericFunctions';
import { campaignOperations, campaignFields } from './CampaignDescription';
import { customerOperations, customerFields } from './CustomerDescription';
import { eventOperations, eventFields } from './EventDescription';
import { segmentOperations, segmentFields } from './SegmentDescription';
export class CustomerIo implements INodeType {
description: INodeTypeDescription = {
displayName: 'Customer.io',
name: 'customerIo',
icon: 'file:customerio.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Customer.io API',
defaults: {
name: 'CustomerIo',
color: '#ffcd00',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'customerIoApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Customer',
value: 'customer',
},
{
name: 'Event',
value: 'event',
},
{
name: 'Campaign',
value: 'campaign',
},
{
name: 'Segment',
value: 'segment',
},
],
default: 'customer',
description: 'Resource to consume.',
},
// CAMPAIGN
...campaignOperations,
...campaignFields,
// CUSTOMER
...customerOperations,
...customerFields,
// EVENT
...eventOperations,
...eventFields,
// SEGMENT
...segmentOperations,
...segmentFields
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: IDataObject[] = [];
const items = this.getInputData();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
const body: IDataObject = {};
let responseData;
for (let i = 0; i < items.length; i++) {
if (resource === 'campaign') {
if (operation === 'get') {
const campaignId = this.getNodeParameter('campaignId', i) as number;
const endpoint = `/campaigns/${campaignId}`;
responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta');
responseData = responseData.campaign;
}
if (operation === 'getAll') {
const endpoint = `/campaigns`;
responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta');
responseData = responseData.campaigns;
}
if (operation === 'getMetrics') {
const campaignId = this.getNodeParameter('campaignId', i) as number;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const period = this.getNodeParameter('period', i) as string;
let endpoint = `/campaigns/${campaignId}/metrics`;
if (period !== 'days') {
endpoint = `${endpoint}?period=${period}`;
}
if (additionalFields.steps) {
body.steps = additionalFields.steps as number;
}
if (additionalFields.type) {
if (additionalFields.type === 'urbanAirship') {
additionalFields.type = 'urban_airship';
} else {
body.type = additionalFields.type as string;
}
}
responseData = await customerIoApiRequest.call(this, 'GET', endpoint, body, 'beta');
responseData = responseData.metric;
}
}
}
if (resource === 'customer') {
if (operation === 'upsert') {
const id = this.getNodeParameter('id', i) as number;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.customProperties) {
const data: any = {}; // tslint:disable-line:no-any
//@ts-ignore
additionalFields.customProperties.customProperty.map(property => {
data[property.key] = property.value;
});
body.data = data;
}
if (additionalFields.email) {
body.email = additionalFields.email as string;
}
if (additionalFields.createdAt) {
body.created_at = new Date(additionalFields.createdAt as string).getTime() / 1000;
}
}
const endpoint = `/customers/${id}`;
responseData = await customerIoApiRequest.call(this, 'PUT', endpoint, body, 'tracking');
responseData = Object.assign({ id }, body);
}
if (operation === 'delete') {
const id = this.getNodeParameter('id', i) as number;
body.id = id;
const endpoint = `/customers/${id}`;
await customerIoApiRequest.call(this, 'DELETE', endpoint, body, 'tracking');
responseData = {
success: true,
};
}
}
if (resource === 'event') {
if (operation === 'track') {
const customerId = this.getNodeParameter('customerId', i) as number;
const eventName = this.getNodeParameter('eventName', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
body.name = eventName;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const data: any = {}; // tslint:disable-line:no-any
if (additionalFields.customAttributes) {
//@ts-ignore
additionalFields.customAttributes.customAttribute.map(property => {
data[property.key] = property.value;
});
}
if (additionalFields.type) {
data.type = additionalFields.type as string;
}
body.data = data;
}
const endpoint = `/customers/${customerId}/events`;
await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking');
responseData = {
success: true,
};
}
if (operation === 'trackAnonymous') {
const eventName = this.getNodeParameter('eventName', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
body.name = eventName;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const data: any = {}; // tslint:disable-line:no-any
if (additionalFields.customAttributes) {
//@ts-ignore
additionalFields.customAttributes.customAttribute.map(property => {
data[property.key] = property.value;
});
}
body.data = data;
}
const endpoint = `/events`;
await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking');
responseData = {
success: true,
};
}
}
if (resource === 'segment') {
const segmentId = this.getNodeParameter('segmentId', i) as number;
const customerIds = this.getNodeParameter('customerIds', i) as string;
body.id = segmentId;
body.ids = customerIds.split(',');
let endpoint = '';
if (operation === 'add') {
endpoint = `/segments/${segmentId}/add_customers`;
} else {
endpoint = `/segments/${segmentId}/remove_customers`;
}
responseData = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'tracking');
responseData = {
success: true,
};
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as unknown as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -11,7 +11,7 @@ import {
} from 'n8n-workflow';
import {
apiRequest,
customerIoApiRequest,
eventExists,
} from './GenericFunctions';
@ -34,7 +34,7 @@ export class CustomerIoTrigger implements INodeType {
description: 'Starts the workflow on a Customer.io update. (Beta)',
defaults: {
name: 'Customer.io Trigger',
color: '#7131ff',
color: '#ffcd00',
},
inputs: [],
outputs: ['main'],
@ -237,7 +237,7 @@ export class CustomerIoTrigger implements INodeType {
const endpoint = '/reporting_webhooks';
let { reporting_webhooks: webhooks } = await apiRequest.call(this, 'GET', endpoint, {});
let { reporting_webhooks: webhooks } = await customerIoApiRequest.call(this, 'GET', endpoint, {}, 'beta');
if (webhooks === null) {
webhooks = [];
@ -295,7 +295,7 @@ export class CustomerIoTrigger implements INodeType {
events: data,
};
webhook = await apiRequest.call(this, 'POST', endpoint, body);
webhook = await customerIoApiRequest.call(this, 'POST', endpoint, body, 'beta');
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = webhook.id as string;
@ -307,7 +307,7 @@ export class CustomerIoTrigger implements INodeType {
if (webhookData.webhookId !== undefined) {
const endpoint = `/reporting_webhooks/${webhookData.webhookId}`;
try {
await apiRequest.call(this, 'DELETE', endpoint, {});
await customerIoApiRequest.call(this, 'DELETE', endpoint, {}, 'beta');
} catch (e) {
return false;
}

View file

@ -0,0 +1,295 @@
import { INodeProperties } from 'n8n-workflow';
export const eventOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'event',
],
},
},
options: [
{
name: 'Track',
value: 'track',
description: 'Track a customer event.',
},
{
name: 'Track Anonymous',
value: 'trackAnonymous',
description: 'Track an anonymous event.',
},
],
default: 'track',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const eventFields = [
/* -------------------------------------------------------------------------- */
/* event:track */
/* -------------------------------------------------------------------------- */
{
displayName: 'Customer ID',
name: 'customerId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'track',
]
},
},
description: 'The unique identifier for the customer.',
},
{
displayName: 'Event Name',
name: 'eventName',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'track',
]
},
},
description: 'Name of the event to track.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'track',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'track',
],
jsonParameters: [
true,
],
},
},
description: 'Object of values to set as described <a href="https://customer.io/docs/api-triggered-data-format#basic-data-formatting" target="_blank">here</a>.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'track',
],
jsonParameters: [
false,
]
},
},
options: [
{
displayName: 'Custom Attributes',
name: 'customAttributes',
type: 'fixedCollection',
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Attribute',
name: 'customAttribute',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
required: true,
default: '',
description: 'Attribute name.',
placeholder: 'Price',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
default: '',
description: 'Attribute value.',
placeholder: '25.50',
},
],
},
]
},
{
displayName: 'Type',
name: 'type',
type: 'string',
default: '',
description: 'Used to change event type. For Page View events set to "page".',
},
],
},
/* -------------------------------------------------------------------------- */
/* event:track anonymous */
/* -------------------------------------------------------------------------- */
{
displayName: 'Event Name',
name: 'eventName',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'trackAnonymous',
]
},
},
description: 'The unique identifier for the customer.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'trackAnonymous',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'trackAnonymous',
],
jsonParameters: [
true,
],
},
},
description: 'Object of values to set as described <a href="https://customer.io/docs/api-triggered-data-format#basic-data-formatting" target="_blank">here</a>.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'trackAnonymous',
],
jsonParameters: [
false,
]
},
},
options: [
{
displayName: 'Custom Attributes',
name: 'customAttributes',
type: 'fixedCollection',
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Attribute',
name: 'customAttribute',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
required: true,
default: '',
description: 'Attribute name.',
placeholder: 'Price',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
default: '',
description: 'Attribute value.',
placeholder: '25.50',
},
],
},
]
},
],
},
] as INodeProperties[];

View file

@ -16,7 +16,7 @@ import {
get,
} from 'lodash';
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
export async function customerIoApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, baseApi?: string, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('customerIoApi');
if (credentials === undefined) {
@ -28,14 +28,26 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${credentials.apiKey}`,
},
method,
body,
qs: query,
uri: `https://beta-api.customer.io/v1/api${endpoint}`,
uri: '',
json: true,
};
if (baseApi === 'tracking') {
options.uri = `https://track.customer.io/api/v1${endpoint}`;
const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64');
Object.assign(options.headers, { 'Authorization': `Basic ${basicAuthKey}` });
} else if (baseApi === 'api') {
options.uri = `https://api.customer.io/v1/api${endpoint}`;
const basicAuthKey = Buffer.from(`${credentials.trackingSiteId}:${credentials.trackingApiKey}`).toString('base64');
Object.assign(options.headers, { 'Authorization': `Basic ${basicAuthKey}` });
} else if (baseApi === 'beta') {
options.uri = `https://beta-api.customer.io/v1/api${endpoint}`;
Object.assign(options.headers, { 'Authorization': `Bearer ${credentials.appApiKey as string}` });
}
try {
return await this.helpers.request!(options);
} catch (error) {
@ -63,3 +75,13 @@ export function eventExists(currentEvents: string[], webhookEvents: IDataObject)
}
return true;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}

View file

@ -0,0 +1,73 @@
import { INodeProperties } from 'n8n-workflow';
export const segmentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'segment',
],
},
},
options: [
{
name: 'Add Customer',
value: 'add',
},
{
name: 'Remove Customer',
value: 'remove',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const segmentFields = [
/* -------------------------------------------------------------------------- */
/* segment:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Segment ID',
name: 'segmentId',
type: 'number',
required: true,
default: 0,
displayOptions: {
show: {
resource: [
'segment',
],
operation: [
'add',
'remove',
]
},
},
description: 'The unique identifier of the segment.',
},
{
displayName: 'Customer IDs',
name: 'customerIds',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'segment',
],
operation: [
'add',
'remove',
]
},
},
description: 'A list of customer ids to add to the segment.',
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -17,6 +17,24 @@ import {
import * as ftpClient from 'promise-ftp';
import * as sftpClient from 'ssh2-sftp-client';
interface ReturnFtpItem {
type: string;
name: string;
size: number;
accessTime: Date;
modifyTime: Date;
rights: {
user: string;
group: string;
other: string;
};
owner: string | number;
group: string | number;
target: string;
sticky?: boolean;
path: string;
}
export class Ftp implements INodeType {
description: INodeTypeDescription = {
displayName: 'FTP',
@ -220,6 +238,21 @@ export class Ftp implements INodeType {
description: 'Path of directory to list contents of.',
required: true,
},
{
displayName: 'Recursive',
displayOptions: {
show: {
operation: [
'list',
],
},
},
name: 'recursive',
type: 'boolean',
default: false,
description: 'Return object representing all directories / objects recursively found within SFTP server',
required: true,
},
],
};
@ -234,6 +267,7 @@ export class Ftp implements INodeType {
let credentials: ICredentialDataDecryptedObject | undefined = undefined;
const protocol = this.getNodeParameter('protocol', 0) as string;
if (protocol === 'sftp') {
credentials = this.getCredentials('sftp');
} else {
@ -244,11 +278,11 @@ export class Ftp implements INodeType {
throw new Error('Failed to get credentials!');
}
let ftp: ftpClient;
let sftp: sftpClient;
let ftp : ftpClient;
let sftp : sftpClient;
if (protocol === 'sftp') {
sftp = new sftpClient();
await sftp.connect({
host: credentials.host as string,
port: credentials.port as number,
@ -258,7 +292,6 @@ export class Ftp implements INodeType {
} else {
ftp = new ftpClient();
await ftp.connect({
host: credentials.host as string,
port: credentials.port as number,
@ -286,8 +319,16 @@ export class Ftp implements INodeType {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
responseData = await sftp!.list(path);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
responseData = await callRecursiveList(path, sftp!, normalizeSFtpItem);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
} else {
responseData = await sftp!.list(path);
responseData.forEach(item => normalizeSFtpItem(item as sftpClient.FileInfo, path));
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
}
if (operation === 'download') {
@ -347,8 +388,16 @@ export class Ftp implements INodeType {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
responseData = await ftp!.list(path);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
responseData = await callRecursiveList(path, ftp!, normalizeFtpItem);
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
} else {
responseData = await ftp!.list(path);
responseData.forEach(item => normalizeFtpItem(item as ftpClient.ListingElement, path));
returnItems.push.apply(returnItems, this.helpers.returnJsonArray(responseData as unknown as IDataObject[]));
}
}
if (operation === 'download') {
@ -432,3 +481,54 @@ export class Ftp implements INodeType {
return [returnItems];
}
}
function normalizeFtpItem(input: ftpClient.ListingElement, path: string) {
const item = input as unknown as ReturnFtpItem;
item.modifyTime = input.date;
item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`;
// @ts-ignore
item.date = undefined;
}
function normalizeSFtpItem(input: sftpClient.FileInfo, path: string) {
const item = input as unknown as ReturnFtpItem;
item.accessTime = new Date(input.accessTime);
item.modifyTime = new Date(input.modifyTime);
item.path = `${path}${path.endsWith('/') ? '' : '/'}${item.name}`;
}
async function callRecursiveList(path: string, client: sftpClient | ftpClient, normalizeFunction: (input: ftpClient.ListingElement & sftpClient.FileInfo, path: string) => void) {
const pathArray : string[] = [path];
let currentPath = path;
const directoryItems : sftpClient.FileInfo[] = [];
let index = 0;
do {
// tslint:disable-next-line: array-type
const returnData : sftpClient.FileInfo[] | (string | ftpClient.ListingElement)[] = await client.list(pathArray[index]);
// @ts-ignore
returnData.map((item : sftpClient.FileInfo) => {
if ((pathArray[index] as string).endsWith('/')) {
currentPath = `${pathArray[index]}${item.name}`;
} else {
currentPath = `${pathArray[index]}/${item.name}`;
}
// Is directory
if (item.type === 'd') {
pathArray.push(currentPath);
}
normalizeFunction(item as ftpClient.ListingElement & sftpClient.FileInfo, currentPath);
directoryItems.push(item);
});
index++;
} while (index <= pathArray.length - 1);
return directoryItems;
}

View file

@ -14,7 +14,6 @@ import {
getFileSha,
} from './GenericFunctions';
export class Github implements INodeType {
description: INodeTypeDescription = {
displayName: 'GitHub',

View file

@ -35,6 +35,11 @@ export const contactOperations = [
value: 'getAll',
description: 'Retrieve all contacts',
},
{
name: 'Update',
value: 'update',
description: 'Update a contact',
},
],
default: 'create',
description: 'The operation to perform.'
@ -347,6 +352,18 @@ export const contactFields = [
},
default: [],
},
{
displayName: 'Honorific Prefix',
name: 'honorificPrefix',
type: 'string',
default: '',
},
{
displayName: 'Honorific Suffix',
name: 'honorificSuffix',
type: 'string',
default: '',
},
{
displayName: 'Middle Name',
name: 'middleName',
@ -935,4 +952,628 @@ export const contactFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* contact:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'contact',
],
},
},
default: '',
},
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
options: [
{
name: '*',
value: '*',
},
{
name: 'Addresses',
value: 'addresses',
},
{
name: 'Biographies',
value: 'biographies',
},
{
name: 'Birthdays',
value: 'birthdays',
},
{
name: 'Cover Photos',
value: 'coverPhotos',
},
{
name: 'Email Addresses',
value: 'emailAddresses',
},
{
name: 'Events',
value: 'events',
},
{
name: 'Genders',
value: 'genders',
},
{
name: 'IM Clients',
value: 'imClients',
},
{
name: 'Interests',
value: 'interests',
},
{
name: 'Locales',
value: 'locales',
},
{
name: 'Memberships',
value: 'memberships',
},
{
name: 'Metadata',
value: 'metadata',
},
{
name: 'Names',
value: 'names',
},
{
name: 'Nicknames',
value: 'nicknames',
},
{
name: 'Occupations',
value: 'occupations',
},
{
name: 'Organizations',
value: 'organizations',
},
{
name: 'Phone Numbers',
value: 'phoneNumbers',
},
{
name: 'Photos',
value: 'photos',
},
{
name: 'Relations',
value: 'relations',
},
{
name: 'Residences',
value: 'residences',
},
{
name: 'Sip Addresses',
value: 'sipAddresses',
},
{
name: 'Skills',
value: 'skills',
},
{
name: 'URLs',
value: 'urls',
},
{
name: 'User Defined',
value: 'userDefined',
},
],
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'contact',
],
},
},
default: '',
description: 'A field mask to restrict which fields on each person are returned. Multiple fields can be specified by separating them with commas.',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'contact',
],
},
},
options: [
{
displayName: 'Etag',
name: 'etag',
type: 'string',
default: '',
description: 'The etag field in the person is nedded to make sure the contact has not changed since your last read',
},
{
displayName: 'Family Name',
name: 'familyName',
type: 'string',
default: '',
},
{
displayName: 'Given Name',
name: 'givenName',
type: 'string',
default: '',
},
{
displayName: 'Addresses',
name: 'addressesUi',
placeholder: 'Add Address',
type: 'fixedCollection',
default: {},
options: [
{
displayName: 'Address',
name: 'addressesValues',
values: [
{
displayName: 'Street Address',
name: 'streetAddress',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
description: 'City',
},
{
displayName: 'Region',
name: 'region',
type: 'string',
default: '',
description: 'Region',
},
{
displayName: 'Country Code',
name: 'countryCode',
type: 'string',
default: '',
},
{
displayName: 'Postal Code',
name: 'postalCode',
type: 'string',
default: '',
description: 'Postal code',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Home',
value: 'home',
},
{
name: 'Work',
value: 'work',
},
{
name: 'Other',
value: 'other',
},
],
default: '',
},
],
},
],
},
{
displayName: 'Birthday',
name: 'birthday',
type: 'dateTime',
default: '',
},
{
displayName: 'Company',
name: 'companyUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Company',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'companyValues',
displayName: 'Company',
values: [
{
displayName: 'Current',
name: 'current',
type: 'boolean',
default: false,
},
{
displayName: 'Domain',
name: 'domain',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
description: 'The end user specified key of the user defined data.',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
description: 'The end user specified value of the user defined data.',
default: '',
},
],
},
],
},
{
displayName: 'Emails',
name: 'emailsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Email',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'emailsValues',
displayName: 'Email',
values: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Home',
value: 'home',
},
{
name: 'Work',
value: 'work',
},
{
name: 'Other',
value: 'other',
},
],
default: '',
description: `The type of the email address. The type can be custom or one of these predefined values`,
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The email address.',
},
],
},
],
},
{
displayName: 'Events',
name: 'eventsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Event',
description: 'An event related to the person.',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'eventsValues',
displayName: 'Event',
values: [
{
displayName: 'Date',
name: 'date',
type: 'dateTime',
default: '',
description: 'The date of the event.',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Anniversary',
value: 'anniversary',
},
{
name: 'Other',
value: 'other',
},
],
default: '',
description: `The type of the event. The type can be custom or one of these predefined values`,
},
],
},
],
},
{
displayName: 'File As',
name: 'fileAs',
type: 'string',
default: '',
description: 'The name that should be used to sort the person in a list.',
},
{
displayName: 'Group',
name: 'group',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getGroups',
},
default: [],
},
{
displayName: 'Honorific Prefix',
name: 'honorificPrefix',
type: 'string',
default: '',
},
{
displayName: 'Honorific Suffix',
name: 'honorificSuffix',
type: 'string',
default: '',
},
{
displayName: 'Middle Name',
name: 'middleName',
type: 'string',
default: '',
},
{
displayName: 'Notes',
name: 'biographies',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
},
{
displayName: 'Phone',
name: 'phoneUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Phone',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'phoneValues',
displayName: 'Phone',
values: [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Home',
value: 'home',
},
{
name: 'Work',
value: 'work',
},
{
name: 'Mobile',
value: 'mobile',
},
{
name: 'Home Fax',
value: 'homeFax',
},
{
name: 'Work Fax',
value: 'workFax',
},
{
name: 'Other Fax',
value: 'otherFax',
},
{
name: 'Pager',
value: 'pager',
},
{
name: 'Work Mobile',
value: 'workMobile',
},
{
name: 'Work Pager',
value: 'workPager',
},
{
name: 'Main',
value: 'main',
},
{
name: 'Google Voice',
value: 'googleVoice',
},
{
name: 'Other',
value: 'other',
},
],
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The phone number.',
},
],
},
],
},
{
displayName: 'Relations',
name: 'relationsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Relation',
typeOptions: {
multipleValues: true
},
options: [
{
name: 'relationsValues',
displayName: 'Relation',
values: [
{
displayName: 'Person',
name: 'person',
type: 'string',
default: '',
description: 'The name of the other person this relation refers to.',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Assistant',
value: 'assistant',
},
{
name: 'Brother',
value: 'brother',
},
{
name: 'Child',
value: 'child',
},
{
name: 'Domestic Partner',
value: 'domesticPartner',
},
{
name: 'Father',
value: 'father',
},
{
name: 'Friend',
value: 'friend',
},
{
name: 'Manager',
value: 'manager',
},
{
name: 'Mother',
value: 'mother',
},
{
name: 'Parent',
value: 'parent',
},
{
name: 'Referred By',
value: 'referredBy',
},
{
name: 'Relative',
value: 'relative',
},
{
name: 'Sister',
value: 'sister',
},
{
name: 'Spouse',
value: 'spouse',
},
],
default: '',
description: `The person's relation to the other person. The type can be custom or one of these predefined values`,
},
],
},
],
},
],
},
] as INodeProperties[];

View file

@ -123,6 +123,16 @@ export class GoogleContacts implements INodeType {
body.names[0].middleName = additionalFields.middleName as string;
}
if (additionalFields.honorificPrefix) {
//@ts-ignore
body.names[0].honorificPrefix = additionalFields.honorificPrefix as string;
}
if (additionalFields.honorificSuffix) {
//@ts-ignore
body.names[0].honorificSuffix = additionalFields.honorificSuffix as string;
}
if (additionalFields.companyUi) {
const companyValues = (additionalFields.companyUi as IDataObject).companyValues as IDataObject[];
body.organizations = companyValues;
@ -298,6 +308,182 @@ export class GoogleContacts implements INodeType {
responseData[i].contactId = responseData[i].resourceName.split('/')[1];
}
}
//https://developers.google.com/people/api/rest/v1/people/updateContact
if (operation === 'update') {
const updatePersonFields = [];
const contactId = this.getNodeParameter('contactId', i) as string;
const fields = this.getNodeParameter('fields', i) as string[];
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
let etag;
if (updateFields.etag) {
etag = updateFields.etag as string;
} else {
const data = await googleApiRequest.call(
this,
'GET',
`/people/${contactId}`,
{},
{ personFields: 'Names' },
);
etag = data.etag;
}
if (fields.includes('*')) {
qs.personFields = allFields.join(',');
} else {
qs.personFields = (fields as string[]).join(',');
}
const body: IDataObject = {
etag,
names: [
{},
],
};
if (updateFields.givenName) {
//@ts-ignore
body.names[0].givenName = updateFields.givenName as string;
}
if (updateFields.familyName) {
//@ts-ignore
body.names[0].familyName = updateFields.familyName as string;
}
if (updateFields.middleName) {
//@ts-ignore
body.names[0].middleName = updateFields.middleName as string;
}
if (updateFields.honorificPrefix) {
//@ts-ignore
body.names[0].honorificPrefix = updateFields.honorificPrefix as string;
}
if (updateFields.honorificSuffix) {
//@ts-ignore
body.names[0].honorificSuffix = updateFields.honorificSuffix as string;
}
if (updateFields.companyUi) {
const companyValues = (updateFields.companyUi as IDataObject).companyValues as IDataObject[];
body.organizations = companyValues;
updatePersonFields.push('organizations');
}
if (updateFields.phoneUi) {
const phoneValues = (updateFields.phoneUi as IDataObject).phoneValues as IDataObject[];
body.phoneNumbers = phoneValues;
updatePersonFields.push('phoneNumbers');
}
if (updateFields.addressesUi) {
const addressesValues = (updateFields.addressesUi as IDataObject).addressesValues as IDataObject[];
body.addresses = addressesValues;
updatePersonFields.push('addresses');
}
if (updateFields.relationsUi) {
const relationsValues = (updateFields.relationsUi as IDataObject).relationsValues as IDataObject[];
body.relations = relationsValues;
updatePersonFields.push('relations');
}
if (updateFields.eventsUi) {
const eventsValues = (updateFields.eventsUi as IDataObject).eventsValues as IDataObject[];
for (let i = 0; i < eventsValues.length; i++) {
const [month, day, year] = moment(eventsValues[i].date as string).format('MM/DD/YYYY').split('/');
eventsValues[i] = {
date: {
day,
month,
year,
},
type: eventsValues[i].type,
};
}
body.events = eventsValues;
updatePersonFields.push('events');
}
if (updateFields.birthday) {
const [month, day, year] = moment(updateFields.birthday as string).format('MM/DD/YYYY').split('/');
body.birthdays = [
{
date: {
day,
month,
year
}
}
];
updatePersonFields.push('birthdays');
}
if (updateFields.emailsUi) {
const emailsValues = (updateFields.emailsUi as IDataObject).emailsValues as IDataObject[];
body.emailAddresses = emailsValues;
updatePersonFields.push('emailAddresses');
}
if (updateFields.biographies) {
body.biographies = [
{
value: updateFields.biographies,
contentType: 'TEXT_PLAIN',
},
];
updatePersonFields.push('biographies');
}
if (updateFields.customFieldsUi) {
const customFieldsValues = (updateFields.customFieldsUi as IDataObject).customFieldsValues as IDataObject[];
body.userDefined = customFieldsValues;
updatePersonFields.push('userDefined');
}
if (updateFields.group) {
const memberships = (updateFields.group as string[]).map((groupId: string) => {
return {
contactGroupMembership: {
contactGroupResourceName: groupId
}
};
});
body.memberships = memberships;
updatePersonFields.push('memberships');
}
if ((body.names as IDataObject[]).length > 0) {
updatePersonFields.push('names');
}
qs.updatePersonFields = updatePersonFields.join(',');
responseData = await googleApiRequest.call(
this,
'PATCH',
`/people/${contactId}:updateContact`,
body,
qs
);
responseData.contactId = responseData.resourceName.split('/')[1];
}
}
}
if (Array.isArray(responseData)) {

View file

@ -476,7 +476,7 @@ export class Gmail implements INodeType {
if (qs.labelIds == '') {
delete qs.labelIds;
} else {
qs.labelIds = (qs.labelIds as string[]).join(',');
qs.labelIds = qs.labelIds as string[];
}
}

View file

@ -442,7 +442,7 @@ export const messageFields = [
typeOptions: {
loadOptionsMethod: 'getLabels',
},
default: '',
default: [],
description: 'Only return messages with labels that match all of the specified label IDs.',
},
{

View file

@ -0,0 +1,157 @@
import {
ITriggerFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeType,
INodeTypeDescription,
ITriggerResponse,
} from 'n8n-workflow';
import * as mqtt from 'mqtt';
import {
IClientOptions,
} from 'mqtt';
export class MqttTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'MQTT Trigger',
name: 'mqttTrigger',
icon: 'file:mqtt.png',
group: ['trigger'],
version: 1,
description: 'Listens to MQTT events',
defaults: {
name: 'MQTT Trigger',
color: '#9b27af',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'mqtt',
required: true,
},
],
properties: [
{
displayName: 'Topics',
name: 'topics',
type: 'string',
default: '',
description: `Topics to subscribe to, multiple can be defined with comma.<br/>
wildcard characters are supported (+ - for single level and # - for multi level)`,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Only Message',
name: 'onlyMessage',
type: 'boolean',
default: false,
description: 'Returns only the message property.',
},
{
displayName: 'JSON Parse Message',
name: 'jsonParseMessage',
type: 'boolean',
default: false,
description: 'Try to parse the message to an object.',
},
],
},
],
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const credentials = this.getCredentials('mqtt');
if (!credentials) {
throw new Error('Credentials are mandatory!');
}
const topics = (this.getNodeParameter('topics') as string).split(',');
const options = this.getNodeParameter('options') as IDataObject;
if (!topics) {
throw new Error('Topics are mandatory!');
}
const protocol = credentials.protocol as string || 'mqtt';
const host = credentials.host as string;
const brokerUrl = `${protocol}://${host}`;
const port = credentials.port as number || 1883;
const clientOptions: IClientOptions = {
port,
};
if (credentials.username && credentials.password) {
clientOptions.username = credentials.username as string;
clientOptions.password = credentials.password as string;
}
const client = mqtt.connect(brokerUrl, clientOptions);
const self = this;
async function manualTriggerFunction() {
await new Promise((resolve, reject) => {
client.on('connect', () => {
client.subscribe(topics, (err, granted) => {
if (err) {
reject(err);
}
client.on('message', (topic: string, message: Buffer | string) => { // tslint:disable-line:no-any
let result: IDataObject = {};
message = message.toString() as string;
if (options.jsonParseMessage) {
try {
message = JSON.parse(message.toString());
} catch (err) { }
}
result.message = message;
result.topic = topic;
if (options.onlyMessage) {
//@ts-ignore
result = message;
}
self.emit([self.helpers.returnJsonArray([result])]);
resolve(true);
});
});
});
client.on('error', (error) => {
reject(error);
});
});
}
manualTriggerFunction();
async function closeFunction() {
client.end();
}
return {
closeFunction,
manualTriggerFunction,
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,155 @@
import {
sign,
} from 'aws4';
import {
get,
} from 'lodash';
import {
OptionsWithUri,
} from 'request';
import {
parseString,
} from 'xml2js';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
import { URL } from 'url';
export async function s3ApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, bucket: string, method: string, path: string, body?: string | Buffer, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
let credentials;
credentials = this.getCredentials('s3');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
if (!(credentials.endpoint as string).startsWith('http')) {
throw new Error('HTTP(S) Scheme is required in endpoint definition');
}
const endpoint = new URL(credentials.endpoint as string);
if (bucket) {
if (credentials.forcePathStyle) {
path = `/${bucket}${path}`;
} else {
endpoint.host = `${bucket}.${endpoint.host}`;
}
}
endpoint.pathname = path;
// Sign AWS API request with the user credentials
const signOpts = {
headers: headers || {},
region: region || credentials.region,
host: endpoint.host,
method,
path: `${path}?${queryToString(query).replace(/\+/g, '%2B')}`,
service: 's3',
body
};
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const options: OptionsWithUri = {
headers: signOpts.headers,
method,
qs: query,
uri: endpoint.toString(),
body: signOpts.body,
};
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response?.body.message || error.response?.body.Message || error.message;
if (error.statusCode === 403) {
if (errorMessage === 'The security token included in the request is invalid.') {
throw new Error('The S3 credentials are not valid!');
} else if (errorMessage.startsWith('The request signature we calculated does not match the signature you provided')) {
throw new Error('The S3 credentials are not valid!');
}
}
throw new Error(`S3 error response [${error.statusCode}]: ${errorMessage}`);
}
}
export async function s3ApiRequestREST(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, bucket: string, method: string, path: string, body?: string, query: IDataObject = {}, headers?: object, options: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
const response = await s3ApiRequest.call(this, bucket, method, path, body, query, headers, options, region);
try {
return JSON.parse(response);
} catch (e) {
return response;
}
}
export async function s3ApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, bucket: string, method: string, path: string, body?: string | Buffer, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
const response = await s3ApiRequest.call(this, bucket, method, path, body, query, headers, option, region);
try {
return await new Promise((resolve, reject) => {
parseString(response, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
} catch (e) {
return e;
}
}
export async function s3ApiRequestSOAPAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, propertyName: string, service: string, method: string, path: string, body?: string, query: IDataObject = {}, headers: IDataObject = {}, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await s3ApiRequestSOAP.call(this, service, method, path, body, query, headers, option, region);
//https://forums.aws.amazon.com/thread.jspa?threadID=55746
if (get(responseData, `${propertyName.split('.')[0]}.NextContinuationToken`)) {
query['continuation-token'] = get(responseData, `${propertyName.split('.')[0]}.NextContinuationToken`);
}
if (get(responseData, propertyName)) {
if (Array.isArray(get(responseData, propertyName))) {
returnData.push.apply(returnData, get(responseData, propertyName));
} else {
returnData.push(get(responseData, propertyName));
}
}
if (query.limit && query.limit <= returnData.length) {
return returnData;
}
} while (
get(responseData, `${propertyName.split('.')[0]}.IsTruncated`) !== undefined &&
get(responseData, `${propertyName.split('.')[0]}.IsTruncated`) !== 'false'
);
return returnData;
}
function queryToString(params: IDataObject) {
return Object.keys(params).map(key => key + '=' + params[key]).join('&');
}

View file

@ -0,0 +1,640 @@
import {
snakeCase,
paramCase,
} from 'change-case';
import {
createHash,
} from 'crypto';
import {
Builder,
} from 'xml2js';
import {
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
bucketFields,
bucketOperations,
} from '../Aws/S3/BucketDescription';
import {
folderFields,
folderOperations,
} from '../Aws/S3/FolderDescription';
import {
fileFields,
fileOperations,
} from '../Aws/S3/FileDescription';
import {
s3ApiRequestREST,
s3ApiRequestSOAP,
s3ApiRequestSOAPAllItems,
} from './GenericFunctions';
export class S3 implements INodeType {
description: INodeTypeDescription = {
displayName: 'S3',
name: 's3',
icon: 'file:s3.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Sends data to any S3-compatible service',
defaults: {
name: 'S3',
color: '#d05b4b',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 's3',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Bucket',
value: 'bucket',
},
{
name: 'File',
value: 'file',
},
{
name: 'Folder',
value: 'folder',
},
],
default: 'file',
description: 'The operation to perform.',
},
// BUCKET
...bucketOperations,
...bucketFields,
// FOLDER
...folderOperations,
...folderFields,
// UPLOAD
...fileOperations,
...fileFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const qs: IDataObject = {};
const headers: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
if (resource === 'bucket') {
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html
if (operation === 'create') {
let credentials;
try {
credentials = this.getCredentials('s3');
} catch (error) {
throw new Error(error);
}
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.acl) {
headers['x-amz-acl'] = paramCase(additionalFields.acl as string);
}
if (additionalFields.bucketObjectLockEnabled) {
headers['x-amz-bucket-object-lock-enabled'] = additionalFields.bucketObjectLockEnabled as boolean;
}
if (additionalFields.grantFullControl) {
headers['x-amz-grant-full-control'] = '';
}
if (additionalFields.grantRead) {
headers['x-amz-grant-read'] = '';
}
if (additionalFields.grantReadAcp) {
headers['x-amz-grant-read-acp'] = '';
}
if (additionalFields.grantWrite) {
headers['x-amz-grant-write'] = '';
}
if (additionalFields.grantWriteAcp) {
headers['x-amz-grant-write-acp'] = '';
}
let region = credentials!.region as string;
if (additionalFields.region) {
region = additionalFields.region as string;
}
const body: IDataObject = {
CreateBucketConfiguration: {
'$': {
xmlns: 'http://s3.amazonaws.com/doc/2006-03-01/',
},
}
};
let data = '';
// if credentials has the S3 defaul region (us-east-1) the body (XML) does not have to be sent.
if (region !== 'us-east-1') {
// @ts-ignore
body.CreateBucketConfiguration.LocationConstraint = [region];
const builder = new Builder();
data = builder.buildObject(body);
}
responseData = await s3ApiRequestSOAP.call(this, `${name}`, 'PUT', '', data, qs, headers);
returnData.push({ success: true });
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
if (returnAll) {
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListAllMyBucketsResult.Buckets.Bucket', '', 'GET', '');
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListAllMyBucketsResult.Buckets.Bucket', '', 'GET', '', '', qs);
responseData = responseData.slice(0, qs.limit);
}
returnData.push.apply(returnData, responseData);
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
if (operation === 'search') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject;
if (additionalFields.prefix) {
qs['prefix'] = additionalFields.prefix as string;
}
if (additionalFields.encodingType) {
qs['encoding-type'] = additionalFields.encodingType as string;
}
if (additionalFields.delmiter) {
qs['delimiter'] = additionalFields.delmiter as string;
}
if (additionalFields.fetchOwner) {
qs['fetch-owner'] = additionalFields.fetchOwner as string;
}
if (additionalFields.startAfter) {
qs['start-after'] = additionalFields.startAfter as string;
}
if (additionalFields.requesterPays) {
qs['x-amz-request-payer'] = 'requester';
}
qs['list-type'] = 2;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._ as string;
if (returnAll) {
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '', '', qs, {}, {}, region);
} else {
qs['max-keys'] = this.getNodeParameter('limit', 0) as number;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', qs, {}, {}, region);
responseData = responseData.ListBucketResult.Contents;
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData);
} else {
returnData.push(responseData);
}
}
}
if (resource === 'folder') {
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
if (operation === 'create') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const folderName = this.getNodeParameter('folderName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let path = `/${folderName}/`;
if (additionalFields.requesterPays) {
headers['x-amz-request-payer'] = 'requester';
}
if (additionalFields.parentFolderKey) {
path = `/${additionalFields.parentFolderKey}${folderName}/`;
}
if (additionalFields.storageClass) {
headers['x-amz-storage-class'] = (snakeCase(additionalFields.storageClass as string)).toUpperCase();
}
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'PUT', path, '', qs, headers, {}, region);
returnData.push({ success: true });
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
if (operation === 'delete') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const folderKey = this.getNodeParameter('folderKey', i) as string;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '/', '', { 'list-type': 2, prefix: folderKey }, {}, {}, region);
// folder empty then just delete it
if (responseData.length === 0) {
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'DELETE', `/${folderKey}`, '', qs, {}, {}, region);
responseData = { deleted: [{ 'Key': folderKey }] };
} else {
// delete everything inside the folder
const body: IDataObject = {
Delete: {
'$': {
xmlns: 'http://s3.amazonaws.com/doc/2006-03-01/',
},
Object: [],
},
};
for (const childObject of responseData) {
//@ts-ignore
(body.Delete.Object as IDataObject[]).push({
Key: childObject.Key as string
});
}
const builder = new Builder();
const data = builder.buildObject(body);
headers['Content-MD5'] = createHash('md5').update(data).digest('base64');
headers['Content-Type'] = 'application/xml';
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'POST', '/', data, { delete: '' }, headers, {}, region);
responseData = { deleted: responseData.DeleteResult.Deleted };
}
returnData.push(responseData);
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
if (operation === 'getAll') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const options = this.getNodeParameter('options', 0) as IDataObject;
if (options.folderKey) {
qs['prefix'] = options.folderKey as string;
}
if (options.fetchOwner) {
qs['fetch-owner'] = options.fetchOwner as string;
}
qs['list-type'] = 2;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
if (returnAll) {
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '', '', qs, {}, {}, region);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '', '', qs, {}, {}, region);
}
if (Array.isArray(responseData)) {
responseData = responseData.filter((e: IDataObject) => (e.Key as string).endsWith('/') && e.Size === '0' && e.Key !== options.folderKey);
if (qs.limit) {
responseData = responseData.splice(0, qs.limit as number);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'file') {
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
if (operation === 'copy') {
const sourcePath = this.getNodeParameter('sourcePath', i) as string;
const destinationPath = this.getNodeParameter('destinationPath', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
headers['x-amz-copy-source'] = sourcePath;
if (additionalFields.requesterPays) {
headers['x-amz-request-payer'] = 'requester';
}
if (additionalFields.storageClass) {
headers['x-amz-storage-class'] = (snakeCase(additionalFields.storageClass as string)).toUpperCase();
}
if (additionalFields.acl) {
headers['x-amz-acl'] = paramCase(additionalFields.acl as string);
}
if (additionalFields.grantFullControl) {
headers['x-amz-grant-full-control'] = '';
}
if (additionalFields.grantRead) {
headers['x-amz-grant-read'] = '';
}
if (additionalFields.grantReadAcp) {
headers['x-amz-grant-read-acp'] = '';
}
if (additionalFields.grantWriteAcp) {
headers['x-amz-grant-write-acp'] = '';
}
if (additionalFields.lockLegalHold) {
headers['x-amz-object-lock-legal-hold'] = (additionalFields.lockLegalHold as boolean) ? 'ON' : 'OFF';
}
if (additionalFields.lockMode) {
headers['x-amz-object-lock-mode'] = (additionalFields.lockMode as string).toUpperCase();
}
if (additionalFields.lockRetainUntilDate) {
headers['x-amz-object-lock-retain-until-date'] = additionalFields.lockRetainUntilDate as string;
}
if (additionalFields.serverSideEncryption) {
headers['x-amz-server-side-encryption'] = additionalFields.serverSideEncryption as string;
}
if (additionalFields.encryptionAwsKmsKeyId) {
headers['x-amz-server-side-encryption-aws-kms-key-id'] = additionalFields.encryptionAwsKmsKeyId as string;
}
if (additionalFields.serverSideEncryptionContext) {
headers['x-amz-server-side-encryption-context'] = additionalFields.serverSideEncryptionContext as string;
}
if (additionalFields.serversideEncryptionCustomerAlgorithm) {
headers['x-amz-server-side-encryption-customer-algorithm'] = additionalFields.serversideEncryptionCustomerAlgorithm as string;
}
if (additionalFields.serversideEncryptionCustomerKey) {
headers['x-amz-server-side-encryption-customer-key'] = additionalFields.serversideEncryptionCustomerKey as string;
}
if (additionalFields.serversideEncryptionCustomerKeyMD5) {
headers['x-amz-server-side-encryption-customer-key-MD5'] = additionalFields.serversideEncryptionCustomerKeyMD5 as string;
}
if (additionalFields.taggingDirective) {
headers['x-amz-tagging-directive'] = (additionalFields.taggingDirective as string).toUpperCase();
}
if (additionalFields.metadataDirective) {
headers['x-amz-metadata-directive'] = (additionalFields.metadataDirective as string).toUpperCase();
}
const destinationParts = destinationPath.split('/');
const bucketName = destinationParts[1];
const destination = `/${destinationParts.slice(2, destinationParts.length).join('/')}`;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'PUT', destination, '', qs, headers, {}, region);
returnData.push(responseData.CopyObjectResult);
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
if (operation === 'download') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const fileKey = this.getNodeParameter('fileKey', i) as string;
const fileName = fileKey.split('/')[fileKey.split('/').length - 1];
if (fileKey.substring(fileKey.length - 1) === '/') {
throw new Error('Downloding a whole directory is not yet supported, please provide a file key');
}
let region = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
region = region.LocationConstraint._;
const response = await s3ApiRequestREST.call(this, bucketName, 'GET', `/${fileKey}`, '', qs, {}, { encoding: null, resolveWithFullResponse: true }, region);
let mimeType: string | undefined;
if (response.headers['content-type']) {
mimeType = response.headers['content-type'];
}
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const data = Buffer.from(response.body as string, 'utf8');
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType);
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
if (operation === 'delete') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const fileKey = this.getNodeParameter('fileKey', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
if (options.versionId) {
qs.versionId = options.versionId as string;
}
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'DELETE', `/${fileKey}`, '', qs, {}, {}, region);
returnData.push({ success: true });
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
if (operation === 'getAll') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const options = this.getNodeParameter('options', 0) as IDataObject;
if (options.folderKey) {
qs['prefix'] = options.folderKey as string;
}
if (options.fetchOwner) {
qs['fetch-owner'] = options.fetchOwner as string;
}
qs['delimiter'] = '/';
qs['list-type'] = 2;
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
if (returnAll) {
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '', '', qs, {}, {}, region);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await s3ApiRequestSOAPAllItems.call(this, 'ListBucketResult.Contents', bucketName, 'GET', '', '', qs, {}, {}, region);
responseData = responseData.splice(0, qs.limit);
}
if (Array.isArray(responseData)) {
responseData = responseData.filter((e: IDataObject) => !(e.Key as string).endsWith('/') && e.Size !== '0');
if (qs.limit) {
responseData = responseData.splice(0, qs.limit as number);
}
returnData.push.apply(returnData, responseData);
}
}
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
if (operation === 'upload') {
const bucketName = this.getNodeParameter('bucketName', i) as string;
const fileName = this.getNodeParameter('fileName', i) as string;
const isBinaryData = this.getNodeParameter('binaryData', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const tagsValues = (this.getNodeParameter('tagsUi', i) as IDataObject).tagsValues as IDataObject[];
let path = '/';
let body;
if (additionalFields.requesterPays) {
headers['x-amz-request-payer'] = 'requester';
}
if (additionalFields.parentFolderKey) {
path = `/${additionalFields.parentFolderKey}/`;
}
if (additionalFields.storageClass) {
headers['x-amz-storage-class'] = (snakeCase(additionalFields.storageClass as string)).toUpperCase();
}
if (additionalFields.acl) {
headers['x-amz-acl'] = paramCase(additionalFields.acl as string);
}
if (additionalFields.grantFullControl) {
headers['x-amz-grant-full-control'] = '';
}
if (additionalFields.grantRead) {
headers['x-amz-grant-read'] = '';
}
if (additionalFields.grantReadAcp) {
headers['x-amz-grant-read-acp'] = '';
}
if (additionalFields.grantWriteAcp) {
headers['x-amz-grant-write-acp'] = '';
}
if (additionalFields.lockLegalHold) {
headers['x-amz-object-lock-legal-hold'] = (additionalFields.lockLegalHold as boolean) ? 'ON' : 'OFF';
}
if (additionalFields.lockMode) {
headers['x-amz-object-lock-mode'] = (additionalFields.lockMode as string).toUpperCase();
}
if (additionalFields.lockRetainUntilDate) {
headers['x-amz-object-lock-retain-until-date'] = additionalFields.lockRetainUntilDate as string;
}
if (additionalFields.serverSideEncryption) {
headers['x-amz-server-side-encryption'] = additionalFields.serverSideEncryption as string;
}
if (additionalFields.encryptionAwsKmsKeyId) {
headers['x-amz-server-side-encryption-aws-kms-key-id'] = additionalFields.encryptionAwsKmsKeyId as string;
}
if (additionalFields.serverSideEncryptionContext) {
headers['x-amz-server-side-encryption-context'] = additionalFields.serverSideEncryptionContext as string;
}
if (additionalFields.serversideEncryptionCustomerAlgorithm) {
headers['x-amz-server-side-encryption-customer-algorithm'] = additionalFields.serversideEncryptionCustomerAlgorithm as string;
}
if (additionalFields.serversideEncryptionCustomerKey) {
headers['x-amz-server-side-encryption-customer-key'] = additionalFields.serversideEncryptionCustomerKey as string;
}
if (additionalFields.serversideEncryptionCustomerKeyMD5) {
headers['x-amz-server-side-encryption-customer-key-MD5'] = additionalFields.serversideEncryptionCustomerKeyMD5 as string;
}
if (tagsValues) {
const tags: string[] = [];
tagsValues.forEach((o: IDataObject) => { tags.push(`${o.key}=${o.value}`); });
headers['x-amz-tagging'] = tags.join('&');
}
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'GET', '', '', { location: '' });
const region = responseData.LocationConstraint._;
if (isBinaryData) {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
if ((items[i].binary as IBinaryKeyData)[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
body = Buffer.from(binaryData.data, BINARY_ENCODING) as Buffer;
headers['Content-Type'] = binaryData.mimeType;
headers['Content-MD5'] = createHash('md5').update(body).digest('base64');
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'PUT', `${path}${fileName || binaryData.fileName}`, body, qs, headers, {}, region);
} else {
const fileContent = this.getNodeParameter('fileContent', i) as string;
body = Buffer.from(fileContent, 'utf8');
headers['Content-Type'] = 'text/html';
headers['Content-MD5'] = createHash('md5').update(fileContent).digest('base64');
responseData = await s3ApiRequestSOAP.call(this, bucketName, 'PUT', `${path}${fileName}`, body, qs, headers, {}, region);
}
returnData.push({ success: true });
}
}
}
if (resource === 'file' && operation === 'download') {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(returnData)];
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,21 +1,25 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject
} from 'n8n-workflow';
export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('salesforceOAuth2Api');
const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1];
const options: OptionsWithUri = {
method,
body: method === "GET" ? undefined : body,
qs,
uri: uri || `https://${subdomain}.salesforce.com/services/data/v39.0${resource}`,
uri: `https://${subdomain}.salesforce.com/services/data/v39.0${uri || endpoint}`,
json: true
};
try {
@ -39,7 +43,7 @@ export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILo
do {
responseData = await salesforceApiRequest.call(this, method, endpoint, body, query, uri);
uri = responseData.nextRecordsUrl;
uri = `${endpoint}/${responseData.nextRecordsUrl?.split('/')?.pop()}`;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData.nextRecordsUrl !== undefined &&

View file

@ -15,63 +15,87 @@ import {
accountFields,
accountOperations,
} from './AccountDescription';
import {
IAccount,
} from './AccountInterface';
import {
attachmentFields,
attachmentOperations,
} from './AttachmentDescription';
import {
IAttachment,
} from './AttachmentInterface';
import {
ICampaignMember,
} from './CampaignMemberInterface';
import {
caseFields,
caseOperations,
} from './CaseDescription';
import {
ICase,
ICaseComment,
} from './CaseInterface';
import {
contactFields,
contactOperations,
} from './ContactDescription';
import {
IContact,
} from './ContactInterface';
import {
salesforceApiRequest,
salesforceApiRequestAllItems,
} from './GenericFunctions';
import {
leadFields,
leadOperations,
} from './LeadDescription';
import {
ILead,
} from './LeadInterface';
import {
INote,
} from './NoteInterface';
import {
opportunityFields,
opportunityOperations,
} from './OpportunityDescription';
import {
IOpportunity,
} from './OpportunityInterface';
import {
taskFields,
taskOperations,
} from './TaskDescription';
import {
ITask,
} from './TaskInterface';
import {
userFields,
userOperations,
} from './UserDescription';
import {
IUser,
} from './UserInterface';
export class Salesforce implements INodeType {
description: INodeTypeDescription = {
@ -135,7 +159,11 @@ export class Salesforce implements INodeType {
value: 'task',
description: 'Represents a business activity such as making a phone call or other to-do items. In the user interface, and records are collectively referred to as activities.',
},
{
name: 'User',
value: 'user',
description: 'Represents a person, which is one user in system.',
},
],
default: 'lead',
description: 'Resource to consume.',
@ -154,6 +182,8 @@ export class Salesforce implements INodeType {
...taskFields,
...attachmentOperations,
...attachmentFields,
...userOperations,
...userFields,
],
};
@ -1885,6 +1915,35 @@ export class Salesforce implements INodeType {
responseData = await salesforceApiRequest.call(this, 'GET', '/sobjects/attachment');
}
}
if (resource === 'user') {
//https://developer.salesforce.com/docs/api-explorer/sobject/User/get-user-id
if (operation === 'get') {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await salesforceApiRequest.call(this, 'GET', `/sobjects/user/${userId}`);
}
//https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_query.htm
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const fields = ['id,name,email'];
if (options.fields) {
// @ts-ignore
fields.push(...options.fields.split(','));
}
try {
if (returnAll) {
qs.q = `SELECT ${fields.join(',')} FROM User`;
responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.q = `SELECT ${fields.join(',')} FROM User Limit ${limit}`;
responseData = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs);
}
} catch(err) {
throw new Error(`Salesforce Error: ${err}`);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {

View file

@ -0,0 +1,126 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const userOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a user',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all users',
},
],
default: 'get',
description: 'The operation to perform.'
}
] as INodeProperties[];
export const userFields = [
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
description: 'Id of user that needs to be fetched'
},
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'user',
],
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: [
'user',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100
},
default: 50,
description: 'How many results to return.'
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields to include separated by ,'
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,10 @@
export interface IUser {
Alias?: string;
Department?: string;
Division?: string;
Email?: string;
IsActive?: boolean;
MobilePhone?: string;
Title?: string;
Username?: string;
}

View file

@ -0,0 +1,528 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const groupOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'group',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a user to a group',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const groupFields = [
/* -------------------------------------------------------------------------- */
/* group:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'group',
],
operation: [
'add',
],
},
},
required: false,
},
{
displayName: 'Group ID',
name: 'groupId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'group',
],
operation: [
'add',
],
},
},
description: 'A Group ID is the unique identifier which you recognize a group by in your own database',
required: true,
},
{
displayName: 'Traits',
name: 'traits',
placeholder: 'Add Trait',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
resource: [
'group',
],
operation: [
'add',
],
},
},
default: {},
options: [
{
name: 'traitsUi',
displayName: 'Trait',
values: [
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email address of a user',
},
{
displayName: 'First Name',
name: 'firstname',
type: 'string',
default: '',
description: 'First name of a user',
},
{
displayName: 'Last Name',
name: 'lastname',
type: 'string',
default: '',
description: 'Last name of a user',
},
{
displayName: 'Gender',
name: 'gender',
type: 'string',
default: '',
description: 'Gender of a user',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: 'Phone number of a user',
},
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
description: 'Users username',
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
description: 'Website of a user',
},
{
displayName: 'Age',
name: 'age',
type: 'number',
default: 1,
description: 'Age of a user',
},
{
displayName: 'Avatar',
name: 'avatar',
type: 'string',
default: '',
description: 'URL to an avatar image for the user',
},
{
displayName: 'Birthday',
name: 'birthday',
type: 'dateTime',
default: '',
description: 'Users birthday',
},
{
displayName: 'Created At',
name: 'createdAt',
type: 'dateTime',
default: '',
description: 'Date the users account was first created',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'Description of the user',
},
{
displayName: 'ID',
name: 'id',
type: 'string',
default: '',
description: 'Unique ID in your database for a user',
},
{
displayName: 'Company',
name: 'company',
placeholder: 'Add Company',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'companyUi',
displayName: 'Company',
values: [
{
displayName: 'ID',
name: 'id',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Industry',
name: 'industry',
type: 'string',
default: '',
},
{
displayName: 'Employee Count',
name: 'employeeCount',
type: 'number',
default: 1,
},
{
displayName: 'Plan',
name: 'plan',
type: 'string',
default: '',
},
]
},
],
},
{
displayName: 'Address',
name: 'address',
placeholder: 'Add Address',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'addressUi',
displayName: 'Address',
values: [
{
displayName: 'Street',
name: 'street',
type: 'string',
default: '',
},
{
displayName: 'City',
name: 'city',
type: 'string',
default: '',
},
{
displayName: 'State',
name: 'state',
type: 'string',
default: '',
},
{
displayName: 'Postal Code',
name: 'postalCode',
type: 'string',
default: '',
},
{
displayName: 'Country',
name: 'country',
type: 'string',
default: '',
},
]
},
],
},
]
},
],
},
{
displayName: 'Context',
name: 'context',
placeholder: 'Add Context',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
resource: [
'group',
],
operation: [
'add',
],
},
},
default: {},
options: [
{
name: 'contextUi',
displayName: 'Context',
values: [
{
displayName: 'Active',
name: 'active',
type: 'boolean',
default: '',
description: 'Whether a user is active',
},
{
displayName: 'IP',
name: 'ip',
type: 'string',
default: '',
description: 'Current users IP address.',
},
{
displayName: 'Locale',
name: 'locate',
type: 'string',
default: '',
description: 'Locale string for the current user, for example en-US.',
},
{
displayName: 'Page',
name: 'page',
type: 'string',
default: '',
description: 'Dictionary of information about the current page in the browser, containing hash, path, referrer, search, title and url',
},
{
displayName: 'Timezone',
name: 'timezone',
type: 'string',
default: '',
description: 'Timezones are sent as tzdata strings to add user timezone information which might be stripped from the timestamp, for example America/New_York',
},
{
displayName: 'App',
name: 'app',
placeholder: 'Add App',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'appUi',
displayName: 'App',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Version',
name: 'version',
type: 'string',
default: '',
},
{
displayName: 'Build',
name: 'build',
type: 'string',
default: '',
},
]
},
],
},
{
displayName: 'Campaign',
name: 'campaign',
placeholder: 'Campaign App',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'campaignUi',
displayName: 'Campaign',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Source',
name: 'source',
type: 'string',
default: '',
},
{
displayName: 'Medium',
name: 'medium',
type: 'string',
default: '',
},
{
displayName: 'Term',
name: 'term',
type: 'string',
default: '',
},
{
displayName: 'Content',
name: 'content',
type: 'string',
default: '',
},
]
},
],
},
{
displayName: 'Device',
name: 'device',
placeholder: 'Add Device',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
name: 'deviceUi',
displayName: 'Device',
values: [
{
displayName: 'ID',
name: 'id',
type: 'string',
default: '',
},
{
displayName: 'Manufacturer',
name: 'manufacturer',
type: 'string',
default: '',
},
{
displayName: 'Model',
name: 'model',
type: 'string',
default: '',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'string',
default: '',
},
{
displayName: 'Version',
name: 'version',
type: 'string',
default: '',
},
],
},
],
},
]
},
],
},
{
displayName: 'Integration',
name: 'integrations',
placeholder: 'Add Integration',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
resource: [
'group',
],
operation: [
'add',
],
},
},
default: {},
options: [
{
name: 'integrationsUi',
displayName: 'Integration',
values: [
{
displayName: 'All',
name: 'all',
type: 'boolean',
default: true,
},
{
displayName: 'Salesforce',
name: 'salesforce',
type: 'boolean',
default: false,
},
],
},
],
},
] as INodeProperties[];

View file

@ -1,4 +1,6 @@
import { INodeProperties } from 'n8n-workflow';
import {
INodeProperties,
} from 'n8n-workflow';
export const identifyOperations = [
{

View file

@ -1,27 +1,41 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
segmentApiRequest,
} from './GenericFunctions';
import {
groupOperations,
groupFields,
} from './GroupDescription';
import {
identifyFields,
identifyOperations,
} from './IdentifyDescription';
import {
IIdentify,
} from './IdentifyInterface';
import {
trackOperations,
trackFields,
} from './TrackDescription';
import { ITrack } from './TrackInterface';
import {
ITrack, IGroup,
} from './TrackInterface';
import * as uuid from 'uuid/v4';
export class Segment implements INodeType {
@ -43,7 +57,7 @@ export class Segment implements INodeType {
{
name: 'segmentApi',
required: true,
}
},
],
properties: [
{
@ -51,10 +65,15 @@ export class Segment implements INodeType {
name: 'resource',
type: 'options',
options: [
{
name: 'Group',
value: 'group',
description: 'Group lets you associate an identified user with a group',
},
{
name: 'Identify',
value: 'identify',
description: 'Identify lets you tie a user to their actions.'
description: 'Identify lets you tie a user to their actions'
},
{
name: 'Track',
@ -65,6 +84,8 @@ export class Segment implements INodeType {
default: 'identify',
description: 'Resource to consume.',
},
...groupOperations,
...groupFields,
...identifyOperations,
...trackOperations,
...identifyFields,
@ -80,7 +101,224 @@ export class Segment implements INodeType {
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 === 'group') {
//https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#group
if (operation === 'add') {
const userId = this.getNodeParameter('userId', i) as string;
const groupId = this.getNodeParameter('groupId', i) as string;
const traits = (this.getNodeParameter('traits', i) as IDataObject).traitsUi as IDataObject;
const context = (this.getNodeParameter('context', i) as IDataObject).contextUi as IDataObject;
const integrations = (this.getNodeParameter('integrations', i) as IDataObject).integrationsUi as IDataObject;
const body: IGroup = {
groupId,
traits: {
company: {},
address: {},
},
context: {
app: {},
campaign: {},
device: {},
},
integrations: {},
};
if (userId) {
body.userId = userId as string;
} else {
body.anonymousId = uuid();
}
if (traits) {
if (traits.email) {
body.traits!.email = traits.email as string;
}
if (traits.firstname) {
body.traits!.firstname = traits.firstname as string;
}
if (traits.lastname) {
body.traits!.lastname = traits.lastname as string;
}
if (traits.gender) {
body.traits!.gender = traits.gender as string;
}
if (traits.phone) {
body.traits!.phone = traits.phone as string;
}
if (traits.username) {
body.traits!.username = traits.username as string;
}
if (traits.website) {
body.traits!.website = traits.website as string;
}
if (traits.age) {
body.traits!.age = traits.age as number;
}
if (traits.avatar) {
body.traits!.avatar = traits.avatar as string;
}
if (traits.birthday) {
body.traits!.birthday = traits.birthday as string;
}
if (traits.createdAt) {
body.traits!.createdAt = traits.createdAt as string;
}
if (traits.description) {
body.traits!.description = traits.description as string;
}
if (traits.id) {
body.traits!.id = traits.id as string;
}
if (traits.company) {
const company = (traits.company as IDataObject).companyUi as IDataObject;
if (company) {
if (company.id) {
//@ts-ignore
body.traits.company.id = company.id as string;
}
if (company.name) {
//@ts-ignore
body.traits.company.name = company.name as string;
}
if (company.industry) {
//@ts-ignore
body.traits.company.industry = company.industry as string;
}
if (company.employeeCount) {
//@ts-ignore
body.traits.company.employeeCount = company.employeeCount as number;
}
if (company.plan) {
//@ts-ignore
body.traits.company.plan = company.plan as string;
}
}
}
if (traits.address) {
const address = (traits.address as IDataObject).addressUi as IDataObject;
if (address) {
if (address.street) {
//@ts-ignore
body.traits.address.street = address.street as string;
}
if (address.city) {
//@ts-ignore
body.traits.address.city = address.city as string;
}
if (address.state) {
//@ts-ignore
body.traits.address.state = address.state as string;
}
if (address.postalCode) {
//@ts-ignore
body.traits.address.postalCode = address.postalCode as string;
}
if (address.country) {
//@ts-ignore
body.traits.address.country = address.country as string;
}
}
}
}
if (context) {
if (context.active) {
body.context!.active = context.active as boolean;
}
if (context.ip) {
body.context!.ip = context.ip as string;
}
if (context.locate) {
body.context!.locate = context.locate as string;
}
if (context.page) {
body.context!.page = context.page as string;
}
if (context.timezone) {
body.context!.timezone = context.timezone as string;
}
if (context.timezone) {
body.context!.timezone = context.timezone as string;
}
if (context.app) {
const app = (context.app as IDataObject).appUi as IDataObject;
if (app) {
if (app.name) {
//@ts-ignore
body.context.app.name = app.name as string;
}
if (app.version) {
//@ts-ignore
body.context.app.version = app.version as string;
}
if (app.build) {
//@ts-ignore
body.context.app.build = app.build as string;
}
}
}
if (context.campaign) {
const campaign = (context.campaign as IDataObject).campaignUi as IDataObject;
if (campaign) {
if (campaign.name) {
//@ts-ignore
body.context.campaign.name = campaign.name as string;
}
if (campaign.source) {
//@ts-ignore
body.context.campaign.source = campaign.source as string;
}
if (campaign.medium) {
//@ts-ignore
body.context.campaign.medium = campaign.medium as string;
}
if (campaign.term) {
//@ts-ignore
body.context.campaign.term = campaign.term as string;
}
if (campaign.content) {
//@ts-ignore
body.context.campaign.content = campaign.content as string;
}
}
}
if (context.device) {
const device = (context.device as IDataObject).deviceUi as IDataObject;
if (device) {
if (device.id) {
//@ts-ignore
body.context.device.id = device.id as string;
}
if (device.manufacturer) {
//@ts-ignore
body.context.device.manufacturer = device.manufacturer as string;
}
if (device.model) {
//@ts-ignore
body.context.device.model = device.model as string;
}
if (device.type) {
//@ts-ignore
body.context.device.type = device.type as string;
}
if (device.version) {
//@ts-ignore
body.context.device.version = device.version as string;
}
}
}
}
if (integrations) {
if (integrations.all) {
body.integrations!.all = integrations.all as boolean;
}
if (integrations.salesforce) {
body.integrations!.salesforce = integrations.salesforce as boolean;
}
}
responseData = await segmentApiRequest.call(this, 'POST', '/group', body);
}
}
if (resource === 'identify') {
//https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#identify
if (operation === 'create') {

View file

@ -1,4 +1,6 @@
import { INodeProperties } from 'n8n-workflow';
import {
INodeProperties,
} from 'n8n-workflow';
export const trackOperations = [
{

View file

@ -1,4 +1,6 @@
import { IDataObject } from "n8n-workflow";
import {
IDataObject,
} from 'n8n-workflow';
export interface ITrack {
event?: string;
@ -11,3 +13,7 @@ export interface ITrack {
properties?: IDataObject;
integrations?: IDataObject;
}
export interface IGroup extends ITrack{
groupId: string;
}

View file

@ -0,0 +1,204 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const eventOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'event',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get event by ID',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all events',
}
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const eventFields = [
/* -------------------------------------------------------------------------- */
/* event:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the organization the events belong to',
},
{
displayName: 'Project Slug',
name: 'projectSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
loadOptionsDependsOn: [
'organizationSlug',
],
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the project the events belong to',
},
{
displayName: 'Full',
name: 'full',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'getAll',
],
},
},
description: 'If this is set to true, then the event payload will include the full event body, including the stack trace',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'event',
],
},
},
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: [
'event',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
/* -------------------------------------------------------------------------- */
/* event:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the organization the events belong to',
},
{
displayName: 'Project Slug',
name: 'projectSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the project the events belong to',
},
{
displayName: 'Event ID',
name: 'eventId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'event',
],
operation: [
'get',
],
},
},
required: true,
description: 'The id of the event to retrieve (either the numeric primary-key or the hexadecimal id as reported by the raven client).',
},
] as INodeProperties[];

View file

@ -0,0 +1,104 @@
import {
OptionsWithUri
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function sentryIoApiRequest(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 authentication = this.getNodeParameter('authentication', 0);
const options: OptionsWithUri = {
headers: {},
method,
qs,
body,
uri: uri ||`https://sentry.io${resource}`,
json: true
};
if (!Object.keys(body).length) {
delete options.body;
}
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
if (options.qs.limit) {
delete options.qs.limit;
}
try {
if (authentication === 'accessToken') {
const credentials = this.getCredentials('sentryIoApi');
options.headers = {
Authorization: `Bearer ${credentials?.token}`,
};
//@ts-ignore
return this.helpers.request(options);
} else {
return await this.helpers.requestOAuth2!.call(this, 'sentryIoOAuth2Api', options);
}
} catch (error) {
throw new Error(`Sentry.io Error: ${error}`);
}
}
export async function sentryApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let link;
let uri: string | undefined;
do {
responseData = await sentryIoApiRequest.call(this, method, resource, body, query, uri, { resolveWithFullResponse: true });
link = responseData.headers.link;
uri = getNext(link);
returnData.push.apply(returnData, responseData.body);
if (query.limit && (query.limit >= returnData.length)) {
return;
}
} while (
hasMore(link)
);
return returnData;
}
function getNext(link: string) {
if (link === undefined) {
return;
}
const next = link.split(',')[1];
if (next.includes('rel="next"')) {
return next.split(';')[0].replace('<', '').replace('>','').trim();
}
}
function hasMore(link: string) {
if (link === undefined) {
return;
}
const next = link.split(',')[1];
if (next.includes('rel="next"')) {
return next.includes('results="true"');
}
}

View file

@ -0,0 +1,20 @@
export interface ICommit {
id: string;
repository?: string;
message?: string;
patch_set?: IPatchSet[];
author_name?: string;
author_email?: string;
timestamp?: Date;
}
export interface IPatchSet {
path: string;
type: string;
}
export interface IRef {
commit: string;
repository: string;
previousCommit?: string;
}

View file

@ -0,0 +1,300 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const issueOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'issue',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete an issue',
},
{
name: 'Get',
value: 'get',
description: 'Get issue by ID',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all issues',
},
{
name: 'Update',
value: 'update',
description: 'Update an issue',
},
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const issueFields = [
/* -------------------------------------------------------------------------- */
/* issue:get/delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue ID',
name: 'issueId',
type: 'string',
default: '',
placeholder: '1234',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'get',
'delete',
],
},
},
required: true,
description: 'ID of issue to get',
},
/* -------------------------------------------------------------------------- */
/* issue:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the organization the issues belong to',
},
{
displayName: 'Project Slug',
name: 'projectSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
loadOptionsDependsOn: [
'organizationSlug',
],
},
default: '',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the project the issues belong to',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'issue',
],
},
},
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: [
'issue',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Stats Period',
name: 'statsPeriod',
type: 'options',
default: '',
description: 'Time period of stats',
options: [
{
name: '14 Days',
value: '14d'
},
{
name: '24 Hours',
value: '24h'
},
]
},
{
displayName: 'Short ID lookup',
name: 'shortIdLookUp',
type: 'boolean',
default: true,
description: 'If this is set to true then short IDs are looked up by this function as well. This can cause the return value of the function to return an event issue of a different project which is why this is an opt-in',
},
]
},
/* -------------------------------------------------------------------------- */
/* issue:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue ID',
name: 'issueId',
type: 'string',
default: '',
placeholder: '1234',
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'update',
],
},
},
required: true,
description: 'ID of issue to get',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issue',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Assigned to',
name: 'assignedTo',
type: 'string',
default: '',
description: 'The actor id (or username) of the user or team that should be assigned to this issue',
},
{
displayName: 'Has Seen',
name: 'hasSeen',
type: 'boolean',
default: true,
description: 'In case this API call is invoked with a user context this allows changing of the flag that indicates if the user has seen the event',
},
{
displayName: 'Is Bookmarked',
name: 'isBookmarked',
type: 'boolean',
default: true,
description: 'In case this API call is invoked with a user context this allows changing of the bookmark flag',
},
{
displayName: 'Is Public',
name: 'isPublic',
type: 'boolean',
default: true,
description: 'Sets the issue to public or private',
},
{
displayName: 'Is Subscribed',
name: 'isSubscribed',
type: 'boolean',
default: true,
},
{
displayName: 'Status',
name: 'status',
type: 'options',
default: '',
description: 'The new status for the issue',
options: [
{
name: 'Ignored',
value: 'ignored'
},
{
name: 'Resolved',
value: 'resolved'
},
{
name: 'Resolved Next Release',
value: 'resolvedInNextRelease'
},
{
name: 'Unresolved',
value: 'unresolved'
},
]
},
]
},
] as INodeProperties[];

View file

@ -0,0 +1,205 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const organizationOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'organization',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create an organization',
},
{
name: 'Get',
value: 'get',
description: 'Get organization by slug',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all organizations',
}
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const organizationFields = [
/* -------------------------------------------------------------------------- */
/* organization:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'organization',
],
},
},
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: [
'organization',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'organization',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Member',
name: 'member',
type: 'boolean',
default: true,
description: 'Restrict results to organizations which you have membership',
},
{
displayName: 'Owner',
name: 'owner',
type: 'boolean',
default: true,
description: 'Restrict results to organizations which you are the owner',
},
]
},
/* -------------------------------------------------------------------------- */
/* organization:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'organization',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the organization the team should be created for',
},
/* -------------------------------------------------------------------------- */
/* organization:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'organization',
],
operation: [
'create',
],
},
},
required: true,
description: 'The slug of the organization the team should be created for',
},
{
displayName: 'Agree to Terms',
name: 'agreeTerms',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'organization',
],
operation: [
'create',
],
},
},
description: 'Signaling you agree to the applicable terms of service and privacy policy of Sentry.io',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'organization',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Slug',
name: 'slug',
type: 'string',
default: '',
description: 'The unique URL slug for this organization. If this is not provided a slug is automatically generated based on the name',
},
]
},
] as INodeProperties[];

View file

@ -0,0 +1,194 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const projectOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'project',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get project by ID',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all projects',
}
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const projectFields = [
/* -------------------------------------------------------------------------- */
/* project:create/get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'project',
],
operation: [
'create',
'get',
'update',
'delete',
],
},
},
required: true,
description: 'The slug of the organization the events belong to',
},
{
displayName: 'Project Slug',
name: 'projectSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
loadOptionsDependsOn: [
'organizationSlug',
],
},
default: '',
displayOptions: {
show: {
resource: [
'project',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the project to retrieve',
},
{
displayName: 'Team Slug',
name: 'teamSlug',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'project',
],
operation: [
'create',
'update',
'delete',
],
},
},
required: true,
description: 'The slug of the team to create a new project for',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'project',
],
operation: [
'create',
],
},
},
required: true,
description: 'The name for the new project',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'project',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Slug',
name: 'slug',
type: 'string',
default: '',
description: 'Optionally a slug for the new project. If its not provided a slug is generated from the name',
},
]
},
/* -------------------------------------------------------------------------- */
/* project:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'project',
],
},
},
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: [
'project',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
] as INodeProperties[];

View file

@ -0,0 +1,429 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const releaseOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'release',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a release',
},
{
name: 'Get',
value: 'get',
description: 'Get release by version identifier',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all releases',
},
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const releaseFields = [
/* -------------------------------------------------------------------------- */
/* release:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the organization the releases belong to',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'release',
],
},
},
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: [
'release',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: 'This parameter can be used to create a “starts with” filter for the version',
},
]
},
/* -------------------------------------------------------------------------- */
/* release:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the organization the release belongs to',
},
{
displayName: 'Version',
name: 'version',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'get',
],
},
},
required: true,
description: 'The version identifier of the release',
},
/* -------------------------------------------------------------------------- */
/* release:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'create',
],
},
},
required: true,
description: 'The slug of the organization the release belongs to',
},
{
displayName: 'Version',
name: 'version',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'create',
],
},
},
required: true,
description: ' a version identifier for this release. Can be a version number, a commit hash etc',
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'create',
],
},
},
required: true,
description: 'A URL that points to the release. This can be the path to an online interface to the sourcecode for instance',
},
{
displayName: 'Projects',
name: 'projects',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
default: '',
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'create',
],
},
},
required: true,
description: 'A list of project slugs that are involved in this release',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'release',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Date released',
name: 'dateReleased',
type: 'dateTime',
default: '',
description: 'an optional date that indicates when the release went live. If not provided the current time is assumed',
},
{
displayName: 'Commits',
name: 'commits',
description: 'an optional list of commit data to be associated with the release',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'commitProperties',
displayName: 'Commit Properties',
values: [
{
displayName: 'Id',
name: 'id',
type: 'string',
default: '',
description: 'the sha of the commit',
required: true
},
{
displayName: 'Author Email',
name: 'authorEmail',
type: 'string',
default: '',
description: 'Authors email',
},
{
displayName: 'Author Name',
name: 'authorName',
type: 'string',
default: '',
description: 'Name of author',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
default: '',
description: 'Message of commit',
},
{
displayName: 'Patch Set',
name: 'patchSet',
description: 'A list of the files that have been changed in the commit. Specifying the patch_set is necessary to power suspect commits and suggested assignees',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'patchSetProperties',
displayName: 'Patch Set Properties',
values: [
{
displayName: 'Path',
name: 'path',
type: 'string',
default: '',
description: 'he path to the file. Both forward and backward slashes are supported',
required: true
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: '',
description: 'he types of changes that happend in that commit',
options: [
{
name: 'Add',
value: 'add'
},
{
name: 'Modify',
value: 'modify'
},
{
name: 'Delete',
value: 'delete'
},
]
},
]
},
],
},
{
displayName: 'Repository',
name: 'repository',
type: 'string',
default: '',
description: 'Repository name',
},
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'dateTime',
default: '',
description: 'Timestamp of commit',
},
]
},
],
},
{
displayName: 'Refs',
name: 'refs',
description: 'an optional way to indicate the start and end commits for each repository included in a release',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'refProperties',
displayName: 'Ref Properties',
values: [
{
displayName: 'Commit',
name: 'commit',
type: 'string',
default: '',
description: 'the head sha of the commit',
required: true
},
{
displayName: 'Repository',
name: 'repository',
type: 'string',
default: '',
description: 'Repository name',
required: true
},
{
displayName: 'Previous Commit',
name: 'previousCommit',
type: 'string',
default: '',
description: 'the sha of the HEAD of the previous release',
},
]
},
],
},
]
},
] as INodeProperties[];

View file

@ -0,0 +1,558 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
eventOperations,
eventFields,
} from './EventDescription';
import {
issueOperations,
issueFields,
} from './IssueDescription';
import {
organizationFields,
organizationOperations,
} from './OrganizationDescription';
import {
projectOperations,
projectFields,
} from './ProjectDescription';
import {
releaseOperations,
releaseFields,
} from './ReleaseDescription';
import {
teamOperations,
teamFields,
} from './TeamDescription';
import {
sentryIoApiRequest,
sentryApiRequestAllItems,
} from './GenericFunctions';
import { ICommit, IPatchSet, IRef } from './Interface';
export class SentryIo implements INodeType {
description: INodeTypeDescription = {
displayName: 'Sentry.io',
name: 'sentryIo',
icon: 'file:sentryio.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Sentry.io API',
defaults: {
name: 'Sentry.io',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'sentryIoOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
{
name: 'sentryIoApi',
required: true,
displayOptions: {
show: {
authentication: [
'accessToken',
],
},
},
},
],
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',
type: 'options',
options: [
{
name: 'Event',
value: 'event',
},
{
name: 'Issue',
value: 'issue',
},
{
name: 'Project',
value: 'project',
},
{
name: 'Release',
value: 'release',
},
{
name: 'Organization',
value: 'organization',
},
{
name: 'Team',
value: 'team',
},
],
default: 'event',
description: 'Resource to consume.',
},
// EVENT
...eventOperations,
...eventFields,
// ISSUE
...issueOperations,
...issueFields,
// ORGANIZATION
...organizationOperations,
...organizationFields,
// PROJECT
...projectOperations,
...projectFields,
// RELEASE
...releaseOperations,
...releaseFields,
// TEAM
...teamOperations,
...teamFields
],
};
methods = {
loadOptions: {
// Get all organizations so they can be displayed easily
async getOrganizations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const organizations = await sentryApiRequestAllItems.call(this, 'GET', `/api/0/organizations/`, {});
for (const organization of organizations) {
returnData.push({
name: organization.slug,
value: organization.slug,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
// Get all projects so can be displayed easily
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const projects = await sentryApiRequestAllItems.call(this, 'GET', `/api/0/projects/`, {});
const organizationSlug = this.getNodeParameter('organizationSlug') as string;
for (const project of projects) {
if (organizationSlug !== project.organization.slug) {
continue;
}
returnData.push({
name: project.slug,
value: project.slug,
});
}
returnData.sort((a, b) => {
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
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 === 'event') {
if (operation === 'getAll') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const projectSlug = this.getNodeParameter('projectSlug', i) as string;
const full = this.getNodeParameter('full', i) as boolean;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = `/api/0/projects/${organizationSlug}/${projectSlug}/events/`;
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
qs.full = full;
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'get') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const projectSlug = this.getNodeParameter('projectSlug', i) as string;
const eventId = this.getNodeParameter('eventId', i) as string;
const endpoint = `/api/0/projects/${organizationSlug}/${projectSlug}/events/${eventId}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
}
if (resource === 'issue') {
if (operation === 'getAll') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const projectSlug = this.getNodeParameter('projectSlug', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = `/api/0/projects/${organizationSlug}/${projectSlug}/issues/`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.statsPeriod) {
qs.statsPeriod = additionalFields.statsPeriod as string;
}
if (additionalFields.shortIdLookup) {
qs.shortIdLookup = additionalFields.shortIdLookup as boolean;
}
if (additionalFields.query) {
qs.query = additionalFields.query as string;
}
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'get') {
const issueId = this.getNodeParameter('issueId', i) as string;
const endpoint = `/api/0/issues/${issueId}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
if (operation === 'delete') {
const issueId = this.getNodeParameter('issueId', i) as string;
const endpoint = `/api/0/issues/${issueId}/`;
responseData = await sentryIoApiRequest.call(this, 'DELETE', endpoint, qs);
responseData = { success: true };
}
if (operation === 'update') {
const issueId = this.getNodeParameter('issueId', i) as string;
const endpoint = `/api/0/issues/${issueId}/`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.status) {
qs.status = additionalFields.status as string;
}
if (additionalFields.assignedTo) {
qs.assignedTo = additionalFields.assignedTo as string;
}
if (additionalFields.hasSeen) {
qs.hasSeen = additionalFields.hasSeen as boolean;
}
if (additionalFields.isBookmarked) {
qs.isBookmarked = additionalFields.isBookmarked as boolean;
}
if (additionalFields.isSubscribed) {
qs.isSubscribed = additionalFields.isSubscribed as boolean;
}
if (additionalFields.isPublic) {
qs.isPublic = additionalFields.isPublic as boolean;
}
responseData = await sentryIoApiRequest.call(this, 'PUT', endpoint, qs);
}
}
if (resource === 'organization') {
if (operation === 'get') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const endpoint = `/api/0/organizations/`;
if (additionalFields.member) {
qs.member = additionalFields.member as boolean;
}
if (additionalFields.owner) {
qs.owner = additionalFields.owner as boolean;
}
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (responseData === undefined) {
responseData = [];
}
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const agreeTerms = this.getNodeParameter('agreeTerms', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const endpoint = `/api/0/organizations/`;
qs.name = name;
qs.agreeTerms = agreeTerms;
if (additionalFields.slug) {
qs.slug = additionalFields.slug as string;
}
responseData = await sentryIoApiRequest.call(this, 'POST', endpoint, qs);
}
}
if (resource === 'project') {
if (operation === 'get') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const projectSlug = this.getNodeParameter('projectSlug', i) as string;
const endpoint = `/api/0/projects/${organizationSlug}/${projectSlug}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = `/api/0/projects/`;
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
}
if (resource === 'release') {
if (operation === 'get') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const version = this.getNodeParameter('version', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/releases/${version}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
if (operation === 'getAll') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/releases/`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (additionalFields.query) {
qs.query = additionalFields.query as string;
}
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'create') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/releases/`;
const version = this.getNodeParameter('version', i) as string;
const url = this.getNodeParameter('url', i) as string;
const projects = this.getNodeParameter('projects', i) as string[];
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.dateReleased) {
qs.dateReleased = additionalFields.dateReleased as string;
}
qs.version = version;
qs.url = url;
qs.projects = projects;
if (additionalFields.commits) {
const commits: ICommit[] = [];
//@ts-ignore
// tslint:disable-next-line: no-any
additionalFields.commits.commitProperties.map((commit: any) => {
const commitObject: ICommit = { id: commit.id };
if (commit.repository) {
commitObject.repository = commit.repository;
}
if (commit.message) {
commitObject.message = commit.message;
}
if (commit.patchSet && Array.isArray(commit.patchSet)) {
commit.patchSet.patchSetProperties.map((patchSet: IPatchSet) => {
commitObject.patch_set?.push(patchSet);
});
}
if (commit.authorName) {
commitObject.author_name = commit.authorName;
}
if (commit.authorEmail) {
commitObject.author_email = commit.authorEmail;
}
if (commit.timestamp) {
commitObject.timestamp = commit.timestamp;
}
commits.push(commitObject);
});
qs.commits = commits;
}
if (additionalFields.refs) {
const refs: IRef[] = [];
//@ts-ignore
additionalFields.refs.refProperties.map((ref: IRef) => {
refs.push(ref);
});
qs.refs = refs;
}
responseData = await sentryIoApiRequest.call(this, 'POST', endpoint, qs);
}
}
if (resource === 'team') {
if (operation === 'get') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const teamSlug = this.getNodeParameter('teamSlug', i) as string;
const endpoint = `/api/0/teams/${organizationSlug}/${teamSlug}/`;
responseData = await sentryIoApiRequest.call(this, 'GET', endpoint, qs);
}
if (operation === 'getAll') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/teams/`;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
qs.limit = limit;
}
responseData = await sentryApiRequestAllItems.call(this, 'GET', endpoint, {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'create') {
const organizationSlug = this.getNodeParameter('organizationSlug', i) as string;
const name = this.getNodeParameter('name', i) as string;
const endpoint = `/api/0/organizations/${organizationSlug}/teams/`;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
qs.name = name;
if (additionalFields.slug) {
qs.slug = additionalFields.slug;
}
responseData = await sentryIoApiRequest.call(this, 'POST', endpoint, 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,290 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const teamOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'team',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new team',
},
{
name: 'Get',
value: 'get',
description: 'Get team by slug',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all teams',
}
],
default: 'get',
description: 'The operation to perform',
},
] as INodeProperties[];
export const teamFields = [
/* -------------------------------------------------------------------------- */
/* team:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'getAll',
],
},
},
required: true,
description: 'The slug of the organization for which the teams should be listed',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'team',
],
},
},
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: [
'team',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return',
},
/* -------------------------------------------------------------------------- */
/* team:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the organization the team belongs to',
},
{
displayName: 'Team Slug',
name: 'teamSlug',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'get',
],
},
},
required: true,
description: 'The slug of the team to get',
},
/* -------------------------------------------------------------------------- */
/* team:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'create',
],
},
},
required: true,
description: 'The slug of the organization the team belongs to',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'create',
],
},
},
required: true,
description: 'The name of the team',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Slug',
name: 'slug',
type: 'string',
default: '',
description: 'The optional slug for this team. If not provided it will be auto generated from the name',
},
]
},
/* -------------------------------------------------------------------------- */
/* team:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization Slug',
name: 'organizationSlug',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'update', 'delete'
],
},
},
required: true,
description: 'The slug of the organization the team belongs to',
},
{
displayName: 'Team Slug',
name: 'teamSlug',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'update', 'delete'
],
},
},
required: true,
description: 'The slug of the team to get',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'team',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Slug',
name: 'slug',
type: 'string',
default: '',
description: 'The new slug of the team. Must be unique and available',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The new name of the team',
},
]
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -2,12 +2,16 @@ import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import { OptionsWithUri } from 'request';
import { IDataObject } from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
// Interface in n8n
export interface IMarkupKeyboard {
@ -138,7 +142,7 @@ export function addAdditionalFields(this: IExecuteFunctions, body: IDataObject,
* @param {object} body
* @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
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, endpoint: string, body: object, query?: IDataObject, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('telegramApi');
if (credentials === undefined) {
@ -157,9 +161,22 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
json: true,
};
if (Object.keys(option).length > 0) {
Object.assign(options, option);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(query).length === 0) {
delete options.qs;
}
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Telegram credentials are not valid!');
@ -175,3 +192,16 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
throw error;
}
}
export function getImageBySize(photos: IDataObject[], size: string): IDataObject | undefined {
const sizes = {
'small': 0,
'medium': 1,
'large': 2,
} as IDataObject;
const index = sizes[size] as number;
return photos[index];
}

View file

@ -0,0 +1,12 @@
export interface IEvent {
message?: {
photo?: [
{
file_id: string,
},
],
document?: {
file_id: string;
},
};
}

View file

@ -146,6 +146,11 @@ export class Telegram implements INodeType {
value: 'editMessageText',
description: 'Edit a text message',
},
{
name: 'Send Animation',
value: 'sendAnimation',
description: 'Send an animated file',
},
{
name: 'Send Audio',
value: 'sendAudio',
@ -209,6 +214,7 @@ export class Telegram implements INodeType {
'member',
'setDescription',
'setTitle',
'sendAnimation',
'sendAudio',
'sendChatAction',
'sendDocument',
@ -513,6 +519,29 @@ export class Telegram implements INodeType {
// ----------------------------------
// message:sendAnimation
// ----------------------------------
{
displayName: 'Animation',
name: 'file',
type: 'string',
default: '',
displayOptions: {
show: {
operation: [
'sendAnimation'
],
resource: [
'message',
],
},
},
description: 'Animation to send. Pass a file_id to send an animation that exists on the Telegram servers (recommended)<br />or pass an HTTP URL for Telegram to get an animation from the Internet.',
},
// ----------------------------------
// message:sendAudio
// ----------------------------------
@ -811,7 +840,7 @@ export class Telegram implements INodeType {
// ----------------------------------
// message:editMessageText/sendAudio/sendMessage/sendPhoto/sendSticker/sendVideo
// message:editMessageText/sendAnimation/sendAudio/sendMessage/sendPhoto/sendSticker/sendVideo
// ----------------------------------
{
@ -820,6 +849,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
operation: [
'sendAnimation',
'sendDocument',
'sendMessage',
'sendPhoto',
@ -1147,6 +1177,7 @@ export class Telegram implements INodeType {
show: {
operation: [
'editMessageText',
'sendAnimation',
'sendDocument',
'sendMessage',
'sendMediaGroup',
@ -1171,6 +1202,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
'/operation': [
'sendAnimation',
'sendAudio',
'sendDocument',
'sendPhoto',
@ -1220,6 +1252,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
'/operation': [
'sendAnimation',
'sendAudio',
'sendVideo',
],
@ -1238,6 +1271,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
'/operation': [
'sendAnimation',
'sendVideo',
],
},
@ -1263,6 +1297,7 @@ export class Telegram implements INodeType {
show: {
'/operation': [
'editMessageText',
'sendAnimation',
'sendAudio',
'sendMessage',
'sendPhoto',
@ -1325,6 +1360,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
'/operation': [
'sendAnimation',
'sendAudio',
'sendDocument',
'sendVideo',
@ -1344,6 +1380,7 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
'/operation': [
'sendAnimation',
'sendVideo',
],
},
@ -1469,6 +1506,21 @@ export class Telegram implements INodeType {
// Add additional fields and replyMarkup
addAdditionalFields.call(this, body, i);
} else if (operation === 'sendAnimation') {
// ----------------------------------
// message:sendAnimation
// ----------------------------------
endpoint = 'sendAnimation';
body.chat_id = this.getNodeParameter('chatId', i) as string;
body.animation = this.getNodeParameter('file', i) as string;
// Add additional fields and replyMarkup
addAdditionalFields.call(this, body, i);
} else if (operation === 'sendAudio') {
// ----------------------------------
// message:sendAudio

View file

@ -4,15 +4,20 @@ import {
} from 'n8n-core';
import {
INodeTypeDescription,
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import {
apiRequest,
getImageBySize,
} from './GenericFunctions';
import {
IEvent,
} from './IEvent';
export class TelegramTrigger implements INodeType {
description: INodeTypeDescription = {
@ -33,7 +38,7 @@ export class TelegramTrigger implements INodeType {
{
name: 'telegramApi',
required: true,
}
},
],
webhooks: [
{
@ -105,6 +110,52 @@ export class TelegramTrigger implements INodeType {
default: [],
description: 'The update types to listen to.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Download Images/Files',
name: 'download',
type: 'boolean',
default: false,
description: `Telegram develiers the image in 3 sizes.<br>
By default, just the larger image would be downloaded.<br>
if you want to change the size set the field 'Image Size'`,
},
{
displayName: 'Image Size',
name: 'imageSize',
type: 'options',
displayOptions: {
show: {
download: [
true,
],
},
},
options: [
{
name: 'Small',
value: 'small',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'Large',
value: 'large',
},
],
default: 'large',
description: 'The size of the image to be downloaded',
},
],
},
],
};
@ -112,6 +163,14 @@ export class TelegramTrigger implements INodeType {
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const endpoint = 'getWebhookInfo';
const webhookReturnData = await apiRequest.call(this, 'POST', endpoint, {});
const webhookUrl = this.getNodeWebhookUrl('default');
if (webhookReturnData.result.url === webhookUrl) {
return true;
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
@ -149,14 +208,74 @@ export class TelegramTrigger implements INodeType {
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
const credentials = this.getCredentials('telegramApi') as IDataObject;
const bodyData = this.getBodyData() as IEvent;
const additionalFields = this.getNodeParameter('additionalFields') as IDataObject;
if (additionalFields.download === true) {
let imageSize = 'large';
if ((bodyData.message && bodyData.message.photo && Array.isArray(bodyData.message.photo) || bodyData.message?.document)) {
if (additionalFields.imageSize) {
imageSize = additionalFields.imageSize as string;
}
let fileId;
if (bodyData.message.photo) {
let image = getImageBySize(bodyData.message.photo as IDataObject[], imageSize) as IDataObject;
// When the image is sent from the desktop app telegram does not resize the image
// So return the only image avaiable
// Basically the Image Size parameter would work just when the images comes from the mobile app
if (image === undefined) {
image = bodyData.message.photo[0];
}
fileId = image.file_id;
} else {
fileId = bodyData.message?.document?.file_id;
}
const { result: { file_path } } = await apiRequest.call(this, 'GET', `getFile?file_id=${fileId}`, {});
const file = await apiRequest.call(this, 'GET', '', {}, {}, { json: false, encoding: null, uri: `https://api.telegram.org/file/bot${credentials.accessToken}/${file_path}`, resolveWithFullResponse: true });
const data = Buffer.from(file.body as string);
const fileName = file_path.split('/').pop();
const binaryData = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName);
return {
workflowData: [
[
{
json: bodyData as unknown as IDataObject,
binary: {
data: binaryData,
},
}
]
],
};
}
}
return {
workflowData: [
this.helpers.returnJsonArray([bodyData])
this.helpers.returnJsonArray([bodyData as unknown as IDataObject])
],
};
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.74.1",
"version": "0.75.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -52,6 +52,7 @@
"dist/credentials/CopperApi.credentials.js",
"dist/credentials/CalendlyApi.credentials.js",
"dist/credentials/CustomerIoApi.credentials.js",
"dist/credentials/S3.credentials.js",
"dist/credentials/CrateDb.credentials.js",
"dist/credentials/DisqusApi.credentials.js",
"dist/credentials/DriftApi.credentials.js",
@ -115,6 +116,7 @@
"dist/credentials/MoceanApi.credentials.js",
"dist/credentials/MondayComApi.credentials.js",
"dist/credentials/MongoDb.credentials.js",
"dist/credentials/Mqtt.credentials.js",
"dist/credentials/Msg91Api.credentials.js",
"dist/credentials/MySql.credentials.js",
"dist/credentials/NextCloudApi.credentials.js",
@ -137,6 +139,8 @@
"dist/credentials/RundeckApi.credentials.js",
"dist/credentials/ShopifyApi.credentials.js",
"dist/credentials/SalesforceOAuth2Api.credentials.js",
"dist/credentials/SentryIoApi.credentials.js",
"dist/credentials/SentryIoOAuth2Api.credentials.js",
"dist/credentials/SlackApi.credentials.js",
"dist/credentials/SlackOAuth2Api.credentials.js",
"dist/credentials/Sms77Api.credentials.js",
@ -215,6 +219,7 @@
"dist/nodes/CrateDb/CrateDb.node.js",
"dist/nodes/Cron.node.js",
"dist/nodes/Crypto.node.js",
"dist/nodes/CustomerIo/CustomerIo.node.js",
"dist/nodes/CustomerIo/CustomerIoTrigger.node.js",
"dist/nodes/DateTime.node.js",
"dist/nodes/Discord/Discord.node.js",
@ -288,6 +293,7 @@
"dist/nodes/Mocean/Mocean.node.js",
"dist/nodes/MondayCom/MondayCom.node.js",
"dist/nodes/MongoDb/MongoDb.node.js",
"dist/nodes/MQTT/MqttTrigger.node.js",
"dist/nodes/MoveBinaryData.node.js",
"dist/nodes/Msg91/Msg91.node.js",
"dist/nodes/MySql/MySql.node.js",
@ -312,8 +318,10 @@
"dist/nodes/Rocketchat/Rocketchat.node.js",
"dist/nodes/RssFeedRead.node.js",
"dist/nodes/Rundeck/Rundeck.node.js",
"dist/nodes/S3/S3.node.js",
"dist/nodes/Salesforce/Salesforce.node.js",
"dist/nodes/Set.node.js",
"dist/nodes/SentryIo/SentryIo.node.js",
"dist/nodes/Shopify/Shopify.node.js",
"dist/nodes/Shopify/ShopifyTrigger.node.js",
"dist/nodes/Signl4/Signl4.node.js",
@ -373,6 +381,7 @@
"@types/mailparser": "^2.7.3",
"@types/moment-timezone": "^0.5.12",
"@types/mongodb": "^3.5.4",
"@types/mqtt": "^2.5.0",
"@types/mssql": "^6.0.2",
"@types/node": "^14.0.27",
"@types/nodemailer": "^6.4.0",
@ -409,9 +418,10 @@
"moment": "2.24.0",
"moment-timezone": "^0.5.28",
"mongodb": "^3.5.5",
"mqtt": "^4.2.0",
"mssql": "^6.2.0",
"mysql2": "^2.0.1",
"n8n-core": "~0.43.0",
"n8n-core": "~0.44.0",
"nodemailer": "^6.4.6",
"pdf-parse": "^1.1.1",
"pg": "^8.3.0",