Merge branch 'Master' into 'Pipedrive-OAuth2-support'
@@ -52,10 +52,10 @@ export class AffinityTrigger implements INodeType {
|
||||
options: [
|
||||
{
|
||||
name: 'file.created',
|
||||
value: 'file.deleted',
|
||||
value: 'file.created',
|
||||
},
|
||||
{
|
||||
name: 'file.created',
|
||||
name: 'file.deleted',
|
||||
value: 'file.deleted',
|
||||
},
|
||||
{
|
||||
@@ -136,7 +136,7 @@ export class AffinityTrigger implements INodeType {
|
||||
},
|
||||
{
|
||||
name: 'opportunity.deleted',
|
||||
value: 'organization.deleted',
|
||||
value: 'opportunity.deleted',
|
||||
},
|
||||
{
|
||||
name: 'person.created',
|
||||
|
||||
140
packages/nodes-base/nodes/CircleCi/CircleCi.node.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
pipelineFields,
|
||||
pipelineOperations,
|
||||
} from './PipelineDescription';
|
||||
|
||||
import {
|
||||
circleciApiRequest,
|
||||
circleciApiRequestAllItems,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class CircleCi implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'CircleCI',
|
||||
name: 'circleCi',
|
||||
icon: 'file:circleCi.png',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume CircleCI API',
|
||||
defaults: {
|
||||
name: 'CircleCI',
|
||||
color: '#04AA51',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'circleCiApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: ' Pipeline',
|
||||
value: 'pipeline',
|
||||
},
|
||||
],
|
||||
default: 'pipeline',
|
||||
description: 'Resource to consume.',
|
||||
},
|
||||
...pipelineOperations,
|
||||
...pipelineFields,
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = items.length as unknown as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (resource === 'pipeline') {
|
||||
if (operation === 'get') {
|
||||
const vcs = this.getNodeParameter('vcs', i) as string;
|
||||
let slug = this.getNodeParameter('projectSlug', i) as string;
|
||||
const pipelineNumber = this.getNodeParameter('pipelineNumber', i) as number;
|
||||
|
||||
slug = slug.replace(new RegExp(/\//g), '%2F');
|
||||
|
||||
const endpoint = `/project/${vcs}/${slug}/pipeline/${pipelineNumber}`;
|
||||
|
||||
responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const vcs = this.getNodeParameter('vcs', i) as string;
|
||||
const filters = this.getNodeParameter('filters', i) as IDataObject;
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
let slug = this.getNodeParameter('projectSlug', i) as string;
|
||||
|
||||
slug = slug.replace(new RegExp(/\//g), '%2F');
|
||||
|
||||
if (filters.branch) {
|
||||
qs.branch = filters.branch;
|
||||
}
|
||||
|
||||
const endpoint = `/project/${vcs}/${slug}/pipeline`;
|
||||
|
||||
if (returnAll === true) {
|
||||
responseData = await circleciApiRequestAllItems.call(this, 'items', 'GET', endpoint, {}, qs);
|
||||
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
responseData = await circleciApiRequest.call(this, 'GET', endpoint, {}, qs);
|
||||
responseData = responseData.items;
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'trigger') {
|
||||
const vcs = this.getNodeParameter('vcs', i) as string;
|
||||
let slug = this.getNodeParameter('projectSlug', i) as string;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
slug = slug.replace(new RegExp(/\//g), '%2F');
|
||||
|
||||
const endpoint = `/project/${vcs}/${slug}/pipeline`;
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
if (additionalFields.branch) {
|
||||
body.branch = additionalFields.branch as string;
|
||||
}
|
||||
|
||||
if (additionalFields.tag) {
|
||||
body.tag = additionalFields.tag as string;
|
||||
}
|
||||
|
||||
responseData = await circleciApiRequest.call(this, 'POST', endpoint, body, qs);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
67
packages/nodes-base/nodes/CircleCi/GenericFunctions.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function circleciApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('circleCiApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Circle-Token': credentials.apiKey,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
uri: uri ||`https://circleci.com/api/v2${resource}`,
|
||||
json: true
|
||||
};
|
||||
options = Object.assign({}, options, option);
|
||||
if (Object.keys(options.body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.body && err.response.body.message) {
|
||||
// Try to return the error prettier
|
||||
throw new Error(`CircleCI error response [${err.statusCode}]: ${err.response.body.message}`);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw err; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an API request to paginated CircleCI endpoint
|
||||
* and return all results
|
||||
*/
|
||||
export async function circleciApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
|
||||
do {
|
||||
responseData = await circleciApiRequest.call(this, method, resource, body, query);
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
query['page-token'] = responseData.next_page_token;
|
||||
} while (
|
||||
responseData.next_page_token !== undefined &&
|
||||
responseData.next_page_token !== null
|
||||
);
|
||||
return returnData;
|
||||
}
|
||||
229
packages/nodes-base/nodes/CircleCi/PipelineDescription.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const pipelineOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a pipeline',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all pipelines',
|
||||
},
|
||||
{
|
||||
name: 'Trigger',
|
||||
value: 'trigger',
|
||||
description: 'Trigger a pipeline',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const pipelineFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* pipeline:shared */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Provider',
|
||||
name: 'vcs',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Bitbucket',
|
||||
value: 'bitbucket',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
value: 'github',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
'getAll',
|
||||
'trigger',
|
||||
],
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Version control system',
|
||||
},
|
||||
{
|
||||
displayName: 'Project Slug',
|
||||
name: 'projectSlug',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
'getAll',
|
||||
'trigger',
|
||||
],
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'n8n-io/n8n',
|
||||
description: 'Project slug in the form org-name/repo-name',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* pipeline:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Pipeline Number',
|
||||
name: 'pipelineNumber',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: 1,
|
||||
description: 'The number of the pipeline',
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* pipeline:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Branch',
|
||||
name: 'branch',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of a vcs branch.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* pipeline:trigger */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'pipeline',
|
||||
],
|
||||
operation: [
|
||||
'trigger',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Branch',
|
||||
name: 'branch',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The branch where the pipeline ran.<br/>
|
||||
The HEAD commit on this branch was used for the pipeline.<br/>
|
||||
Note that branch and tag are mutually exclusive.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Tag',
|
||||
name: 'tag',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The tag used by the pipeline.<br/>
|
||||
The commit that this tag points to was used for the pipeline.<br/>
|
||||
Note that branch and tag are mutually exclusive`,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as INodeProperties[];
|
||||
BIN
packages/nodes-base/nodes/CircleCi/circleCi.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
246
packages/nodes-base/nodes/CrateDb/CrateDb.node.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as pgPromise from 'pg-promise';
|
||||
|
||||
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/Postgres.node.functions';
|
||||
|
||||
export class CrateDb implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'CrateDB',
|
||||
name: 'crateDb',
|
||||
icon: 'file:cratedb.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Gets, add and update data in CrateDB.',
|
||||
defaults: {
|
||||
name: 'CrateDB',
|
||||
color: '#47889f',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'crateDb',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Execute Query',
|
||||
value: 'executeQuery',
|
||||
description: 'Executes a SQL query.',
|
||||
},
|
||||
{
|
||||
name: 'Insert',
|
||||
value: 'insert',
|
||||
description: 'Insert rows in database.',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Updates rows in database.',
|
||||
},
|
||||
],
|
||||
default: 'insert',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['executeQuery'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'SELECT id, name FROM product WHERE id < 40',
|
||||
required: true,
|
||||
description: 'The SQL query to execute.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Schema',
|
||||
name: 'schema',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: 'public',
|
||||
required: true,
|
||||
description: 'Name of the schema the table belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to insert data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'id,name,description',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for the new rows.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Fields',
|
||||
name: 'returnFields',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '*',
|
||||
description: 'Comma separated list of the fields that the operation will return',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to update data in',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Key',
|
||||
name: 'updateKey',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
required: true,
|
||||
description:
|
||||
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'name,description',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for rows to update.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = this.getCredentials('crateDb');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const pgp = pgPromise();
|
||||
|
||||
const config = {
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
database: credentials.database as string,
|
||||
user: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
|
||||
sslmode: (credentials.ssl as string) || 'disable',
|
||||
};
|
||||
|
||||
const db = pgp(config);
|
||||
|
||||
let returnItems = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
if (operation === 'executeQuery') {
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
|
||||
} else if (operation === 'insert') {
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
|
||||
const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
// Add the id to the data
|
||||
for (let i = 0; i < insertData.length; i++) {
|
||||
returnItems.push({
|
||||
json: {
|
||||
...insertData[i],
|
||||
...insertItems[i],
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
|
||||
const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(updateItems);
|
||||
} else {
|
||||
await pgp.end();
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
await pgp.end();
|
||||
|
||||
return this.prepareOutputData(returnItems);
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/CrateDb/cratedb.png
Normal file
|
After Width: | Height: | Size: 475 B |
@@ -243,9 +243,10 @@ export class DateTime implements INodeType {
|
||||
if (currentDate === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!moment(currentDate as string | number).isValid()) {
|
||||
if (options.fromFormat === undefined && !moment(currentDate as string | number).isValid()) {
|
||||
throw new Error('The date input format could not be recognized. Please set the "From Format" field');
|
||||
}
|
||||
|
||||
if (Number.isInteger(currentDate as unknown as number)) {
|
||||
newDate = moment.unix(currentDate as unknown as number);
|
||||
} else {
|
||||
|
||||
@@ -37,9 +37,44 @@ export class Drift implements INodeType {
|
||||
{
|
||||
name: 'driftApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'driftOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
|
||||
@@ -12,25 +12,15 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const credentials = this.getCredentials('driftApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const endpoint = 'https://driftapi.com';
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.accessToken}`,
|
||||
},
|
||||
headers: {},
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
uri: uri || `${endpoint}${resource}`,
|
||||
uri: uri || `https://driftapi.com${resource}`,
|
||||
json: true
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.form;
|
||||
}
|
||||
@@ -38,11 +28,27 @@ export async function driftApiRequest(this: IExecuteFunctions | IWebhookFunction
|
||||
delete options.qs;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
const credentials = this.getCredentials('driftApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers!['Authorization'] = `Bearer ${credentials.accessToken}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2!.call(this, 'driftOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
const errorMessage = error.message || (error.response.body && error.response.body.message );
|
||||
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
const errorMessage = error.response.body.error.message;
|
||||
throw new Error(`Drift error response [${error.statusCode}]: ${errorMessage}`);
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
@@ -9,8 +10,9 @@ import {
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
import {
|
||||
OptionsWithUri
|
||||
} from 'request';
|
||||
|
||||
export class Dropbox implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -23,7 +25,7 @@ export class Dropbox implements INodeType {
|
||||
description: 'Access data on Dropbox',
|
||||
defaults: {
|
||||
name: 'Dropbox',
|
||||
color: '#22BB44',
|
||||
color: '#0061FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
@@ -454,6 +456,7 @@ export class Dropbox implements INodeType {
|
||||
let requestMethod = '';
|
||||
let body: IDataObject | Buffer;
|
||||
let isJson = false;
|
||||
let query: IDataObject = {};
|
||||
|
||||
let headers: IDataObject;
|
||||
|
||||
@@ -470,8 +473,9 @@ export class Dropbox implements INodeType {
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
headers['Dropbox-API-Arg'] = JSON.stringify({
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
|
||||
query.arg = JSON.stringify({
|
||||
path: this.getNodeParameter('path', i) as string
|
||||
});
|
||||
|
||||
endpoint = 'https://content.dropboxapi.com/2/files/download';
|
||||
@@ -483,9 +487,10 @@ export class Dropbox implements INodeType {
|
||||
|
||||
requestMethod = 'POST';
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
headers['Dropbox-API-Arg'] = JSON.stringify({
|
||||
|
||||
query.arg = JSON.stringify({
|
||||
mode: 'overwrite',
|
||||
path: this.getNodeParameter('path', i) as string,
|
||||
path: this.getNodeParameter('path', i) as string
|
||||
});
|
||||
|
||||
endpoint = 'https://content.dropboxapi.com/2/files/upload';
|
||||
@@ -594,8 +599,8 @@ export class Dropbox implements INodeType {
|
||||
const options: OptionsWithUri = {
|
||||
headers,
|
||||
method: requestMethod,
|
||||
qs: {},
|
||||
uri: endpoint,
|
||||
qs: query,
|
||||
json: isJson,
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,25 @@ export class EventbriteTrigger implements INodeType {
|
||||
{
|
||||
name: 'eventbriteApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'privateKey',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'eventbriteOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -46,6 +64,23 @@ export class EventbriteTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Private Key',
|
||||
value: 'privateKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'privateKey',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Organization',
|
||||
name: 'organization',
|
||||
@@ -149,7 +184,6 @@ export class EventbriteTrigger implements INodeType {
|
||||
description: 'By default does the webhook-data only contain the URL to receive<br />the object data manually. If this option gets activated it<br />will resolve the data automatically.',
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
methods = {
|
||||
@@ -192,23 +226,39 @@ export class EventbriteTrigger implements INodeType {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
if (webhookData.webhookId === undefined) {
|
||||
return false;
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const organisation = this.getNodeParameter('organization') as string;
|
||||
const actions = this.getNodeParameter('actions') as string[];
|
||||
|
||||
const endpoint = `/organizations/${organisation}/webhooks/`;
|
||||
|
||||
const { webhooks } = await eventbriteApiRequest.call(this, 'GET', endpoint);
|
||||
|
||||
const check = (currentActions: string[], webhookActions: string[]) => {
|
||||
for (const currentAction of currentActions) {
|
||||
if (!webhookActions.includes(currentAction)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
if (webhook.endpoint_url === webhookUrl && check(actions, webhook.actions)) {
|
||||
webhookData.webhookId = webhook.id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const endpoint = `/webhooks/${webhookData.webhookId}/`;
|
||||
try {
|
||||
await eventbriteApiRequest.call(this, 'GET', endpoint);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const organisation = this.getNodeParameter('organization') as string;
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const actions = this.getNodeParameter('actions') as string[];
|
||||
const endpoint = `/webhooks/`;
|
||||
const endpoint = `/organizations/${organisation}/webhooks/`;
|
||||
const body: IDataObject = {
|
||||
endpoint_url: webhookUrl,
|
||||
actions: actions.join(','),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { OptionsWithUri } from 'request';
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
@@ -6,16 +9,14 @@ import {
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('eventbriteApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: { 'Authorization': `Bearer ${credentials.apiKey}`},
|
||||
headers: {},
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
@@ -27,14 +28,26 @@ export async function eventbriteApiRequest(this: IHookFunctions | IExecuteFuncti
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'privateKey') {
|
||||
const credentials = this.getCredentials('eventbriteApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2!.call(this, 'eventbriteOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
let errorMessage = error.message;
|
||||
if (error.response.body && error.response.body.error_description) {
|
||||
errorMessage = error.response.body.error_description;
|
||||
}
|
||||
|
||||
throw new Error('Eventbrite Error: ' + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,13 @@ export class FacebookGraphApi implements INodeType {
|
||||
placeholder: 'videos',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore SSL Issues',
|
||||
name: 'allowUnauthorizedCerts',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Still download the response even if SSL certificate validation is not possible. (Not recommended)',
|
||||
},
|
||||
{
|
||||
displayName: 'Send Binary Data',
|
||||
name: 'sendBinaryData',
|
||||
@@ -301,6 +308,7 @@ export class FacebookGraphApi implements INodeType {
|
||||
qs: {
|
||||
access_token: graphApiCredentials!.accessToken,
|
||||
},
|
||||
rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean,
|
||||
};
|
||||
|
||||
if (options !== undefined) {
|
||||
|
||||
@@ -17,14 +17,14 @@ import {
|
||||
|
||||
export class Github implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Github',
|
||||
displayName: 'GitHub',
|
||||
name: 'github',
|
||||
icon: 'file:github.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Retrieve data from Github API.',
|
||||
description: 'Retrieve data from GitHub API.',
|
||||
defaults: {
|
||||
name: 'Github',
|
||||
name: 'GitHub',
|
||||
color: '#665533',
|
||||
},
|
||||
inputs: ['main'],
|
||||
@@ -178,7 +178,7 @@ export class Github implements INodeType {
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get the data of a single issues',
|
||||
description: 'Get the data of a single issue',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
@@ -220,7 +220,7 @@ export class Github implements INodeType {
|
||||
{
|
||||
name: 'List Popular Paths',
|
||||
value: 'listPopularPaths',
|
||||
description: 'Get the data of a file in repositoryGet the top 10 popular content paths over the last 14 days.',
|
||||
description: 'Get the top 10 popular content paths over the last 14 days.',
|
||||
},
|
||||
{
|
||||
name: 'List Referrers',
|
||||
@@ -244,11 +244,6 @@ export class Github implements INodeType {
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get Emails',
|
||||
value: 'getEmails',
|
||||
description: 'Returns the email addresses of a user',
|
||||
},
|
||||
{
|
||||
name: 'Get Repositories',
|
||||
value: 'getRepositories',
|
||||
@@ -463,7 +458,7 @@ export class Github implements INodeType {
|
||||
description: 'The name of the author of the commit.',
|
||||
},
|
||||
{
|
||||
displayName: 'EMail',
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
@@ -496,7 +491,7 @@ export class Github implements INodeType {
|
||||
description: 'The name of the committer of the commit.',
|
||||
},
|
||||
{
|
||||
displayName: 'EMail',
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
@@ -1019,28 +1014,28 @@ export class Github implements INodeType {
|
||||
name: 'assignee',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Return only issuse which are assigned to a specific user.',
|
||||
description: 'Return only issues which are assigned to a specific user.',
|
||||
},
|
||||
{
|
||||
displayName: 'Creator',
|
||||
name: 'creator',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Return only issuse which were created by a specific user.',
|
||||
description: 'Return only issues which were created by a specific user.',
|
||||
},
|
||||
{
|
||||
displayName: 'Mentioned',
|
||||
name: 'mentioned',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Return only issuse in which a specific user was mentioned.',
|
||||
description: 'Return only issues in which a specific user was mentioned.',
|
||||
},
|
||||
{
|
||||
displayName: 'Labels',
|
||||
name: 'labels',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Return only issuse with the given labels. Multiple lables can be separated by comma.',
|
||||
description: 'Return only issues with the given labels. Multiple lables can be separated by comma.',
|
||||
},
|
||||
{
|
||||
displayName: 'Updated Since',
|
||||
|
||||
@@ -34,7 +34,25 @@ export class GithubTrigger implements INodeType {
|
||||
{
|
||||
name: 'githubApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'githubOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -45,6 +63,23 @@ export class GithubTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Repository Owner',
|
||||
name: 'owner',
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
/**
|
||||
* Make an API request to Gitlab
|
||||
@@ -17,24 +19,43 @@ import {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('gitlabApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options : OptionsWithUri = {
|
||||
method,
|
||||
headers: {
|
||||
'Private-Token': `${credentials.accessToken}`,
|
||||
},
|
||||
headers: {},
|
||||
body,
|
||||
qs: query,
|
||||
uri: `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`,
|
||||
uri: '',
|
||||
json: true
|
||||
};
|
||||
|
||||
if (query === undefined) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
const credentials = this.getCredentials('gitlabApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers!['Private-Token'] = `${credentials.accessToken}`;
|
||||
|
||||
options.uri = `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`;
|
||||
|
||||
return await this.helpers.request(options);
|
||||
} else {
|
||||
const credentials = this.getCredentials('gitlabOAuth2Api');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.uri = `${(credentials.server as string).replace(/\/$/, '')}/api/v4${endpoint}`;
|
||||
|
||||
return await this.helpers.requestOAuth2!.call(this, 'gitlabOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
gitlabApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
|
||||
export class Gitlab implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Gitlab',
|
||||
@@ -33,9 +32,44 @@ export class Gitlab implements INodeType {
|
||||
{
|
||||
name: 'gitlabApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitlabOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
@@ -793,10 +827,26 @@ export class Gitlab implements INodeType {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('gitlabApi');
|
||||
let credentials;
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
try {
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
credentials = this.getCredentials('gitlabApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
} else {
|
||||
credentials = this.getCredentials('gitlabOAuth2Api');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
// Operations which overwrite the returned data
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
gitlabApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
|
||||
export class GitlabTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Gitlab Trigger',
|
||||
@@ -34,7 +33,25 @@ export class GitlabTrigger implements INodeType {
|
||||
{
|
||||
name: 'gitlabApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gitlabOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -45,6 +62,23 @@ export class GitlabTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Repository Owner',
|
||||
name: 'owner',
|
||||
@@ -135,7 +169,10 @@ export class GitlabTrigger implements INodeType {
|
||||
// Webhook got created before so check if it still exists
|
||||
const owner = this.getNodeParameter('owner') as string;
|
||||
const repository = this.getNodeParameter('repository') as string;
|
||||
const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`;
|
||||
|
||||
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
|
||||
|
||||
const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await gitlabApiRequest.call(this, 'GET', endpoint, {});
|
||||
@@ -175,15 +212,22 @@ export class GitlabTrigger implements INodeType {
|
||||
events[`${e}_events`] = true;
|
||||
}
|
||||
|
||||
const endpoint = `/projects/${owner}%2F${repository}/hooks`;
|
||||
// gitlab set the push_events to true when the field it's not sent.
|
||||
// set it to false when it's not picked by the user.
|
||||
if (events['push_events'] === undefined) {
|
||||
events['push_events'] = false;
|
||||
}
|
||||
|
||||
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
|
||||
|
||||
const endpoint = `/projects/${path}/hooks`;
|
||||
|
||||
const body = {
|
||||
url: webhookUrl,
|
||||
events,
|
||||
...events,
|
||||
enable_ssl_verification: false,
|
||||
};
|
||||
|
||||
|
||||
let responseData;
|
||||
try {
|
||||
responseData = await gitlabApiRequest.call(this, 'POST', endpoint, body);
|
||||
@@ -208,7 +252,10 @@ export class GitlabTrigger implements INodeType {
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const owner = this.getNodeParameter('owner') as string;
|
||||
const repository = this.getNodeParameter('repository') as string;
|
||||
const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`;
|
||||
|
||||
const path = (`${owner}/${repository}`).replace(/\//g,'%2F');
|
||||
|
||||
const endpoint = `/projects/${path}/hooks/${webhookData.webhookId}`;
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { INodeProperties } from "n8n-workflow";
|
||||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const eventOperations = [
|
||||
{
|
||||
@@ -37,37 +39,36 @@ export const eventOperations = [
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update an event',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
description: 'The operation to perform.'
|
||||
}
|
||||
] as INodeProperties[];
|
||||
|
||||
export const eventFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Calendar',
|
||||
name: 'calendar',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCalendars',
|
||||
loadOptionsMethod: 'getCalendars'
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
'create'
|
||||
],
|
||||
resource: [
|
||||
'event',
|
||||
'event'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
displayName: 'Start',
|
||||
@@ -85,7 +86,7 @@ export const eventFields = [
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Start time of the event.',
|
||||
description: 'Start time of the event.'
|
||||
},
|
||||
{
|
||||
displayName: 'End',
|
||||
@@ -103,7 +104,7 @@ export const eventFields = [
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'End time of the event.',
|
||||
description: 'End time of the event.'
|
||||
},
|
||||
{
|
||||
displayName: 'Use Default Reminders',
|
||||
@@ -119,7 +120,7 @@ export const eventFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
default: true
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
@@ -153,7 +154,7 @@ export const eventFields = [
|
||||
},
|
||||
],
|
||||
default: 'no',
|
||||
description: 'Wheater the event is all day or not',
|
||||
description: 'Wheater the event is all day or not'
|
||||
},
|
||||
{
|
||||
displayName: 'Attendees',
|
||||
@@ -176,6 +177,15 @@ export const eventFields = [
|
||||
default: '',
|
||||
description: 'The color of the event.',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Guests Can Invite Others',
|
||||
name: 'guestsCanInviteOthers',
|
||||
@@ -239,7 +249,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'Yearly',
|
||||
value: 'yearly',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
@@ -254,9 +264,9 @@ export const eventFields = [
|
||||
name: 'repeatHowManyTimes',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
minValue: 1
|
||||
},
|
||||
default: 1,
|
||||
default: 1
|
||||
},
|
||||
{
|
||||
displayName: 'Send Updates',
|
||||
@@ -266,7 +276,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'All',
|
||||
value: 'all',
|
||||
description: ' Notifications are sent to all guests',
|
||||
description: 'Notifications are sent to all guests'
|
||||
},
|
||||
{
|
||||
name: 'External Only',
|
||||
@@ -276,8 +286,8 @@ export const eventFields = [
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
description: ' No notifications are sent. This value should only be used for migration use case',
|
||||
},
|
||||
description: 'No notifications are sent. This value should only be used for migration use case',
|
||||
}
|
||||
],
|
||||
description: 'Whether to send notifications about the creation of the new event',
|
||||
default: '',
|
||||
@@ -303,7 +313,7 @@ export const eventFields = [
|
||||
name: 'Busy',
|
||||
value: 'opaque',
|
||||
description: ' The event does block time on the calendar.',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: 'opaque',
|
||||
description: 'Whether the event blocks time on the calendar',
|
||||
@@ -316,7 +326,7 @@ export const eventFields = [
|
||||
loadOptionsMethod: 'getTimezones',
|
||||
},
|
||||
default: '',
|
||||
description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.'
|
||||
description: 'The timezone the event will have set. By default events are schedule on timezone set in n8n.',
|
||||
},
|
||||
{
|
||||
displayName: 'Visibility',
|
||||
@@ -331,7 +341,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'Default',
|
||||
value: 'default',
|
||||
description: ' Uses the default visibility for events on the calendar.',
|
||||
description: 'Uses the default visibility for events on the calendar.',
|
||||
},
|
||||
{
|
||||
name: 'Private',
|
||||
@@ -345,7 +355,7 @@ export const eventFields = [
|
||||
},
|
||||
],
|
||||
default: 'default',
|
||||
description: 'Visibility of the event.',
|
||||
description: 'Visibility of the event.'
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -356,7 +366,7 @@ export const eventFields = [
|
||||
default: '',
|
||||
placeholder: 'Add Reminder',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValues: true
|
||||
},
|
||||
required: false,
|
||||
displayOptions: {
|
||||
@@ -404,13 +414,13 @@ export const eventFields = [
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`,
|
||||
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Calendar',
|
||||
name: 'calendar',
|
||||
@@ -429,7 +439,7 @@ export const eventFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
displayName: 'Event ID',
|
||||
@@ -473,7 +483,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'All',
|
||||
value: 'all',
|
||||
description: ' Notifications are sent to all guests',
|
||||
description: 'Notifications are sent to all guests',
|
||||
},
|
||||
{
|
||||
name: 'External Only',
|
||||
@@ -483,17 +493,17 @@ export const eventFields = [
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
description: ' No notifications are sent. This value should only be used for migration use case',
|
||||
},
|
||||
description: 'No notifications are sent. This value should only be used for migration use case',
|
||||
}
|
||||
],
|
||||
description: 'Whether to send notifications about the creation of the new event',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Calendar',
|
||||
name: 'calendar',
|
||||
@@ -512,7 +522,7 @@ export const eventFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
displayName: 'Event ID',
|
||||
@@ -565,12 +575,12 @@ export const eventFields = [
|
||||
},
|
||||
default: '',
|
||||
description: `Time zone used in the response. The default is the time zone of the calendar.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Calendar',
|
||||
name: 'calendar',
|
||||
@@ -589,7 +599,7 @@ export const eventFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
default: ''
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
@@ -678,7 +688,7 @@ export const eventFields = [
|
||||
name: 'Updated',
|
||||
value: 'updated',
|
||||
description: 'Order by last modification time (ascending).',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
description: 'The order of the events returned in the result.',
|
||||
@@ -743,18 +753,18 @@ export const eventFields = [
|
||||
default: '',
|
||||
description: `Lower bound for an event's last modification time (as a RFC3339 timestamp) to filter by.<b/r>
|
||||
When specified, entries deleted since this time will always be included regardless of showDeleted`,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* event:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Calendar',
|
||||
name: 'calendar',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCalendars',
|
||||
loadOptionsMethod: 'getCalendars'
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
@@ -800,7 +810,7 @@ export const eventFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
default: true
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
@@ -831,7 +841,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'No',
|
||||
value: 'no',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: 'no',
|
||||
description: 'Wheater the event is all day or not',
|
||||
@@ -857,6 +867,15 @@ export const eventFields = [
|
||||
default: '',
|
||||
description: 'The color of the event.',
|
||||
},
|
||||
{
|
||||
displayName: 'Description',
|
||||
name: 'description',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'End',
|
||||
name: 'end',
|
||||
@@ -927,7 +946,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'Yearly',
|
||||
value: 'yearly',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
@@ -971,8 +990,8 @@ export const eventFields = [
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
description: ' No notifications are sent. This value should only be used for migration use case',
|
||||
},
|
||||
description: 'No notifications are sent. This value should only be used for migration use case',
|
||||
}
|
||||
],
|
||||
description: 'Whether to send notifications about the creation of the new event',
|
||||
default: '',
|
||||
@@ -1011,7 +1030,7 @@ export const eventFields = [
|
||||
loadOptionsMethod: 'getTimezones',
|
||||
},
|
||||
default: '',
|
||||
description: 'The timezone the event will have set. By default events are schedule on n8n timezone '
|
||||
description: 'The timezone the event will have set. By default events are schedule on n8n timezone',
|
||||
},
|
||||
{
|
||||
displayName: 'Visibility',
|
||||
@@ -1026,7 +1045,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'Default',
|
||||
value: 'default',
|
||||
description: ' Uses the default visibility for events on the calendar.',
|
||||
description: 'Uses the default visibility for events on the calendar.',
|
||||
},
|
||||
{
|
||||
name: 'Public',
|
||||
@@ -1037,7 +1056,7 @@ export const eventFields = [
|
||||
name: 'Private',
|
||||
value: 'private',
|
||||
description: 'The event is private and only event attendees may view event details.',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: 'default',
|
||||
description: 'Visibility of the event.',
|
||||
@@ -1051,7 +1070,7 @@ export const eventFields = [
|
||||
default: '',
|
||||
placeholder: 'Add Reminder',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValues: true
|
||||
},
|
||||
required: false,
|
||||
displayOptions: {
|
||||
@@ -1084,7 +1103,7 @@ export const eventFields = [
|
||||
{
|
||||
name: 'Popup',
|
||||
value: 'popup',
|
||||
},
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
@@ -1099,8 +1118,8 @@ export const eventFields = [
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
description: `If the event doesn't use the default reminders, this lists the reminders specific to the event`,
|
||||
},
|
||||
}
|
||||
] as INodeProperties[];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IDataObject } from "n8n-workflow";
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export interface IReminder {
|
||||
useDefault?: boolean;
|
||||
|
||||
@@ -33,9 +33,15 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options);
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body && error.response.body.message) {
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
|
||||
let errors = error.response.body.error.errors;
|
||||
|
||||
errors = errors.map((e: IDataObject) => e.message);
|
||||
// Try to return the error prettier
|
||||
throw new Error(`Google Calendar error response [${error.statusCode}]: ${error.response.body.message}`);
|
||||
throw new Error(
|
||||
`Google Calendar error response [${error.statusCode}]: ${errors.join('|')}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class GoogleCalendar implements INodeType {
|
||||
{
|
||||
name: 'googleCalendarOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
@@ -60,7 +60,7 @@ export class GoogleCalendar implements INodeType {
|
||||
},
|
||||
],
|
||||
default: 'event',
|
||||
description: 'The resource to operate on.',
|
||||
description: 'The resource to operate on.'
|
||||
},
|
||||
...eventOperations,
|
||||
...eventFields,
|
||||
@@ -71,55 +71,70 @@ export class GoogleCalendar implements INodeType {
|
||||
loadOptions: {
|
||||
// Get all the calendars to display them to user so that he can
|
||||
// select them easily
|
||||
async getCalendars(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
async getCalendars(
|
||||
this: ILoadOptionsFunctions
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const calendars = await googleApiRequestAllItems.call(this, 'items', 'GET', '/calendar/v3/users/me/calendarList');
|
||||
const calendars = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'items',
|
||||
'GET',
|
||||
'/calendar/v3/users/me/calendarList'
|
||||
);
|
||||
for (const calendar of calendars) {
|
||||
const calendarName = calendar.summary;
|
||||
const calendarId = calendar.id;
|
||||
returnData.push({
|
||||
name: calendarName,
|
||||
value: calendarId,
|
||||
value: calendarId
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
// Get all the colors to display them to user so that he can
|
||||
// select them easily
|
||||
async getColors(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
async getColors(
|
||||
this: ILoadOptionsFunctions
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { calendar } = await googleApiRequest.call(this, 'GET', '/calendar/v3/colors');
|
||||
for (const key of Object.keys(calendar)) {
|
||||
const colorName = calendar[key].background;
|
||||
const { event } = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/calendar/v3/colors'
|
||||
);
|
||||
for (const key of Object.keys(event)) {
|
||||
const colorName = `Background: ${event[key].background} - Foreground: ${event[key].foreground}`;
|
||||
const colorId = key;
|
||||
returnData.push({
|
||||
name: `${colorName} - ${colorId}`,
|
||||
value: colorId,
|
||||
name: `${colorName}`,
|
||||
value: colorId
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
// Get all the timezones to display them to user so that he can
|
||||
// select them easily
|
||||
async getTimezones(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
async getTimezones(
|
||||
this: ILoadOptionsFunctions
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
for (const timezone of moment.tz.names()) {
|
||||
const timezoneName = timezone;
|
||||
const timezoneId = timezone;
|
||||
returnData.push({
|
||||
name: timezoneName,
|
||||
value: timezoneId,
|
||||
value: timezoneId
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = items.length as unknown as number;
|
||||
const length = (items.length as unknown) as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
@@ -131,8 +146,14 @@ export class GoogleCalendar implements INodeType {
|
||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||
const start = this.getNodeParameter('start', i) as string;
|
||||
const end = this.getNodeParameter('end', i) as string;
|
||||
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
const useDefaultReminders = this.getNodeParameter(
|
||||
'useDefaultReminders',
|
||||
i
|
||||
) as boolean;
|
||||
const additionalFields = this.getNodeParameter(
|
||||
'additionalFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
if (additionalFields.maxAttendees) {
|
||||
qs.maxAttendees = additionalFields.maxAttendees as number;
|
||||
}
|
||||
@@ -145,17 +166,19 @@ export class GoogleCalendar implements INodeType {
|
||||
const body: IEvent = {
|
||||
start: {
|
||||
dateTime: start,
|
||||
timeZone: additionalFields.timeZone || this.getTimezone(),
|
||||
timeZone: additionalFields.timeZone || this.getTimezone()
|
||||
},
|
||||
end: {
|
||||
dateTime: end,
|
||||
timeZone: additionalFields.timeZone || this.getTimezone(),
|
||||
timeZone: additionalFields.timeZone || this.getTimezone()
|
||||
}
|
||||
};
|
||||
if (additionalFields.attendees) {
|
||||
body.attendees = (additionalFields.attendees as string[]).map(attendee => {
|
||||
return { email: attendee };
|
||||
});
|
||||
body.attendees = (additionalFields.attendees as string[]).map(
|
||||
attendee => {
|
||||
return { email: attendee };
|
||||
}
|
||||
);
|
||||
}
|
||||
if (additionalFields.color) {
|
||||
body.colorId = additionalFields.color as string;
|
||||
@@ -188,9 +211,12 @@ export class GoogleCalendar implements INodeType {
|
||||
body.visibility = additionalFields.visibility as string;
|
||||
}
|
||||
if (!useDefaultReminders) {
|
||||
const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[];
|
||||
const reminders = (this.getNodeParameter(
|
||||
'remindersUi',
|
||||
i
|
||||
) as IDataObject).remindersValues as IDataObject[];
|
||||
body.reminders = {
|
||||
useDefault: false,
|
||||
useDefault: false
|
||||
};
|
||||
if (reminders) {
|
||||
body.reminders.overrides = reminders;
|
||||
@@ -198,32 +224,54 @@ export class GoogleCalendar implements INodeType {
|
||||
}
|
||||
if (additionalFields.allday) {
|
||||
body.start = {
|
||||
date: moment(start).utc().format('YYYY-MM-DD'),
|
||||
date: moment(start)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD')
|
||||
};
|
||||
body.end = {
|
||||
date: moment(end).utc().format('YYYY-MM-DD'),
|
||||
date: moment(end)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD')
|
||||
};
|
||||
}
|
||||
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
||||
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
||||
body.recurrence = [];
|
||||
if (additionalFields.repeatHowManyTimes
|
||||
&& additionalFields.repeatUntil) {
|
||||
throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`);
|
||||
if (
|
||||
additionalFields.repeatHowManyTimes &&
|
||||
additionalFields.repeatUntil
|
||||
) {
|
||||
throw new Error(
|
||||
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`
|
||||
);
|
||||
}
|
||||
if (additionalFields.repeatFrecuency) {
|
||||
body.recurrence?.push(`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`);
|
||||
body.recurrence?.push(
|
||||
`FREQ=${(additionalFields.repeatFrecuency as string).toUpperCase()};`
|
||||
);
|
||||
}
|
||||
if (additionalFields.repeatHowManyTimes) {
|
||||
body.recurrence?.push(`COUNT=${additionalFields.repeatHowManyTimes};`);
|
||||
body.recurrence?.push(
|
||||
`COUNT=${additionalFields.repeatHowManyTimes};`
|
||||
);
|
||||
}
|
||||
if (additionalFields.repeatUntil) {
|
||||
body.recurrence?.push(`UNTIL=${moment(additionalFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`);
|
||||
body.recurrence?.push(
|
||||
`UNTIL=${moment(additionalFields.repeatUntil as string)
|
||||
.utc()
|
||||
.format('YYYYMMDDTHHmmss')}Z`
|
||||
);
|
||||
}
|
||||
if (body.recurrence.length !== 0) {
|
||||
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
||||
}
|
||||
responseData = await googleApiRequest.call(this, 'POST', `/calendar/v3/calendars/${calendarId}/events`, body, qs);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/calendar/v3/calendars/${calendarId}/events`,
|
||||
body,
|
||||
qs
|
||||
);
|
||||
}
|
||||
//https://developers.google.com/calendar/v3/reference/events/delete
|
||||
if (operation === 'delete') {
|
||||
@@ -233,8 +281,13 @@ export class GoogleCalendar implements INodeType {
|
||||
if (options.sendUpdates) {
|
||||
qs.sendUpdates = options.sendUpdates as number;
|
||||
}
|
||||
responseData = await googleApiRequest.call(this, 'DELETE', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {});
|
||||
responseData = { success: true };
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||
{}
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
//https://developers.google.com/calendar/v3/reference/events/get
|
||||
if (operation === 'get') {
|
||||
@@ -247,7 +300,13 @@ export class GoogleCalendar implements INodeType {
|
||||
if (options.timeZone) {
|
||||
qs.timeZone = options.timeZone as string;
|
||||
}
|
||||
responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, {}, qs);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
}
|
||||
//https://developers.google.com/calendar/v3/reference/events/list
|
||||
if (operation === 'getAll') {
|
||||
@@ -288,10 +347,23 @@ export class GoogleCalendar implements INodeType {
|
||||
qs.updatedMin = options.updatedMin as string;
|
||||
}
|
||||
if (returnAll) {
|
||||
responseData = await googleApiRequestAllItems.call(this, 'items', 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs);
|
||||
responseData = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'items',
|
||||
'GET',
|
||||
`/calendar/v3/calendars/${calendarId}/events`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
} else {
|
||||
qs.maxResults = this.getNodeParameter('limit', i) as number;
|
||||
responseData = await googleApiRequest.call(this, 'GET', `/calendar/v3/calendars/${calendarId}/events`, {}, qs);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/calendar/v3/calendars/${calendarId}/events`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
responseData = responseData.items;
|
||||
}
|
||||
}
|
||||
@@ -299,8 +371,14 @@ export class GoogleCalendar implements INodeType {
|
||||
if (operation === 'update') {
|
||||
const calendarId = this.getNodeParameter('calendar', i) as string;
|
||||
const eventId = this.getNodeParameter('eventId', i) as string;
|
||||
const useDefaultReminders = this.getNodeParameter('useDefaultReminders', i) as boolean;
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
|
||||
const useDefaultReminders = this.getNodeParameter(
|
||||
'useDefaultReminders',
|
||||
i
|
||||
) as boolean;
|
||||
const updateFields = this.getNodeParameter(
|
||||
'updateFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
if (updateFields.maxAttendees) {
|
||||
qs.maxAttendees = updateFields.maxAttendees as number;
|
||||
}
|
||||
@@ -314,19 +392,21 @@ export class GoogleCalendar implements INodeType {
|
||||
if (updateFields.start) {
|
||||
body.start = {
|
||||
dateTime: updateFields.start,
|
||||
timeZone: updateFields.timeZone || this.getTimezone(),
|
||||
timeZone: updateFields.timeZone || this.getTimezone()
|
||||
};
|
||||
}
|
||||
if (updateFields.end) {
|
||||
body.end = {
|
||||
dateTime: updateFields.end,
|
||||
timeZone: updateFields.timeZone || this.getTimezone(),
|
||||
timeZone: updateFields.timeZone || this.getTimezone()
|
||||
};
|
||||
}
|
||||
if (updateFields.attendees) {
|
||||
body.attendees = (updateFields.attendees as string[]).map(attendee => {
|
||||
return { email: attendee };
|
||||
});
|
||||
body.attendees = (updateFields.attendees as string[]).map(
|
||||
attendee => {
|
||||
return { email: attendee };
|
||||
}
|
||||
);
|
||||
}
|
||||
if (updateFields.color) {
|
||||
body.colorId = updateFields.color as string;
|
||||
@@ -359,46 +439,64 @@ export class GoogleCalendar implements INodeType {
|
||||
body.visibility = updateFields.visibility as string;
|
||||
}
|
||||
if (!useDefaultReminders) {
|
||||
const reminders = (this.getNodeParameter('remindersUi', i) as IDataObject).remindersValues as IDataObject[];
|
||||
const reminders = (this.getNodeParameter(
|
||||
'remindersUi',
|
||||
i
|
||||
) as IDataObject).remindersValues as IDataObject[];
|
||||
body.reminders = {
|
||||
useDefault: false,
|
||||
useDefault: false
|
||||
};
|
||||
if (reminders) {
|
||||
body.reminders.overrides = reminders;
|
||||
}
|
||||
}
|
||||
if (updateFields.allday
|
||||
&& updateFields.start
|
||||
&& updateFields.end) {
|
||||
if (updateFields.allday && updateFields.start && updateFields.end) {
|
||||
body.start = {
|
||||
date: moment(updateFields.start as string).utc().format('YYYY-MM-DD'),
|
||||
date: moment(updateFields.start as string)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD')
|
||||
};
|
||||
body.end = {
|
||||
date: moment(updateFields.end as string).utc().format('YYYY-MM-DD'),
|
||||
date: moment(updateFields.end as string)
|
||||
.utc()
|
||||
.format('YYYY-MM-DD')
|
||||
};
|
||||
}
|
||||
//exampel: RRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10;UNTIL=20110701T170000Z
|
||||
//https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
|
||||
body.recurrence = [];
|
||||
if (updateFields.repeatHowManyTimes
|
||||
&& updateFields.repeatUntil) {
|
||||
throw new Error(`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`);
|
||||
if (updateFields.repeatHowManyTimes && updateFields.repeatUntil) {
|
||||
throw new Error(
|
||||
`You can set either 'Repeat How Many Times' or 'Repeat Until' but not both`
|
||||
);
|
||||
}
|
||||
if (updateFields.repeatFrecuency) {
|
||||
body.recurrence?.push(`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`);
|
||||
body.recurrence?.push(
|
||||
`FREQ=${(updateFields.repeatFrecuency as string).toUpperCase()};`
|
||||
);
|
||||
}
|
||||
if (updateFields.repeatHowManyTimes) {
|
||||
body.recurrence?.push(`COUNT=${updateFields.repeatHowManyTimes};`);
|
||||
}
|
||||
if (updateFields.repeatUntil) {
|
||||
body.recurrence?.push(`UNTIL=${moment(updateFields.repeatUntil as string).utc().format('YYYYMMDDTHHmmss')}Z`);
|
||||
body.recurrence?.push(
|
||||
`UNTIL=${moment(updateFields.repeatUntil as string)
|
||||
.utc()
|
||||
.format('YYYYMMDDTHHmmss')}Z`
|
||||
);
|
||||
}
|
||||
if (body.recurrence.length !== 0) {
|
||||
body.recurrence = [`RRULE:${body.recurrence.join('')}`];
|
||||
} else {
|
||||
delete body.recurrence;
|
||||
}
|
||||
responseData = await googleApiRequest.call(this, 'PATCH', `/calendar/v3/calendars/${calendarId}/events/${eventId}`, body, qs);
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/calendar/v3/calendars/${calendarId}/events/${eventId}`,
|
||||
body,
|
||||
qs
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
142
packages/nodes-base/nodes/Google/Drive/GenericFunctions.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as moment from 'moment-timezone';
|
||||
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string;
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://www.googleapis.com${resource}`,
|
||||
json: true,
|
||||
};
|
||||
options = Object.assign({}, options, option);
|
||||
try {
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
if (authenticationMethod === 'serviceAccount') {
|
||||
const credentials = this.getCredentials('googleApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const { access_token } = await getAccessToken.call(this, credentials as IDataObject);
|
||||
|
||||
options.headers!.Authorization = `Bearer ${access_token}`;
|
||||
//@ts-ignore
|
||||
return await this.helpers.request(options);
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2.call(this, 'googleDriveOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
|
||||
let errorMessages;
|
||||
|
||||
if (error.response.body.error.errors) {
|
||||
// Try to return the error prettier
|
||||
errorMessages = error.response.body.error.errors;
|
||||
|
||||
errorMessages = errorMessages.map((errorItem: IDataObject) => errorItem.message);
|
||||
|
||||
errorMessages = errorMessages.join('|');
|
||||
|
||||
} else if (error.response.body.error.message) {
|
||||
errorMessages = error.response.body.error.message;
|
||||
}
|
||||
|
||||
throw new Error(`Google Drive error response [${error.statusCode}]: ${errorMessages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
query.maxResults = 100;
|
||||
|
||||
do {
|
||||
responseData = await googleApiRequest.call(this, method, endpoint, body, query);
|
||||
query.pageToken = responseData['nextPageToken'];
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
} while (
|
||||
responseData['nextPageToken'] !== undefined &&
|
||||
responseData['nextPageToken'] !== ''
|
||||
);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise<IDataObject> {
|
||||
//https://developers.google.com/identity/protocols/oauth2/service-account#httprest
|
||||
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/drive.appdata',
|
||||
'https://www.googleapis.com/auth/drive.photos.readonly',
|
||||
];
|
||||
|
||||
const now = moment().unix();
|
||||
|
||||
const signature = jwt.sign(
|
||||
{
|
||||
'iss': credentials.email as string,
|
||||
'sub': credentials.email as string,
|
||||
'scope': scopes.join(' '),
|
||||
'aud': `https://oauth2.googleapis.com/token`,
|
||||
'iat': now,
|
||||
'exp': now + 3600,
|
||||
},
|
||||
credentials.privateKey as string,
|
||||
{
|
||||
algorithm: 'RS256',
|
||||
header: {
|
||||
'kid': credentials.privateKey as string,
|
||||
'typ': 'JWT',
|
||||
'alg': 'RS256',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
method: 'POST',
|
||||
form: {
|
||||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||
assertion: signature,
|
||||
},
|
||||
uri: 'https://oauth2.googleapis.com/token',
|
||||
json: true
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
return this.helpers.request(options);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { google } from 'googleapis';
|
||||
const { Readable } = require('stream');
|
||||
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
@@ -12,8 +10,9 @@ import {
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getAuthenticationClient } from '../GoogleApi';
|
||||
|
||||
import {
|
||||
googleApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class GoogleDrive implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -34,9 +33,43 @@ export class GoogleDrive implements INodeType {
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'serviceAccount',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'googleDriveOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Service Account',
|
||||
value: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
@@ -764,7 +797,7 @@ export class GoogleDrive implements INodeType {
|
||||
{
|
||||
name: 'domain',
|
||||
value: 'domain',
|
||||
description:"All files shared to the user's domain that are searchable",
|
||||
description: 'All files shared to the user\'s domain that are searchable',
|
||||
},
|
||||
{
|
||||
name: 'drive',
|
||||
@@ -813,26 +846,6 @@ export class GoogleDrive implements INodeType {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('googleApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const scopes = [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/drive.appdata',
|
||||
'https://www.googleapis.com/auth/drive.photos.readonly',
|
||||
];
|
||||
|
||||
const client = await getAuthenticationClient(credentials.email as string, credentials.privateKey as string, scopes);
|
||||
|
||||
const drive = google.drive({
|
||||
version: 'v3',
|
||||
// @ts-ignore
|
||||
auth: client,
|
||||
});
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
@@ -857,22 +870,20 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const fileId = this.getNodeParameter('fileId', i) as string;
|
||||
|
||||
const copyOptions = {
|
||||
fileId,
|
||||
const body: IDataObject = {
|
||||
fields: queryFields,
|
||||
requestBody: {} as IDataObject,
|
||||
};
|
||||
|
||||
const optionProperties = ['name', 'parents'];
|
||||
for (const propertyName of optionProperties) {
|
||||
if (options[propertyName] !== undefined) {
|
||||
copyOptions.requestBody[propertyName] = options[propertyName];
|
||||
body[propertyName] = options[propertyName];
|
||||
}
|
||||
}
|
||||
|
||||
const response = await drive.files.copy(copyOptions);
|
||||
const response = await googleApiRequest.call(this, 'POST', `/drive/v3/files/${fileId}/copy`, body);
|
||||
|
||||
returnData.push(response.data as IDataObject);
|
||||
returnData.push(response as IDataObject);
|
||||
|
||||
} else if (operation === 'download') {
|
||||
// ----------------------------------
|
||||
@@ -881,15 +892,13 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const fileId = this.getNodeParameter('fileId', i) as string;
|
||||
|
||||
const response = await drive.files.get(
|
||||
{
|
||||
fileId,
|
||||
alt: 'media',
|
||||
},
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
},
|
||||
);
|
||||
const requestOptions = {
|
||||
resolveWithFullResponse: true,
|
||||
encoding: null,
|
||||
json: false,
|
||||
};
|
||||
|
||||
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files/${fileId}`, {}, { alt: 'media' }, undefined, requestOptions);
|
||||
|
||||
let mimeType: string | undefined;
|
||||
if (response.headers['content-type']) {
|
||||
@@ -912,7 +921,7 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
|
||||
const data = Buffer.from(response.data as string);
|
||||
const data = Buffer.from(response.body as string);
|
||||
|
||||
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, undefined, mimeType);
|
||||
|
||||
@@ -936,7 +945,7 @@ export class GoogleDrive implements INodeType {
|
||||
queryCorpora = options.corpora as string;
|
||||
}
|
||||
|
||||
let driveId : string | undefined;
|
||||
let driveId: string | undefined;
|
||||
driveId = options.driveId as string;
|
||||
if (driveId === '') {
|
||||
driveId = undefined;
|
||||
@@ -988,20 +997,19 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const pageSize = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
const res = await drive.files.list({
|
||||
const qs = {
|
||||
pageSize,
|
||||
orderBy: 'modifiedTime',
|
||||
fields: `nextPageToken, files(${queryFields})`,
|
||||
spaces: querySpaces,
|
||||
corpora: queryCorpora,
|
||||
driveId,
|
||||
q: queryString,
|
||||
includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''), // Actually depracated,
|
||||
supportsAllDrives: (queryCorpora !== '' || driveId !== ''), // see https://developers.google.com/drive/api/v3/reference/files/list
|
||||
// However until June 2020 still needs to be set, to avoid API errors.
|
||||
});
|
||||
includeItemsFromAllDrives: (queryCorpora !== '' || driveId !== ''),
|
||||
supportsAllDrives: (queryCorpora !== '' || driveId !== ''),
|
||||
};
|
||||
|
||||
const files = res!.data.files;
|
||||
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/files`, {}, qs);
|
||||
|
||||
const files = response!.files;
|
||||
|
||||
return [this.helpers.returnJsonArray(files as IDataObject[])];
|
||||
|
||||
@@ -1044,29 +1052,35 @@ export class GoogleDrive implements INodeType {
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
const parents = this.getNodeParameter('parents', i) as string[];
|
||||
|
||||
const response = await drive.files.create({
|
||||
requestBody: {
|
||||
name,
|
||||
originalFilename,
|
||||
parents,
|
||||
},
|
||||
let qs: IDataObject = {
|
||||
fields: queryFields,
|
||||
media: {
|
||||
mimeType,
|
||||
body: ((buffer: Buffer) => {
|
||||
const readableInstanceStream = new Readable({
|
||||
read() {
|
||||
this.push(buffer);
|
||||
this.push(null);
|
||||
}
|
||||
});
|
||||
uploadType: 'media',
|
||||
};
|
||||
|
||||
return readableInstanceStream;
|
||||
})(body),
|
||||
const requestOptions = {
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
'Content-Length': body.byteLength,
|
||||
},
|
||||
});
|
||||
encoding: null,
|
||||
json: false,
|
||||
};
|
||||
|
||||
returnData.push(response.data as IDataObject);
|
||||
let response = await googleApiRequest.call(this, 'POST', `/upload/drive/v3/files`, body, qs, undefined, requestOptions);
|
||||
|
||||
body = {
|
||||
mimeType,
|
||||
name,
|
||||
originalFilename,
|
||||
};
|
||||
|
||||
qs = {
|
||||
addParents: parents.join(','),
|
||||
};
|
||||
|
||||
response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${JSON.parse(response).id}`, body, qs);
|
||||
|
||||
returnData.push(response as IDataObject);
|
||||
}
|
||||
|
||||
} else if (resource === 'folder') {
|
||||
@@ -1077,19 +1091,19 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
|
||||
const fileMetadata = {
|
||||
const body = {
|
||||
name,
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
parents: options.parents || [],
|
||||
};
|
||||
|
||||
const response = await drive.files.create({
|
||||
// @ts-ignore
|
||||
resource: fileMetadata,
|
||||
const qs = {
|
||||
fields: queryFields,
|
||||
});
|
||||
};
|
||||
|
||||
returnData.push(response.data as IDataObject);
|
||||
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs);
|
||||
|
||||
returnData.push(response as IDataObject);
|
||||
}
|
||||
}
|
||||
if (['file', 'folder'].includes(resource)) {
|
||||
@@ -1100,9 +1114,7 @@ export class GoogleDrive implements INodeType {
|
||||
|
||||
const fileId = this.getNodeParameter('fileId', i) as string;
|
||||
|
||||
await drive.files.delete({
|
||||
fileId,
|
||||
});
|
||||
const response = await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`);
|
||||
|
||||
// If we are still here it did succeed
|
||||
returnData.push({
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
import { JWT } from 'google-auth-library';
|
||||
import { google } from 'googleapis';
|
||||
|
||||
|
||||
/**
|
||||
* Returns the authentication client needed to access spreadsheet
|
||||
*/
|
||||
export async function getAuthenticationClient(email: string, privateKey: string, scopes: string[]): Promise <JWT> {
|
||||
const client = new google.auth.JWT(
|
||||
email,
|
||||
undefined,
|
||||
privateKey,
|
||||
scopes,
|
||||
undefined
|
||||
);
|
||||
|
||||
// TODO: Check later if this or the above should be cached
|
||||
await client.authorize();
|
||||
|
||||
// @ts-ignore
|
||||
return client;
|
||||
}
|
||||
@@ -622,7 +622,7 @@ export class GoogleSheets implements INodeType {
|
||||
// ----------------------------------
|
||||
// append
|
||||
// ----------------------------------
|
||||
const keyRow = this.getNodeParameter('keyRow', 0) as number;
|
||||
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
@@ -670,7 +670,7 @@ export class GoogleSheets implements INodeType {
|
||||
sheetId: range.sheetId,
|
||||
dimension: deletePropertyToDimensions[propertyName] as string,
|
||||
startIndex: range.startIndex,
|
||||
endIndex: range.startIndex + range.amount,
|
||||
endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -693,8 +693,8 @@ export class GoogleSheets implements INodeType {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
|
||||
const keyRow = this.getNodeParameter('keyRow', 0) as number;
|
||||
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
|
||||
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
@@ -735,8 +735,8 @@ export class GoogleSheets implements INodeType {
|
||||
}
|
||||
];
|
||||
} else {
|
||||
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
|
||||
const keyRow = this.getNodeParameter('keyRow', 0) as number;
|
||||
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
|
||||
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
|
||||
|
||||
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
|
||||
}
|
||||
@@ -769,8 +769,8 @@ export class GoogleSheets implements INodeType {
|
||||
const data = await sheet.batchUpdate(updateData, valueInputMode);
|
||||
} else {
|
||||
const keyName = this.getNodeParameter('key', 0) as string;
|
||||
const keyRow = this.getNodeParameter('keyRow', 0) as number;
|
||||
const dataStartRow = this.getNodeParameter('dataStartRow', 0) as number;
|
||||
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
|
||||
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
|
||||
|
||||
const setData: IDataObject[] = [];
|
||||
items.forEach((item) => {
|
||||
|
||||
92
packages/nodes-base/nodes/Google/Task/GenericFunctions.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function googleApiRequest(
|
||||
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
|
||||
method: string,
|
||||
resource: string,
|
||||
body: IDataObject = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
headers: IDataObject = {}
|
||||
): Promise<any> { // tslint:disable-line:no-any
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://www.googleapis.com${resource}`,
|
||||
json: true
|
||||
};
|
||||
|
||||
try {
|
||||
if (Object.keys(headers).length !== 0) {
|
||||
options.headers = Object.assign({}, options.headers, headers);
|
||||
}
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2.call(
|
||||
this,
|
||||
'googleTasksOAuth2Api',
|
||||
options
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
|
||||
let errors = error.response.body.error.errors;
|
||||
|
||||
errors = errors.map((e: IDataObject) => e.message);
|
||||
// Try to return the error prettier
|
||||
throw new Error(
|
||||
`Google Tasks error response [${error.statusCode}]: ${errors.join('|')}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function googleApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
||||
propertyName: string,
|
||||
method: string,
|
||||
endpoint: string,
|
||||
body: IDataObject = {},
|
||||
query: IDataObject = {}
|
||||
): Promise<any> { // tslint:disable-line:no-any
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
query.maxResults = 100;
|
||||
|
||||
do {
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
method,
|
||||
endpoint,
|
||||
body,
|
||||
query
|
||||
);
|
||||
query.pageToken = responseData['nextPageToken'];
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
} while (
|
||||
responseData['nextPageToken'] !== undefined &&
|
||||
responseData['nextPageToken'] !== ''
|
||||
);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
279
packages/nodes-base/nodes/Google/Task/GoogleTasks.node.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
googleApiRequest,
|
||||
googleApiRequestAllItems,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
taskOperations,
|
||||
taskFields,
|
||||
} from './TaskDescription';
|
||||
|
||||
export class GoogleTasks implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Google Tasks',
|
||||
name: 'googleTasks',
|
||||
icon: 'file:googleTasks.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Google Tasks API.',
|
||||
defaults: {
|
||||
name: 'Google Tasks',
|
||||
color: '#3E87E4'
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleTasksOAuth2Api',
|
||||
required: true
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Task',
|
||||
value: 'task'
|
||||
}
|
||||
],
|
||||
default: 'task',
|
||||
description: 'The resource to operate on.'
|
||||
},
|
||||
...taskOperations,
|
||||
...taskFields
|
||||
]
|
||||
};
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the tasklists to display them to user so that he can select them easily
|
||||
|
||||
async getTasks(
|
||||
this: ILoadOptionsFunctions
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tasks = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'items',
|
||||
'GET',
|
||||
'/tasks/v1/users/@me/lists'
|
||||
);
|
||||
for (const task of tasks) {
|
||||
const taskName = task.title;
|
||||
const taskId = task.id;
|
||||
returnData.push({
|
||||
name: taskName,
|
||||
value: taskId
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = (items.length as unknown) as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
let body: IDataObject = {};
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (resource === 'task') {
|
||||
if (operation === 'create') {
|
||||
body = {};
|
||||
//https://developers.google.com/tasks/v1/reference/tasks/insert
|
||||
const taskId = this.getNodeParameter('task', i) as string;
|
||||
body.title = this.getNodeParameter('title', i) as string;
|
||||
const additionalFields = this.getNodeParameter(
|
||||
'additionalFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
|
||||
if (additionalFields.parent) {
|
||||
qs.parent = additionalFields.parent as string;
|
||||
}
|
||||
if (additionalFields.previous) {
|
||||
qs.previous = additionalFields.previous as string;
|
||||
}
|
||||
|
||||
if (additionalFields.status) {
|
||||
body.status = additionalFields.status as string;
|
||||
}
|
||||
|
||||
if (additionalFields.notes) {
|
||||
body.notes = additionalFields.notes as string;
|
||||
}
|
||||
if (additionalFields.dueDate) {
|
||||
body.dueDate = additionalFields.dueDate as string;
|
||||
}
|
||||
|
||||
if (additionalFields.completed) {
|
||||
body.completed = additionalFields.completed as string;
|
||||
}
|
||||
|
||||
if (additionalFields.deleted) {
|
||||
body.deleted = additionalFields.deleted as boolean;
|
||||
}
|
||||
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/tasks/v1/lists/${taskId}/tasks`,
|
||||
body,
|
||||
qs
|
||||
);
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
//https://developers.google.com/tasks/v1/reference/tasks/delete
|
||||
const taskListId = this.getNodeParameter('task', i) as string;
|
||||
const taskId = this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
|
||||
{}
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
if (operation === 'get') {
|
||||
//https://developers.google.com/tasks/v1/reference/tasks/get
|
||||
const taskListId = this.getNodeParameter('task', i) as string;
|
||||
const taskId = this.getNodeParameter('taskId', i) as string;
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
//https://developers.google.com/tasks/v1/reference/tasks/list
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
const taskListId = this.getNodeParameter('task', i) as string;
|
||||
const options = this.getNodeParameter(
|
||||
'additionalFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
if (options.completedMax) {
|
||||
qs.completedMax = options.completedMax as string;
|
||||
}
|
||||
if (options.completedMin) {
|
||||
qs.completedMin = options.completedMin as string;
|
||||
}
|
||||
if (options.dueMin) {
|
||||
qs.dueMin = options.dueMin as string;
|
||||
}
|
||||
if (options.dueMax) {
|
||||
qs.dueMax = options.dueMax as string;
|
||||
}
|
||||
if (options.showCompleted) {
|
||||
qs.showCompleted = options.showCompleted as boolean;
|
||||
}
|
||||
if (options.showDeleted) {
|
||||
qs.showDeleted = options.showDeleted as boolean;
|
||||
}
|
||||
if (options.showHidden) {
|
||||
qs.showHidden = options.showHidden as boolean;
|
||||
}
|
||||
if (options.updatedMin) {
|
||||
qs.updatedMin = options.updatedMin as string;
|
||||
}
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'items',
|
||||
'GET',
|
||||
`/tasks/v1/lists/${taskListId}/tasks`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
} else {
|
||||
qs.maxResults = this.getNodeParameter('limit', i) as number;
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/tasks/v1/lists/${taskListId}/tasks`,
|
||||
{},
|
||||
qs
|
||||
);
|
||||
responseData = responseData.items;
|
||||
}
|
||||
}
|
||||
if (operation === 'update') {
|
||||
body = {};
|
||||
//https://developers.google.com/tasks/v1/reference/tasks/patch
|
||||
const taskListId = this.getNodeParameter('task', i) as string;
|
||||
const taskId = this.getNodeParameter('taskId', i) as string;
|
||||
const updateFields = this.getNodeParameter(
|
||||
'updateFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
|
||||
if (updateFields.previous) {
|
||||
qs.previous = updateFields.previous as string;
|
||||
}
|
||||
|
||||
if (updateFields.status) {
|
||||
body.status = updateFields.status as string;
|
||||
}
|
||||
|
||||
if (updateFields.notes) {
|
||||
body.notes = updateFields.notes as string;
|
||||
}
|
||||
|
||||
if (updateFields.title) {
|
||||
body.title = updateFields.title as string;
|
||||
}
|
||||
|
||||
if (updateFields.dueDate) {
|
||||
body.dueDate = updateFields.dueDate as string;
|
||||
}
|
||||
|
||||
if (updateFields.completed) {
|
||||
body.completed = updateFields.completed as string;
|
||||
}
|
||||
|
||||
if (updateFields.deleted) {
|
||||
body.deleted = updateFields.deleted as boolean;
|
||||
}
|
||||
|
||||
responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/tasks/v1/lists/${taskListId}/tasks/${taskId}`,
|
||||
body,
|
||||
qs
|
||||
);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else if (responseData !== undefined) {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
493
packages/nodes-base/nodes/Google/Task/TaskDescription.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const taskOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Add a task to tasklist',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Retrieve a task',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Retrieve all tasks from a tasklist',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a task',
|
||||
}
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
}
|
||||
] as INodeProperties[];
|
||||
|
||||
export const taskFields = [
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* task:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'TaskList',
|
||||
name: 'task',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTasks',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Title of the task.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Completion Date',
|
||||
name: 'completed',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Deleted',
|
||||
name: 'deleted',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Flag indicating whether the task has been deleted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date',
|
||||
name: 'dueDate',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Due date of the task.',
|
||||
},
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Additional Notes.',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent',
|
||||
name: 'parent',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Parent task identifier. If the task is created at the top level, this parameter is omitted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Previous',
|
||||
name: 'previous',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Needs Action',
|
||||
value: 'needsAction',
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
value: 'completed',
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
description: 'Current status of the task.',
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* task:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'TaskList',
|
||||
name: 'task',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTasks',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* task:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'TaskList',
|
||||
name: 'task',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTasks',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
}
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* task:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'TaskList',
|
||||
name: 'task',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTasks',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100
|
||||
},
|
||||
default: 20,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Completed Max',
|
||||
name: 'completedMax',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Upper bound for a task completion date (as a RFC 3339 timestamp) to filter by.',
|
||||
},
|
||||
{
|
||||
displayName: 'Completed Min',
|
||||
name: 'completedMin',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Lower bound for a task completion date (as a RFC 3339 timestamp) to filter by.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Min',
|
||||
name: 'dueMin',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Lower bound for a task due date (as a RFC 3339 timestamp) to filter by.',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Max',
|
||||
name: 'dueMax',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Upper bound for a task due date (as a RFC 3339 timestamp) to filter by.',
|
||||
},
|
||||
{
|
||||
displayName: 'Show Completed',
|
||||
name: 'showCompleted',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Flag indicating whether completed tasks are returned in the result',
|
||||
},
|
||||
{
|
||||
displayName: 'Show Deleted',
|
||||
name: 'showDeleted',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Flag indicating whether deleted tasks are returned in the result',
|
||||
},
|
||||
{
|
||||
displayName: 'Show Hidden',
|
||||
name: 'showHidden',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Flag indicating whether hidden tasks are returned in the result',
|
||||
},
|
||||
{
|
||||
displayName: 'Updated Min',
|
||||
name: 'updatedMin',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Lower bound for a task last modification time (as a RFC 3339 timestamp) to filter by.',
|
||||
},
|
||||
]
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* task:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'TaskList',
|
||||
name: 'task',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTasks',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Update Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Completion Date',
|
||||
name: 'completed',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: `Completion date of the task (as a RFC 3339 timestamp). This field is omitted if the task has not been completed.`,
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Deleted',
|
||||
name: 'deleted',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Flag indicating whether the task has been deleted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: 'Additional Notes.',
|
||||
},
|
||||
{
|
||||
displayName: 'Previous',
|
||||
name: 'previous',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Previous sibling task identifier. If the task is created at the first position among its siblings, this parameter is omitted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Status',
|
||||
name: 'status',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Needs Update',
|
||||
value: 'needsAction',
|
||||
},
|
||||
{
|
||||
name: 'Completed',
|
||||
value: 'completed',
|
||||
}
|
||||
],
|
||||
default: '',
|
||||
description: 'Current status of the task.',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Title of the task.',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as INodeProperties[];
|
||||
BIN
packages/nodes-base/nodes/Google/Task/googleTasks.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
80
packages/nodes-base/nodes/HackerNews/GenericFunctions.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
|
||||
/**
|
||||
* Make an API request to HackerNews
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} endpoint
|
||||
* @param {IDataObject} qs
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function hackerNewsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
|
||||
const options: OptionsWithUri = {
|
||||
method,
|
||||
qs,
|
||||
uri: `http://hn.algolia.com/api/v1/${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
// Try to return the error prettier
|
||||
throw new Error(`Hacker News error response [${error.statusCode}]: ${error.response.body.error}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make an API request to HackerNews
|
||||
* and return all results
|
||||
*
|
||||
* @export
|
||||
* @param {(IHookFunctions | IExecuteFunctions)} this
|
||||
* @param {string} method
|
||||
* @param {string} endpoint
|
||||
* @param {IDataObject} qs
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function hackerNewsApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, qs: IDataObject): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
qs.hitsPerPage = 100;
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
let itemsReceived = 0;
|
||||
|
||||
do {
|
||||
responseData = await hackerNewsApiRequest.call(this, method, endpoint, qs);
|
||||
returnData.push.apply(returnData, responseData.hits);
|
||||
|
||||
if (returnData !== undefined) {
|
||||
itemsReceived += returnData.length;
|
||||
}
|
||||
|
||||
} while (
|
||||
responseData.nbHits > itemsReceived
|
||||
);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
384
packages/nodes-base/nodes/HackerNews/HackerNews.node.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
hackerNewsApiRequest,
|
||||
hackerNewsApiRequestAllItems,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class HackerNews implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Hacker News',
|
||||
name: 'hackerNews',
|
||||
icon: 'file:hackernews.png',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Hacker News API',
|
||||
defaults: {
|
||||
name: 'Hacker News',
|
||||
color: '#ff6600',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
// ----------------------------------
|
||||
// Resources
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'All',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
name: 'Article',
|
||||
value: 'article',
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
default: 'article',
|
||||
description: 'Resource to consume.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// Operations
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'all',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all items',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
description: 'Operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'article',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a Hacker News article',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
description: 'Operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a Hacker News user',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
description: 'Operation to perform.',
|
||||
},
|
||||
// ----------------------------------
|
||||
// Fields
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Article ID',
|
||||
name: 'articleId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The ID of the Hacker News article to be returned',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'article',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The Hacker News user to be returned',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return all results for the query or only up to a limit.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'all',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 100,
|
||||
description: 'Limit of Hacker News articles to be returned for the query.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'all',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'article',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include comments',
|
||||
name: 'includeComments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to include all the comments in a Hacker News article.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'all',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Keyword',
|
||||
name: 'keyword',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The keyword for filtering the results of the query.',
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: 'Story',
|
||||
value: 'story',
|
||||
description: 'Returns query results filtered by story tag',
|
||||
},
|
||||
{
|
||||
name: 'Comment',
|
||||
value: 'comment',
|
||||
description: 'Returns query results filtered by comment tag',
|
||||
},
|
||||
{
|
||||
name: 'Poll',
|
||||
value: 'poll',
|
||||
description: 'Returns query results filtered by poll tag',
|
||||
},
|
||||
{
|
||||
name: 'Show HN',
|
||||
value: 'show_hn', // snake case per HN tags
|
||||
description: 'Returns query results filtered by Show HN tag',
|
||||
},
|
||||
{
|
||||
name: 'Ask HN',
|
||||
value: 'ask_hn', // snake case per HN tags
|
||||
description: 'Returns query results filtered by Ask HN tag',
|
||||
},
|
||||
{
|
||||
name: 'Front Page',
|
||||
value: 'front_page', // snake case per HN tags
|
||||
description: 'Returns query results filtered by Front Page tag',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Tags for filtering the results of the query.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
let returnAll = false;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
let qs: IDataObject = {};
|
||||
let endpoint = '';
|
||||
let includeComments = false;
|
||||
|
||||
if (resource === 'all') {
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
const keyword = additionalFields.keyword as string;
|
||||
const tags = additionalFields.tags as string[];
|
||||
|
||||
qs = {
|
||||
query: keyword,
|
||||
tags: tags ? tags.join() : '',
|
||||
};
|
||||
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
if (!returnAll) {
|
||||
qs.hitsPerPage = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
|
||||
endpoint = 'search?';
|
||||
|
||||
} else {
|
||||
throw new Error(`The operation '${operation}' is unknown!`);
|
||||
}
|
||||
} else if (resource === 'article') {
|
||||
|
||||
if (operation === 'get') {
|
||||
|
||||
endpoint = `items/${this.getNodeParameter('articleId', i)}`;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
includeComments = additionalFields.includeComments as boolean;
|
||||
|
||||
} else {
|
||||
throw new Error(`The operation '${operation}' is unknown!`);
|
||||
}
|
||||
|
||||
} else if (resource === 'user') {
|
||||
|
||||
if (operation === 'get') {
|
||||
endpoint = `users/${this.getNodeParameter('username', i)}`;
|
||||
|
||||
} else {
|
||||
throw new Error(`The operation '${operation}' is unknown!`);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`The resource '${resource}' is unknown!`);
|
||||
}
|
||||
|
||||
|
||||
let responseData;
|
||||
if (returnAll === true) {
|
||||
responseData = await hackerNewsApiRequestAllItems.call(this, 'GET', endpoint, qs);
|
||||
} else {
|
||||
responseData = await hackerNewsApiRequest.call(this, 'GET', endpoint, qs);
|
||||
if (resource === 'all' && operation === 'getAll') {
|
||||
responseData = responseData.hits;
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'article' && operation === 'get' && !includeComments) {
|
||||
delete responseData.children;
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/HackerNews/hackernews.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
@@ -802,7 +802,7 @@ export class HttpRequest implements INodeType {
|
||||
|
||||
if (oAuth2Api !== undefined) {
|
||||
//@ts-ignore
|
||||
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
|
||||
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
|
||||
} else {
|
||||
response = await this.helpers.request(requestOptions);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
|
||||
export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const node = this.getNode();
|
||||
const credentialName = Object.keys(node.credentials!)[0];
|
||||
const credentials = this.getCredentials(credentialName);
|
||||
let authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
if (this.getNode().type.includes('Trigger')) {
|
||||
authenticationMethod = 'developerApi';
|
||||
}
|
||||
|
||||
query!.hapikey = credentials!.apiKey as string;
|
||||
const options: OptionsWithUri = {
|
||||
method,
|
||||
qs: query,
|
||||
@@ -28,18 +29,42 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions
|
||||
json: true,
|
||||
useQuerystring: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'apiKey') {
|
||||
const credentials = this.getCredentials('hubspotApi');
|
||||
|
||||
options.qs.hapikey = credentials!.apiKey as string;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else if (authenticationMethod === 'developerApi') {
|
||||
const credentials = this.getCredentials('hubspotDeveloperApi');
|
||||
|
||||
options.qs.hapikey = credentials!.apiKey as string;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, 'Bearer');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body && error.response.body.errors) {
|
||||
// Try to return the error prettier
|
||||
let errorMessages = error.response.body.errors;
|
||||
let errorMessages;
|
||||
|
||||
if (errorMessages[0].message) {
|
||||
// @ts-ignore
|
||||
errorMessages = errorMessages.map(errorItem => errorItem.message);
|
||||
if (error.response && error.response.body) {
|
||||
|
||||
if (error.response.body.message) {
|
||||
|
||||
errorMessages = [error.response.body.message];
|
||||
|
||||
} else if (error.response.body.errors) {
|
||||
// Try to return the error prettier
|
||||
errorMessages = error.response.body.errors;
|
||||
|
||||
if (errorMessages[0].message) {
|
||||
// @ts-ignore
|
||||
errorMessages = errorMessages.map(errorItem => errorItem.message);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessages.join('|')}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,44 @@ export class Hubspot implements INodeType {
|
||||
{
|
||||
name: 'hubspotApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'apiKey',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hubspotOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Key',
|
||||
value: 'apiKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiKey',
|
||||
description: 'The method of authentication.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
|
||||
@@ -246,7 +246,13 @@ export class HubspotTrigger implements INodeType {
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const credentials = this.getCredentials('hubspotDeveloperApi');
|
||||
|
||||
const credentials = this.getCredentials('hubspotDeveloperApi') as IDataObject;
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials found!');
|
||||
}
|
||||
|
||||
const req = this.getRequestObject();
|
||||
const bodyData = req.body;
|
||||
const headerData = this.getHeaderData();
|
||||
@@ -254,12 +260,18 @@ export class HubspotTrigger implements INodeType {
|
||||
if (headerData['x-hubspot-signature'] === undefined) {
|
||||
return {};
|
||||
}
|
||||
const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`;
|
||||
const signature = createHash('sha256').update(hash).digest('hex');
|
||||
//@ts-ignore
|
||||
if (signature !== headerData['x-hubspot-signature']) {
|
||||
return {};
|
||||
|
||||
// check signare if client secret is defined
|
||||
|
||||
if (credentials.clientSecret !== '') {
|
||||
const hash = `${credentials!.clientSecret}${JSON.stringify(bodyData)}`;
|
||||
const signature = createHash('sha256').update(hash).digest('hex');
|
||||
//@ts-ignore
|
||||
if (signature !== headerData['x-hubspot-signature']) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < bodyData.length; i++) {
|
||||
const subscriptionType = bodyData[i].subscriptionType as string;
|
||||
if (subscriptionType.includes('contact')) {
|
||||
|
||||
@@ -41,12 +41,12 @@ export const issueOperations = [
|
||||
{
|
||||
name: 'Notify',
|
||||
value: 'notify',
|
||||
description: 'Creates an email notification for an issue and adds it to the mail queue.',
|
||||
description: 'Create an email notification for an issue and add it to the mail queue',
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'transitions',
|
||||
description: `Returns either all transitions or a transition that can be performed by the user on an issue, based on the issue's status.`,
|
||||
description: `Return either all transitions or a transition that can be performed by the user on an issue, based on the issue's status`,
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
OptionsWithUrl,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
@@ -14,37 +14,53 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {} ,headers?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('mailchimpApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const headerWithAuthentication = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` });
|
||||
|
||||
if (!(credentials.apiKey as string).includes('-')) {
|
||||
throw new Error('The API key is not valid!');
|
||||
}
|
||||
|
||||
const datacenter = (credentials.apiKey as string).split('-').pop();
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0) as string;
|
||||
|
||||
const host = 'api.mailchimp.com/3.0';
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: headerWithAuthentication,
|
||||
const options: OptionsWithUrl = {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
uri: `https://${datacenter}.${host}${endpoint}`,
|
||||
body,
|
||||
url: ``,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(body).length !== 0) {
|
||||
options.body = body;
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'apiKey') {
|
||||
const credentials = this.getCredentials('mailchimpApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` });
|
||||
|
||||
if (!(credentials.apiKey as string).includes('-')) {
|
||||
throw new Error('The API key is not valid!');
|
||||
}
|
||||
|
||||
const datacenter = (credentials.apiKey as string).split('-').pop();
|
||||
options.url = `https://${datacenter}.${host}${endpoint}`;
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject;
|
||||
|
||||
const { api_endpoint } = await getMetadata.call(this, credentials.oauthTokenData as IDataObject);
|
||||
|
||||
options.url = `${api_endpoint}/3.0${endpoint}`;
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, 'Bearer');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response.body && error.response.body.detail) {
|
||||
if (error.respose && error.response.body && error.response.body.detail) {
|
||||
throw new Error(`Mailchimp Error response [${error.statusCode}]: ${error.response.body.detail}`);
|
||||
}
|
||||
throw error;
|
||||
@@ -80,3 +96,17 @@ export function validateJSON(json: string | undefined): any { // tslint:disable-
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMetadata(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, oauthTokenData: IDataObject) {
|
||||
const credentials = this.getCredentials('mailchimpOAuth2Api') as IDataObject;
|
||||
const options: OptionsWithUrl = {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `OAuth ${oauthTokenData.access_token}`,
|
||||
},
|
||||
method: 'GET',
|
||||
url: credentials.metadataUrl as string,
|
||||
json: true,
|
||||
};
|
||||
return this.helpers.request!(options);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ interface ICreateMemberBody {
|
||||
timestamp_opt?: string;
|
||||
tags?: string[];
|
||||
merge_fields?: IDataObject;
|
||||
interests?: IDataObject;
|
||||
}
|
||||
|
||||
export class Mailchimp implements INodeType {
|
||||
@@ -69,14 +70,53 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'mailchimpApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'apiKey',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mailchimpOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Key',
|
||||
value: 'apiKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiKey',
|
||||
description: 'Method of authentication.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'List Group',
|
||||
value: 'listGroup',
|
||||
},
|
||||
{
|
||||
name: 'Member',
|
||||
value: 'member',
|
||||
@@ -159,6 +199,28 @@ export class Mailchimp implements INodeType {
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'listGroup',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all groups',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* member:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@@ -221,27 +283,22 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'Subscribed',
|
||||
value: 'subscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Unsubscribed',
|
||||
value: 'unsubscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Cleaned',
|
||||
value: 'cleaned',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Pending',
|
||||
value: 'pending',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Transactional',
|
||||
value: 'transactional',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
@@ -252,7 +309,6 @@ export class Mailchimp implements INodeType {
|
||||
name: 'jsonParameters',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -289,12 +345,10 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
@@ -461,7 +515,6 @@ export class Mailchimp implements INodeType {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -484,7 +537,86 @@ export class Mailchimp implements INodeType {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
'member',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Interest Groups',
|
||||
name: 'groupsUi',
|
||||
placeholder: 'Add Interest Group',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
'member'
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
jsonParameters: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'groupsValues',
|
||||
displayName: 'Group',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Interest Group',
|
||||
},
|
||||
values: [
|
||||
{
|
||||
displayName: 'Category ID',
|
||||
name: 'categoryId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getGroupCategories',
|
||||
loadOptionsDependsOn: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Category Field ID',
|
||||
name: 'categoryFieldId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Interest Groups',
|
||||
name: 'groupJson',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -737,12 +869,10 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
@@ -756,27 +886,22 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'Subscribed',
|
||||
value: 'subscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Unsubscribed',
|
||||
value: 'unsubscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Cleaned',
|
||||
value: 'cleaned',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Pending',
|
||||
value: 'pending',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Transactional',
|
||||
value: 'transactional',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
@@ -839,7 +964,6 @@ export class Mailchimp implements INodeType {
|
||||
name: 'jsonParameters',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -876,17 +1000,73 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Type of email this member asked to get',
|
||||
},
|
||||
{
|
||||
displayName: 'Interest Groups',
|
||||
name: 'groupsUi',
|
||||
placeholder: 'Add Interest Group',
|
||||
type: 'fixedCollection',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/resource':[
|
||||
'member'
|
||||
],
|
||||
'/operation':[
|
||||
'update',
|
||||
],
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'groupsValues',
|
||||
displayName: 'Group',
|
||||
typeOptions: {
|
||||
multipleValueButtonText: 'Add Interest Group',
|
||||
},
|
||||
values: [
|
||||
{
|
||||
displayName: 'Category ID',
|
||||
name: 'categoryId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getGroupCategories',
|
||||
loadOptionsDependsOn: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Category Field ID',
|
||||
name: 'categoryFieldId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Language',
|
||||
name: 'language',
|
||||
@@ -989,27 +1169,22 @@ export class Mailchimp implements INodeType {
|
||||
{
|
||||
name: 'Subscribed',
|
||||
value: 'subscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Unsubscribed',
|
||||
value: 'unsubscribed',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Cleaned',
|
||||
value: 'cleaned',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Pending',
|
||||
value: 'pending',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name: 'Transactional',
|
||||
value: 'transactional',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
@@ -1084,7 +1259,6 @@ export class Mailchimp implements INodeType {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -1107,7 +1281,28 @@ export class Mailchimp implements INodeType {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
description: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
'member',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
jsonParameters: [
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Interest Groups',
|
||||
name: 'groupJson',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource:[
|
||||
@@ -1215,6 +1410,96 @@ export class Mailchimp implements INodeType {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* member:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLists',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'listGroup',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
options: [],
|
||||
required: true,
|
||||
description: 'List of lists',
|
||||
},
|
||||
{
|
||||
displayName: 'Group Category',
|
||||
name: 'groupCategory',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getGroupCategories',
|
||||
loadOptionsDependsOn: [
|
||||
'list',
|
||||
],
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'listGroup',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
options: [],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'listGroup',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'listGroup',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 1000,
|
||||
},
|
||||
default: 500,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1226,7 +1511,7 @@ export class Mailchimp implements INodeType {
|
||||
// select them easily
|
||||
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { lists } = await mailchimpApiRequest.call(this, '/lists', 'GET');
|
||||
const lists = await mailchimpApiRequestAllItems.call(this, '/lists', 'GET', 'lists');
|
||||
for (const list of lists) {
|
||||
const listName = list.name;
|
||||
const listId = list.id;
|
||||
@@ -1254,6 +1539,23 @@ export class Mailchimp implements INodeType {
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the interest fields to display them to user so that he can
|
||||
// select them easily
|
||||
async getGroupCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const listId = this.getCurrentNodeParameter('list');
|
||||
const { categories } = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories`, 'GET');
|
||||
for (const category of categories) {
|
||||
const categoryName = category.title;
|
||||
const categoryId = category.id;
|
||||
returnData.push({
|
||||
name: categoryName,
|
||||
value: categoryId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1267,6 +1569,22 @@ export class Mailchimp implements INodeType {
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (resource === 'listGroup') {
|
||||
//https://mailchimp.com/developer/reference/lists/interest-categories/#get_/lists/-list_id-/interest-categories/-interest_category_id-
|
||||
if (operation === 'getAll') {
|
||||
const listId = this.getNodeParameter('list', i) as string;
|
||||
const categoryId = this.getNodeParameter('groupCategory', i) as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
if (returnAll === true) {
|
||||
responseData = await mailchimpApiRequestAllItems.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', 'interests', {}, qs);
|
||||
} else {
|
||||
qs.count = this.getNodeParameter('limit', i) as number;
|
||||
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/interest-categories/${categoryId}/interests`, 'GET', {}, qs);
|
||||
responseData = responseData.interests;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'member') {
|
||||
//https://mailchimp.com/developer/reference/lists/list-members/#post_/lists/-list_id-/members
|
||||
if (operation === 'create') {
|
||||
@@ -1328,15 +1646,29 @@ export class Mailchimp implements INodeType {
|
||||
}
|
||||
body.merge_fields = mergeFields;
|
||||
}
|
||||
|
||||
const groupsValues = (this.getNodeParameter('groupsUi', i) as IDataObject).groupsValues as IDataObject[];
|
||||
if (groupsValues) {
|
||||
const groups = {};
|
||||
for (let i = 0; i < groupsValues.length; i++) {
|
||||
// @ts-ignore
|
||||
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
|
||||
}
|
||||
body.interests = groups;
|
||||
}
|
||||
} else {
|
||||
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
|
||||
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
|
||||
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
|
||||
if (locationJson) {
|
||||
body.location = locationJson;
|
||||
}
|
||||
if (mergeFieldsJson) {
|
||||
body.merge_fields = mergeFieldsJson;
|
||||
}
|
||||
if (groupJson) {
|
||||
body.interests = groupJson;
|
||||
}
|
||||
}
|
||||
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body);
|
||||
}
|
||||
@@ -1469,15 +1801,31 @@ export class Mailchimp implements INodeType {
|
||||
body.merge_fields = mergeFields;
|
||||
}
|
||||
}
|
||||
if (updateFields.groupsUi) {
|
||||
const groupsValues = (updateFields.groupsUi as IDataObject).groupsValues as IDataObject[];
|
||||
if (groupsValues) {
|
||||
const groups = {};
|
||||
for (let i = 0; i < groupsValues.length; i++) {
|
||||
// @ts-ignore
|
||||
groups[groupsValues[i].categoryFieldId] = groupsValues[i].value;
|
||||
}
|
||||
body.interests = groups;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string);
|
||||
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string);
|
||||
const groupJson = validateJSON(this.getNodeParameter('groupJson', i) as string);
|
||||
|
||||
if (locationJson) {
|
||||
body.location = locationJson;
|
||||
}
|
||||
if (mergeFieldsJson) {
|
||||
body.merge_fields = mergeFieldsJson;
|
||||
}
|
||||
if (groupJson) {
|
||||
body.interests = groupJson;
|
||||
}
|
||||
}
|
||||
responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'PUT', body);
|
||||
}
|
||||
@@ -1536,6 +1884,7 @@ export class Mailchimp implements INodeType {
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,25 @@ export class MailchimpTrigger implements INodeType {
|
||||
{
|
||||
name: 'mailchimpApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'apiKey',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mailchimpOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -50,6 +68,23 @@ export class MailchimpTrigger implements INodeType {
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Key',
|
||||
value: 'apiKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiKey',
|
||||
description: 'Method of authentication.',
|
||||
},
|
||||
{
|
||||
displayName: 'List',
|
||||
name: 'list',
|
||||
|
||||
@@ -62,7 +62,7 @@ export class Mattermost implements INodeType {
|
||||
},
|
||||
],
|
||||
default: 'message',
|
||||
description: 'The resource to operate on.',
|
||||
description: 'The resource to operate on',
|
||||
},
|
||||
|
||||
|
||||
@@ -95,22 +95,22 @@ export class Mattermost implements INodeType {
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Soft-deletes a channel',
|
||||
description: 'Soft delete a channel',
|
||||
},
|
||||
{
|
||||
name: 'Member',
|
||||
value: 'members',
|
||||
description: 'Get a page of members for a channel.',
|
||||
description: 'Get a page of members for a channel',
|
||||
},
|
||||
{
|
||||
name: 'Restore',
|
||||
value: 'restore',
|
||||
description: 'Restores a soft-deleted channel',
|
||||
description: 'Restores a soft deleted channel',
|
||||
},
|
||||
{
|
||||
name: 'Statistics',
|
||||
value: 'statistics',
|
||||
description: 'Get statistics for a channel.',
|
||||
description: 'Get statistics for a channel',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
@@ -131,7 +131,7 @@ export class Mattermost implements INodeType {
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Soft deletes a post, by marking the post as deleted in the database.',
|
||||
description: 'Soft delete a post, by marking the post as deleted in the database',
|
||||
},
|
||||
{
|
||||
name: 'Post',
|
||||
@@ -140,7 +140,7 @@ export class Mattermost implements INodeType {
|
||||
},
|
||||
],
|
||||
default: 'post',
|
||||
description: 'The operation to perform.',
|
||||
description: 'The operation to perform',
|
||||
},
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ export class Mattermost implements INodeType {
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The non-unique UI name for the channel.',
|
||||
description: 'The non-unique UI name for the channel',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
@@ -210,7 +210,7 @@ export class Mattermost implements INodeType {
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The unique handle for the channel, will be present in the channel URL.',
|
||||
description: 'The unique handle for the channel, will be present in the channel URL',
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
@@ -264,7 +264,7 @@ export class Mattermost implements INodeType {
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The ID of the channel to soft-delete.',
|
||||
description: 'The ID of the channel to soft delete',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
@@ -226,6 +226,94 @@ export const contactFields = [
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'addressUi',
|
||||
placeholder: 'Address',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'addressValues',
|
||||
displayName: 'Address',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Address Line 1',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Address Line 2',
|
||||
name: 'address2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Zip Code',
|
||||
name: 'zipCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'B2B or B2C',
|
||||
name: 'b2bOrb2c',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'B2B',
|
||||
value: 'B2B',
|
||||
},
|
||||
{
|
||||
name: 'B2C',
|
||||
value: 'B2C',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'CRM ID',
|
||||
name: 'crmId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Fax',
|
||||
name: 'fax',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Has Purchased',
|
||||
name: 'hasPurchased',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'IP Address',
|
||||
name: 'ipAddress',
|
||||
@@ -240,6 +328,12 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'Date/time in UTC;',
|
||||
},
|
||||
{
|
||||
displayName: 'Mobile',
|
||||
name: 'mobile',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner ID',
|
||||
name: 'ownerId',
|
||||
@@ -247,6 +341,112 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'ID of a Mautic user to assign this contact to',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Prospect or Customer',
|
||||
name: 'prospectOrCustomer',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Prospect',
|
||||
value: 'Prospect',
|
||||
},
|
||||
{
|
||||
name: 'Customer',
|
||||
value: 'Customer',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Sandbox',
|
||||
name: 'sandbox',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Stage',
|
||||
name: 'stage',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getStages',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Social Media',
|
||||
name: 'socialMediaUi',
|
||||
placeholder: 'Social Media',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'socialMediaValues',
|
||||
displayName: 'Social Media',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Facebook',
|
||||
name: 'facebook',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Foursquare',
|
||||
name: 'foursquare',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Instagram',
|
||||
name: 'instagram',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'LinkedIn',
|
||||
name: 'linkedIn',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Skype',
|
||||
name: 'skype',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Twitter',
|
||||
name: 'twitter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -318,6 +518,103 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'Contact parameters',
|
||||
},
|
||||
{
|
||||
displayName: 'Address',
|
||||
name: 'addressUi',
|
||||
placeholder: 'Address',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'addressValues',
|
||||
displayName: 'Address',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Address Line 1',
|
||||
name: 'address1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Address Line 2',
|
||||
name: 'address2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'City',
|
||||
name: 'city',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'State',
|
||||
name: 'state',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Zip Code',
|
||||
name: 'zipCode',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'B2B or B2C',
|
||||
name: 'b2bOrb2c',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'B2B',
|
||||
value: 'B2B',
|
||||
},
|
||||
{
|
||||
name: 'B2C',
|
||||
value: 'B2C',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'CRM ID',
|
||||
name: 'crmId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
@@ -332,6 +629,19 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'Email address of the contact.',
|
||||
},
|
||||
{
|
||||
displayName: 'Fax',
|
||||
name: 'fax',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
@@ -346,6 +656,47 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'First Name',
|
||||
},
|
||||
{
|
||||
displayName: 'Has Purchased',
|
||||
name: 'hasPurchased',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'IP Address',
|
||||
name: 'ipAddress',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'IP address to associate with the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Active',
|
||||
name: 'lastActive',
|
||||
type: 'dateTime',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Date/time in UTC;',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
@@ -360,6 +711,60 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'LastName',
|
||||
},
|
||||
{
|
||||
displayName: 'Mobile',
|
||||
name: 'mobile',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner ID',
|
||||
name: 'ownerId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of a Mautic user to assign this contact to',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Position',
|
||||
name: 'position',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Position',
|
||||
},
|
||||
{
|
||||
displayName: 'Primary Company',
|
||||
name: 'company',
|
||||
@@ -378,9 +783,9 @@ export const contactFields = [
|
||||
description: 'Primary company',
|
||||
},
|
||||
{
|
||||
displayName: 'Position',
|
||||
name: 'position',
|
||||
type: 'string',
|
||||
displayName: 'Prospect or Customer',
|
||||
name: 'prospectOrCustomer',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
@@ -388,8 +793,62 @@ export const contactFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Prospect',
|
||||
value: 'Prospect',
|
||||
},
|
||||
{
|
||||
name: 'Customer',
|
||||
value: 'Customer',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Sandbox',
|
||||
name: 'sandbox',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Stage',
|
||||
name: 'stage',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getStages',
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
default: '',
|
||||
description: 'Position',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
@@ -405,27 +864,94 @@ export const contactFields = [
|
||||
default: '',
|
||||
description: 'Title',
|
||||
},
|
||||
{
|
||||
displayName: 'Social Media',
|
||||
name: 'socialMediaUi',
|
||||
placeholder: 'Social Media',
|
||||
type: 'fixedCollection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'socialMediaValues',
|
||||
displayName: 'Social Media',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Facebook',
|
||||
name: 'facebook',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Foursquare',
|
||||
name: 'foursquare',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Instagram',
|
||||
name: 'instagram',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'LinkedIn',
|
||||
name: 'linkedIn',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Skype',
|
||||
name: 'skype',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Twitter',
|
||||
name: 'twitter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Website',
|
||||
name: 'website',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'IP Address',
|
||||
name: 'ipAddress',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/jsonParameters': [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'IP address to associate with the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Active',
|
||||
name: 'lastActive',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'Date/time in UTC;',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner ID',
|
||||
name: 'ownerId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'ID of a Mautic user to assign this contact to',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
import { errors } from 'imap-simple';
|
||||
|
||||
interface OMauticErrorResponse {
|
||||
errors: Array<{
|
||||
@@ -19,7 +18,7 @@ interface OMauticErrorResponse {
|
||||
}>;
|
||||
}
|
||||
|
||||
function getErrors(error: OMauticErrorResponse): string {
|
||||
export function getErrors(error: OMauticErrorResponse): string {
|
||||
const returnErrors: string[] = [];
|
||||
|
||||
for (const errorItem of error.errors) {
|
||||
@@ -31,23 +30,40 @@ function getErrors(error: OMauticErrorResponse): string {
|
||||
|
||||
|
||||
export async function mauticApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('mauticApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0, 'credentials') as string;
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: { Authorization: `Basic ${base64Key}` },
|
||||
headers: {},
|
||||
method,
|
||||
qs: query,
|
||||
uri: uri || `${credentials.url}/api${endpoint}`,
|
||||
uri: uri || `/api${endpoint}`,
|
||||
body,
|
||||
json: true
|
||||
};
|
||||
try {
|
||||
const returnData = await this.helpers.request!(options);
|
||||
|
||||
if (returnData.error) {
|
||||
try {
|
||||
|
||||
let returnData;
|
||||
|
||||
if (authenticationMethod === 'credentials') {
|
||||
const credentials = this.getCredentials('mauticApi') as IDataObject;
|
||||
|
||||
const base64Key = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
|
||||
|
||||
options.headers!.Authorization = `Basic ${base64Key}`;
|
||||
|
||||
options.uri = `${credentials.url}${options.uri}`;
|
||||
//@ts-ignore
|
||||
returnData = await this.helpers.request(options);
|
||||
} else {
|
||||
const credentials = this.getCredentials('mauticOAuth2Api') as IDataObject;
|
||||
|
||||
options.uri = `${credentials.url}${options.uri}`;
|
||||
//@ts-ignore
|
||||
returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options);
|
||||
}
|
||||
|
||||
if (returnData.errors) {
|
||||
// They seem to to sometimes return 200 status but still error.
|
||||
throw new Error(getErrors(returnData));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { snakeCase } from 'change-case';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
@@ -15,12 +13,18 @@ import {
|
||||
mauticApiRequest,
|
||||
mauticApiRequestAllItems,
|
||||
validateJSON,
|
||||
getErrors,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
contactFields,
|
||||
contactOperations,
|
||||
} from './ContactDescription';
|
||||
|
||||
import {
|
||||
snakeCase,
|
||||
} from 'change-case';
|
||||
|
||||
export class Mautic implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Mautic',
|
||||
@@ -40,9 +44,43 @@ export class Mautic implements INodeType {
|
||||
{
|
||||
name: 'mauticApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'credentials',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mauticOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Credentials',
|
||||
value: 'credentials',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'credentials',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
@@ -77,6 +115,32 @@ export class Mautic implements INodeType {
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
// Get all the available tags to display them to user so that he can
|
||||
// select them easily
|
||||
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const tags = await mauticApiRequestAllItems.call(this, 'tags', 'GET', '/tags');
|
||||
for (const tag of tags) {
|
||||
returnData.push({
|
||||
name: tag.tag,
|
||||
value: tag.tag,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
// Get all the available stages to display them to user so that he can
|
||||
// select them easily
|
||||
async getStages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const stages = await mauticApiRequestAllItems.call(this, 'stages', 'GET', '/stages');
|
||||
for (const stage of stages) {
|
||||
returnData.push({
|
||||
name: stage.name,
|
||||
value: stage.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -124,6 +188,62 @@ export class Mautic implements INodeType {
|
||||
if (additionalFields.ownerId) {
|
||||
body.ownerId = additionalFields.ownerId as string;
|
||||
}
|
||||
if (additionalFields.addressUi) {
|
||||
const addressValues = (additionalFields.addressUi as IDataObject).addressValues as IDataObject;
|
||||
if (addressValues) {
|
||||
body.address1 = addressValues.address1 as string;
|
||||
body.address2 = addressValues.address2 as string;
|
||||
body.city = addressValues.city as string;
|
||||
body.state = addressValues.state as string;
|
||||
body.country = addressValues.country as string;
|
||||
body.zipcode = addressValues.zipCode as string;
|
||||
}
|
||||
}
|
||||
if (additionalFields.socialMediaUi) {
|
||||
const socialMediaValues = (additionalFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject;
|
||||
if (socialMediaValues) {
|
||||
body.facebook = socialMediaValues.facebook as string;
|
||||
body.foursquare = socialMediaValues.foursquare as string;
|
||||
body.instagram = socialMediaValues.instagram as string;
|
||||
body.linkedin = socialMediaValues.linkedIn as string;
|
||||
body.skype = socialMediaValues.skype as string;
|
||||
body.twitter = socialMediaValues.twitter as string;
|
||||
}
|
||||
}
|
||||
if (additionalFields.b2bOrb2c) {
|
||||
body.b2b_or_b2c = additionalFields.b2bOrb2c as string;
|
||||
}
|
||||
if (additionalFields.crmId) {
|
||||
body.crm_id = additionalFields.crmId as string;
|
||||
}
|
||||
if (additionalFields.fax) {
|
||||
body.fax = additionalFields.fax as string;
|
||||
}
|
||||
if (additionalFields.hasPurchased) {
|
||||
body.haspurchased = additionalFields.hasPurchased as boolean;
|
||||
}
|
||||
if (additionalFields.mobile) {
|
||||
body.mobile = additionalFields.mobile as string;
|
||||
}
|
||||
if (additionalFields.phone) {
|
||||
body.phone = additionalFields.phone as string;
|
||||
}
|
||||
if (additionalFields.prospectOrCustomer) {
|
||||
body.prospect_or_customer = additionalFields.prospectOrCustomer as string;
|
||||
}
|
||||
if (additionalFields.sandbox) {
|
||||
body.sandbox = additionalFields.sandbox as boolean;
|
||||
}
|
||||
if (additionalFields.stage) {
|
||||
body.stage = additionalFields.stage as string;
|
||||
}
|
||||
if (additionalFields.tags) {
|
||||
body.tags = additionalFields.tags as string;
|
||||
}
|
||||
if (additionalFields.website) {
|
||||
body.website = additionalFields.website as string;
|
||||
}
|
||||
|
||||
responseData = await mauticApiRequest.call(this, 'POST', '/contacts/new', body);
|
||||
responseData = responseData.contact;
|
||||
}
|
||||
@@ -167,6 +287,61 @@ export class Mautic implements INodeType {
|
||||
if (updateFields.ownerId) {
|
||||
body.ownerId = updateFields.ownerId as string;
|
||||
}
|
||||
if (updateFields.addressUi) {
|
||||
const addressValues = (updateFields.addressUi as IDataObject).addressValues as IDataObject;
|
||||
if (addressValues) {
|
||||
body.address1 = addressValues.address1 as string;
|
||||
body.address2 = addressValues.address2 as string;
|
||||
body.city = addressValues.city as string;
|
||||
body.state = addressValues.state as string;
|
||||
body.country = addressValues.country as string;
|
||||
body.zipcode = addressValues.zipCode as string;
|
||||
}
|
||||
}
|
||||
if (updateFields.socialMediaUi) {
|
||||
const socialMediaValues = (updateFields.socialMediaUi as IDataObject).socialMediaValues as IDataObject;
|
||||
if (socialMediaValues) {
|
||||
body.facebook = socialMediaValues.facebook as string;
|
||||
body.foursquare = socialMediaValues.foursquare as string;
|
||||
body.instagram = socialMediaValues.instagram as string;
|
||||
body.linkedin = socialMediaValues.linkedIn as string;
|
||||
body.skype = socialMediaValues.skype as string;
|
||||
body.twitter = socialMediaValues.twitter as string;
|
||||
}
|
||||
}
|
||||
if (updateFields.b2bOrb2c) {
|
||||
body.b2b_or_b2c = updateFields.b2bOrb2c as string;
|
||||
}
|
||||
if (updateFields.crmId) {
|
||||
body.crm_id = updateFields.crmId as string;
|
||||
}
|
||||
if (updateFields.fax) {
|
||||
body.fax = updateFields.fax as string;
|
||||
}
|
||||
if (updateFields.hasPurchased) {
|
||||
body.haspurchased = updateFields.hasPurchased as boolean;
|
||||
}
|
||||
if (updateFields.mobile) {
|
||||
body.mobile = updateFields.mobile as string;
|
||||
}
|
||||
if (updateFields.phone) {
|
||||
body.phone = updateFields.phone as string;
|
||||
}
|
||||
if (updateFields.prospectOrCustomer) {
|
||||
body.prospect_or_customer = updateFields.prospectOrCustomer as string;
|
||||
}
|
||||
if (updateFields.sandbox) {
|
||||
body.sandbox = updateFields.sandbox as boolean;
|
||||
}
|
||||
if (updateFields.stage) {
|
||||
body.stage = updateFields.stage as string;
|
||||
}
|
||||
if (updateFields.tags) {
|
||||
body.tags = updateFields.tags as string;
|
||||
}
|
||||
if (updateFields.website) {
|
||||
body.website = updateFields.website as string;
|
||||
}
|
||||
responseData = await mauticApiRequest.call(this, 'PATCH', `/contacts/${contactId}/edit`, body);
|
||||
responseData = responseData.contact;
|
||||
}
|
||||
@@ -193,6 +368,9 @@ export class Mautic implements INodeType {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
qs.start = 0;
|
||||
responseData = await mauticApiRequest.call(this, 'GET', '/contacts', {}, qs);
|
||||
if (responseData.errors) {
|
||||
throw new Error(getErrors(responseData));
|
||||
}
|
||||
responseData = responseData.contacts;
|
||||
responseData = Object.values(responseData);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,25 @@ export class MauticTrigger implements INodeType {
|
||||
{
|
||||
name: 'mauticApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'credentials',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mauticOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -49,6 +67,22 @@ export class MauticTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Credentials',
|
||||
value: 'credentials',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'credentials',
|
||||
},
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
|
||||
64
packages/nodes-base/nodes/MessageBird/GenericFunctions.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Make an API request to Message Bird
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function messageBirdApiRequest(
|
||||
this: IHookFunctions | IExecuteFunctions,
|
||||
method: string,
|
||||
resource: string,
|
||||
body: IDataObject,
|
||||
query: IDataObject = {},
|
||||
): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('messageBirdApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials returned!');
|
||||
}
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `AccessKey ${credentials.accessKey}`,
|
||||
},
|
||||
method,
|
||||
qs: query,
|
||||
body,
|
||||
uri: `https://rest.messagebird.com${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request(options);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
throw new Error('The Message Bird credentials are not valid!');
|
||||
}
|
||||
|
||||
if (error.response && error.response.body && error.response.body.errors) {
|
||||
// Try to return the error prettier
|
||||
const errorMessage = error.response.body.errors.map((e: IDataObject) => e.description);
|
||||
|
||||
throw new Error(`MessageBird Error response [${error.statusCode}]: ${errorMessage.join('|')}`);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
364
packages/nodes-base/nodes/MessageBird/MessageBird.node.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
messageBirdApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class MessageBird implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'MessageBird',
|
||||
name: 'messageBird',
|
||||
icon: 'file:messagebird.png',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Sending SMS',
|
||||
defaults: {
|
||||
name: 'MessageBird',
|
||||
color: '#2481d7',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'messageBirdApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'SMS',
|
||||
value: 'sms',
|
||||
},
|
||||
],
|
||||
default: 'sms',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sms',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Send',
|
||||
value: 'send',
|
||||
description: 'Send text messages (SMS)',
|
||||
},
|
||||
],
|
||||
default: 'send',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// sms:send
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'From',
|
||||
name: 'originator',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '14155238886',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'send',
|
||||
],
|
||||
resource: [
|
||||
'sms',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The number from which to send the message.',
|
||||
},
|
||||
{
|
||||
displayName: 'To',
|
||||
name: 'recipients',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '14155238886/+14155238886',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'send',
|
||||
],
|
||||
resource: [
|
||||
'sms',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'All recipients separated by commas.',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'send',
|
||||
],
|
||||
resource: [
|
||||
'sms',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The message to be send.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Fields',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Created Date-time',
|
||||
name: 'createdDatetime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'The date and time of the creation of the message in RFC3339 format (Y-m-dTH:i:sP).',
|
||||
},
|
||||
{
|
||||
displayName: 'Datacoding',
|
||||
name: 'datacoding',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Auto',
|
||||
value: 'auto',
|
||||
},
|
||||
{
|
||||
name: 'Plain',
|
||||
value: 'plain',
|
||||
},
|
||||
{
|
||||
name: 'Unicode',
|
||||
value: 'unicode',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Using unicode will limit the maximum number of characters to 70 instead of 160.',
|
||||
},
|
||||
{
|
||||
displayName: 'Gateway',
|
||||
name: 'gateway',
|
||||
type: 'number',
|
||||
default: '',
|
||||
description: 'The SMS route that is used to send the message.',
|
||||
},
|
||||
{
|
||||
displayName: 'Group IDs',
|
||||
name: 'groupIds',
|
||||
placeholder: '1,2',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Group IDs separated by commas, If provided recipients can be omitted.',
|
||||
},
|
||||
{
|
||||
displayName: 'Message Type',
|
||||
name: 'mclass',
|
||||
type: 'options',
|
||||
placeholder: 'Permissible values from 0-3',
|
||||
options: [
|
||||
{
|
||||
name: 'Flash',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: 'Normal',
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
default: 1,
|
||||
description: 'Indicated the message type. 1 is a normal message, 0 is a flash message.',
|
||||
},
|
||||
{
|
||||
displayName: 'Reference',
|
||||
name: 'reference',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A client reference.',
|
||||
},
|
||||
{
|
||||
displayName: 'Report Url',
|
||||
name: 'reportUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The status report URL to be used on a per-message basis.<br /> Reference is required for a status report webhook to be sent.',
|
||||
},
|
||||
{
|
||||
displayName: 'Scheduled Date-time',
|
||||
name: 'scheduledDatetime',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'The scheduled date and time of the message in RFC3339 format (Y-m-dTH:i:sP).',
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Binary',
|
||||
value: 'binary',
|
||||
},
|
||||
{
|
||||
name: 'Flash',
|
||||
value: 'flash',
|
||||
},
|
||||
{
|
||||
name: 'SMS',
|
||||
value: 'sms',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'The type of message.<br /> Values can be: sms, binary, or flash.',
|
||||
},
|
||||
{
|
||||
displayName: 'Type Details',
|
||||
name: 'typeDetails',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A hash with extra information.<br /> Is only used when a binary message is sent.',
|
||||
},
|
||||
{
|
||||
displayName: 'Validity',
|
||||
name: 'validity',
|
||||
type: 'number',
|
||||
default: 1,
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
description: 'The amount of seconds that the message is valid.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let operation: string;
|
||||
let resource: string;
|
||||
|
||||
// For POST
|
||||
let bodyRequest: IDataObject;
|
||||
// For Query string
|
||||
let qs: IDataObject;
|
||||
|
||||
let requestMethod;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
qs = {};
|
||||
|
||||
resource = this.getNodeParameter('resource', i) as string;
|
||||
operation = this.getNodeParameter('operation', i) as string;
|
||||
|
||||
if (resource === 'sms') {
|
||||
//https://developers.messagebird.com/api/sms-messaging/#sms-api
|
||||
if (operation === 'send') {
|
||||
// ----------------------------------
|
||||
// sms:send
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'POST';
|
||||
const originator = this.getNodeParameter('originator', i) as string;
|
||||
const body = this.getNodeParameter('message', i) as string;
|
||||
|
||||
bodyRequest = {
|
||||
recipients: [],
|
||||
originator,
|
||||
body
|
||||
};
|
||||
const additionalFields = this.getNodeParameter(
|
||||
'additionalFields',
|
||||
i
|
||||
) as IDataObject;
|
||||
|
||||
if (additionalFields.groupIds) {
|
||||
bodyRequest.groupIds = additionalFields.groupIds as string;
|
||||
}
|
||||
if (additionalFields.type) {
|
||||
bodyRequest.type = additionalFields.type as string;
|
||||
}
|
||||
if (additionalFields.reference) {
|
||||
bodyRequest.reference = additionalFields.reference as string;
|
||||
}
|
||||
if (additionalFields.reportUrl) {
|
||||
bodyRequest.reportUrl = additionalFields.reportUrl as string;
|
||||
}
|
||||
if (additionalFields.validity) {
|
||||
bodyRequest.validity = additionalFields.reportUrl as number;
|
||||
}
|
||||
if (additionalFields.gateway) {
|
||||
bodyRequest.gateway = additionalFields.gateway as string;
|
||||
}
|
||||
if (additionalFields.typeDetails) {
|
||||
bodyRequest.typeDetails = additionalFields.typeDetails as string;
|
||||
}
|
||||
if (additionalFields.datacoding) {
|
||||
bodyRequest.datacoding = additionalFields.datacoding as string;
|
||||
}
|
||||
if (additionalFields.mclass) {
|
||||
bodyRequest.mclass = additionalFields.mclass as number;
|
||||
}
|
||||
if (additionalFields.scheduledDatetime) {
|
||||
bodyRequest.scheduledDatetime = additionalFields.scheduledDatetime as string;
|
||||
}
|
||||
if (additionalFields.createdDatetime) {
|
||||
bodyRequest.createdDatetime = additionalFields.createdDatetime as string;
|
||||
}
|
||||
|
||||
const receivers = this.getNodeParameter('recipients', i) as string;
|
||||
|
||||
bodyRequest.recipients = receivers.split(',').map(item => {
|
||||
return parseInt(item, 10);
|
||||
});
|
||||
} else {
|
||||
throw new Error(`The operation "${operation}" is not known!`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`The resource "${resource}" is not known!`);
|
||||
}
|
||||
|
||||
const responseData = await messageBirdApiRequest.call(
|
||||
this,
|
||||
requestMethod,
|
||||
'/messages',
|
||||
bodyRequest,
|
||||
qs
|
||||
);
|
||||
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/MessageBird/messagebird.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
144
packages/nodes-base/nodes/Microsoft/Sql/GenericFunctions.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import { ITables } from './TableInterface';
|
||||
|
||||
/**
|
||||
* Returns a copy of the item which only contains the json data and
|
||||
* of that only the defined properties
|
||||
*
|
||||
* @param {INodeExecutionData} item The item to copy
|
||||
* @param {string[]} properties The properties it should include
|
||||
* @returns
|
||||
*/
|
||||
export function copyInputItem(
|
||||
item: INodeExecutionData,
|
||||
properties: string[],
|
||||
): IDataObject {
|
||||
// Prepare the data to insert and copy it to be returned
|
||||
const newItem: IDataObject = {};
|
||||
for (const property of properties) {
|
||||
if (item.json[property] === undefined) {
|
||||
newItem[property] = null;
|
||||
} else {
|
||||
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
|
||||
}
|
||||
}
|
||||
return newItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ITables with the columns for the operations
|
||||
*
|
||||
* @param {INodeExecutionData[]} items The items to extract the tables/columns for
|
||||
* @param {function} getNodeParam getter for the Node's Parameters
|
||||
* @returns {ITables} {tableName: {colNames: [items]}};
|
||||
*/
|
||||
export function createTableStruct(
|
||||
getNodeParam: Function,
|
||||
items: INodeExecutionData[],
|
||||
additionalProperties: string[] = [],
|
||||
keyName?: string,
|
||||
): ITables {
|
||||
return items.reduce((tables, item, index) => {
|
||||
const table = getNodeParam('table', index) as string;
|
||||
const columnString = getNodeParam('columns', index) as string;
|
||||
const columns = columnString.split(',').map(column => column.trim());
|
||||
const itemCopy = copyInputItem(item, columns.concat(additionalProperties));
|
||||
const keyParam = keyName
|
||||
? (getNodeParam(keyName, index) as string)
|
||||
: undefined;
|
||||
if (tables[table] === undefined) {
|
||||
tables[table] = {};
|
||||
}
|
||||
if (tables[table][columnString] === undefined) {
|
||||
tables[table][columnString] = [];
|
||||
}
|
||||
if (keyName) {
|
||||
itemCopy[keyName] = keyParam;
|
||||
}
|
||||
tables[table][columnString].push(itemCopy);
|
||||
return tables;
|
||||
}, {} as ITables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a queue of queries on given ITables.
|
||||
*
|
||||
* @param {ITables} tables The ITables to be processed.
|
||||
* @param {function} buildQueryQueue function that builds the queue of promises
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function executeQueryQueue(
|
||||
tables: ITables,
|
||||
buildQueryQueue: Function,
|
||||
): Promise<any[]> { // tslint:disable-line:no-any
|
||||
return Promise.all(
|
||||
Object.keys(tables).map(table => {
|
||||
const columnsResults = Object.keys(tables[table]).map(columnString => {
|
||||
return Promise.all(
|
||||
buildQueryQueue({
|
||||
table,
|
||||
columnString,
|
||||
items: tables[table][columnString],
|
||||
}),
|
||||
);
|
||||
});
|
||||
return Promise.all(columnsResults);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the values from the item for INSERT
|
||||
*
|
||||
* @param {IDataObject} item The item to extract
|
||||
* @returns {string} (Val1, Val2, ...)
|
||||
*/
|
||||
export function extractValues(item: IDataObject): string {
|
||||
return `(${Object.values(item as any) // tslint:disable-line:no-any
|
||||
.map(val => (typeof val === 'string' ? `'${val}'` : val)) // maybe other types such as dates have to be handled as well
|
||||
.join(',')})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the SET from the item for UPDATE
|
||||
*
|
||||
* @param {IDataObject} item The item to extract from
|
||||
* @param {string[]} columns The columns to update
|
||||
* @returns {string} col1 = val1, col2 = val2
|
||||
*/
|
||||
export function extractUpdateSet(item: IDataObject, columns: string[]): string {
|
||||
return columns
|
||||
.map(
|
||||
column =>
|
||||
`${column} = ${
|
||||
typeof item[column] === 'string' ? `'${item[column]}'` : item[column]
|
||||
}`,
|
||||
)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the WHERE condition from the item for UPDATE
|
||||
*
|
||||
* @param {IDataObject} item The item to extract from
|
||||
* @param {string} key The column name to build the condition with
|
||||
* @returns {string} id = '123'
|
||||
*/
|
||||
export function extractUpdateCondition(item: IDataObject, key: string): string {
|
||||
return `${key} = ${
|
||||
typeof item[key] === 'string' ? `'${item[key]}'` : item[key]
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the WHERE condition from the items for DELETE
|
||||
*
|
||||
* @param {IDataObject[]} items The items to extract the values from
|
||||
* @param {string} key The column name to extract the value from for the delete condition
|
||||
* @returns {string} (Val1, Val2, ...)
|
||||
*/
|
||||
export function extractDeleteValues(items: IDataObject[], key: string): string {
|
||||
return `(${items
|
||||
.map(item => (typeof item[key] === 'string' ? `'${item[key]}'` : item[key]))
|
||||
.join(',')})`;
|
||||
}
|
||||
394
packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { chunk, flatten } from '../../utils/utilities';
|
||||
|
||||
import * as mssql from 'mssql';
|
||||
|
||||
import { ITables } from './TableInterface';
|
||||
|
||||
import {
|
||||
copyInputItem,
|
||||
createTableStruct,
|
||||
executeQueryQueue,
|
||||
extractDeleteValues,
|
||||
extractUpdateCondition,
|
||||
extractUpdateSet,
|
||||
extractValues,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class MicrosoftSql implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Microsoft SQL',
|
||||
name: 'microsoftSql',
|
||||
icon: 'file:mssql.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Gets, add and update data in Microsoft SQL.',
|
||||
defaults: {
|
||||
name: 'Microsoft SQL',
|
||||
color: '#1d4bab',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'microsoftSql',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Execute Query',
|
||||
value: 'executeQuery',
|
||||
description: 'Executes a SQL query.',
|
||||
},
|
||||
{
|
||||
name: 'Insert',
|
||||
value: 'insert',
|
||||
description: 'Insert rows in database.',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Updates rows in database.',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Deletes rows in database.',
|
||||
},
|
||||
],
|
||||
default: 'insert',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['executeQuery'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'SELECT id, name FROM product WHERE id < 40',
|
||||
required: true,
|
||||
description: 'The SQL query to execute.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to insert data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'id,name,description',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for the new rows.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to update data in',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Key',
|
||||
name: 'updateKey',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
required: true,
|
||||
description:
|
||||
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'name,description',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for rows to update.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to delete data.',
|
||||
},
|
||||
{
|
||||
displayName: 'Delete Key',
|
||||
name: 'deleteKey',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['delete'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
required: true,
|
||||
description:
|
||||
'Name of the property which decides which rows in the database should be deleted. Normally that would be "id".',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = this.getCredentials('microsoftSql');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const config = {
|
||||
server: credentials.server as string,
|
||||
port: credentials.port as number,
|
||||
database: credentials.database as string,
|
||||
user: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
domain: credentials.domain ? (credentials.domain as string) : undefined,
|
||||
};
|
||||
|
||||
const pool = new mssql.ConnectionPool(config);
|
||||
await pool.connect();
|
||||
|
||||
let returnItems = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
try {
|
||||
if (operation === 'executeQuery') {
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const rawQuery = this.getNodeParameter('query', 0) as string;
|
||||
|
||||
const queryResult = await pool.request().query(rawQuery);
|
||||
|
||||
const result =
|
||||
queryResult.recordsets.length > 1
|
||||
? flatten(queryResult.recordsets)
|
||||
: queryResult.recordsets[0];
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(result as IDataObject[]);
|
||||
} else if (operation === 'insert') {
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
|
||||
const tables = createTableStruct(this.getNodeParameter, items);
|
||||
await executeQueryQueue(
|
||||
tables,
|
||||
({
|
||||
table,
|
||||
columnString,
|
||||
items,
|
||||
}: {
|
||||
table: string;
|
||||
columnString: string;
|
||||
items: IDataObject[];
|
||||
}): Array<Promise<object>> => {
|
||||
return chunk(items, 1000).map(insertValues => {
|
||||
const values = insertValues
|
||||
.map((item: IDataObject) => extractValues(item))
|
||||
.join(',');
|
||||
|
||||
return pool
|
||||
.request()
|
||||
.query(
|
||||
`INSERT INTO ${table}(${columnString}) VALUES ${values};`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
returnItems = items;
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
|
||||
const updateKeys = items.map(
|
||||
(item, index) => this.getNodeParameter('updateKey', index) as string,
|
||||
);
|
||||
const tables = createTableStruct(
|
||||
this.getNodeParameter,
|
||||
items,
|
||||
['updateKey'].concat(updateKeys),
|
||||
'updateKey',
|
||||
);
|
||||
await executeQueryQueue(
|
||||
tables,
|
||||
({
|
||||
table,
|
||||
columnString,
|
||||
items,
|
||||
}: {
|
||||
table: string;
|
||||
columnString: string;
|
||||
items: IDataObject[];
|
||||
}): Array<Promise<object>> => {
|
||||
return items.map(item => {
|
||||
const columns = columnString
|
||||
.split(',')
|
||||
.map(column => column.trim());
|
||||
|
||||
const setValues = extractUpdateSet(item, columns);
|
||||
const condition = extractUpdateCondition(
|
||||
item,
|
||||
item.updateKey as string,
|
||||
);
|
||||
|
||||
return pool
|
||||
.request()
|
||||
.query(`UPDATE ${table} SET ${setValues} WHERE ${condition};`);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
returnItems = items;
|
||||
} else if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
|
||||
const tables = items.reduce((tables, item, index) => {
|
||||
const table = this.getNodeParameter('table', index) as string;
|
||||
const deleteKey = this.getNodeParameter('deleteKey', index) as string;
|
||||
if (tables[table] === undefined) {
|
||||
tables[table] = {};
|
||||
}
|
||||
if (tables[table][deleteKey] === undefined) {
|
||||
tables[table][deleteKey] = [];
|
||||
}
|
||||
tables[table][deleteKey].push(item);
|
||||
return tables;
|
||||
}, {} as ITables);
|
||||
|
||||
const queriesResults = await Promise.all(
|
||||
Object.keys(tables).map(table => {
|
||||
const deleteKeyResults = Object.keys(tables[table]).map(
|
||||
deleteKey => {
|
||||
const deleteItemsList = chunk(
|
||||
tables[table][deleteKey].map(item =>
|
||||
copyInputItem(item as INodeExecutionData, [deleteKey]),
|
||||
),
|
||||
1000,
|
||||
);
|
||||
const queryQueue = deleteItemsList.map(deleteValues => {
|
||||
return pool
|
||||
.request()
|
||||
.query(
|
||||
`DELETE FROM ${table} WHERE ${deleteKey} IN ${extractDeleteValues(
|
||||
deleteValues,
|
||||
deleteKey,
|
||||
)};`,
|
||||
);
|
||||
});
|
||||
return Promise.all(queryQueue);
|
||||
},
|
||||
);
|
||||
return Promise.all(deleteKeyResults);
|
||||
}),
|
||||
);
|
||||
|
||||
const rowsDeleted = flatten(queriesResults).reduce(
|
||||
(acc: number, resp: mssql.IResult<object>): number =>
|
||||
(acc += resp.rowsAffected.reduce((sum, val) => (sum += val))),
|
||||
0,
|
||||
);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray({
|
||||
rowsDeleted,
|
||||
} as IDataObject);
|
||||
} else {
|
||||
await pool.close();
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.continueOnFail() === true) {
|
||||
returnItems = items;
|
||||
} else {
|
||||
await pool.close();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
await pool.close();
|
||||
|
||||
return this.prepareOutputData(returnItems);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
export interface ITables {
|
||||
[key: string]: {
|
||||
[key: string]: IDataObject[];
|
||||
};
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Microsoft/Sql/mssql.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
@@ -15,6 +15,21 @@ export const boardItemOperations = [
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Update',
|
||||
value: 'addUpdate',
|
||||
description: `Add an update to an item.`,
|
||||
},
|
||||
{
|
||||
name: 'Change Column Value',
|
||||
value: 'changeColumnValue',
|
||||
description: 'Change a column value for a board item',
|
||||
},
|
||||
{
|
||||
name: 'Change Multiple Column Values',
|
||||
value: 'changeMultipleColumnValues',
|
||||
description: 'Change multiple column values for a board item',
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
@@ -48,6 +63,192 @@ export const boardItemOperations = [
|
||||
|
||||
export const boardItemFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* boardItem:addUpdate */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Item ID',
|
||||
name: 'itemId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'addUpdate',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The unique identifier of the item to add update to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Text',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'addUpdate',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The update text to add.',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* boardItem:changeColumnValue */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Board ID',
|
||||
name: 'boardId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getBoards',
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeColumnValue',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The unique identifier of the board.',
|
||||
},
|
||||
{
|
||||
displayName: 'Item ID',
|
||||
name: 'itemId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeColumnValue',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The unique identifier of the item to to change column of.',
|
||||
},
|
||||
{
|
||||
displayName: 'Column ID',
|
||||
name: 'columnId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getColumns',
|
||||
loadOptionsDependsOn: [
|
||||
'boardId'
|
||||
],
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeColumnValue',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: `The column's unique identifier.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'json',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeColumnValue',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The column value in JSON format. Documentation can be found <a href="https://monday.com/developers/v2#mutations-section-columns-change-column-value" target="_blank">here</a>.',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* boardItem:changeMultipleColumnValues */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Board ID',
|
||||
name: 'boardId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getBoards',
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeMultipleColumnValues',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The unique identifier of the board.',
|
||||
},
|
||||
{
|
||||
displayName: 'Item ID',
|
||||
name: 'itemId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeMultipleColumnValues',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: `Item's ID`
|
||||
},
|
||||
{
|
||||
displayName: 'Column Values',
|
||||
name: 'columnValues',
|
||||
type: 'json',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'boardItem',
|
||||
],
|
||||
operation: [
|
||||
'changeMultipleColumnValues',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'The column fields and values in JSON format. Documentation can be found <a href="https://monday.com/developers/v2#mutations-section-columns-change-multiple-column-values" target="_blank">here</a>.',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* boardItem:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
@@ -455,6 +455,84 @@ export class MondayCom implements INodeType {
|
||||
}
|
||||
}
|
||||
if (resource === 'boardItem') {
|
||||
if (operation === 'addUpdate') {
|
||||
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
|
||||
const value = this.getNodeParameter('value', i) as string;
|
||||
|
||||
const body: IGraphqlBody = {
|
||||
query:
|
||||
`mutation ($itemId: Int!, $value: String!) {
|
||||
create_update (item_id: $itemId, body: $value) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
itemId,
|
||||
value,
|
||||
},
|
||||
};
|
||||
|
||||
responseData = await mondayComApiRequest.call(this, body);
|
||||
responseData = responseData.data.create_update;
|
||||
}
|
||||
if (operation === 'changeColumnValue') {
|
||||
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
|
||||
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
|
||||
const columnId = this.getNodeParameter('columnId', i) as string;
|
||||
const value = this.getNodeParameter('value', i) as string;
|
||||
|
||||
const body: IGraphqlBody = {
|
||||
query:
|
||||
`mutation ($boardId: Int!, $itemId: Int!, $columnId: String!, $value: JSON!) {
|
||||
change_column_value (board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
boardId,
|
||||
itemId,
|
||||
columnId,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
throw new Error('Custom Values must be a valid JSON');
|
||||
}
|
||||
body.variables.value = JSON.stringify(JSON.parse(value));
|
||||
|
||||
responseData = await mondayComApiRequest.call(this, body);
|
||||
responseData = responseData.data.change_column_value;
|
||||
}
|
||||
if (operation === 'changeMultipleColumnValues') {
|
||||
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
|
||||
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
|
||||
const columnValues = this.getNodeParameter('columnValues', i) as string;
|
||||
|
||||
const body: IGraphqlBody = {
|
||||
query:
|
||||
`mutation ($boardId: Int!, $itemId: Int!, $columnValues: JSON!) {
|
||||
change_multiple_column_values (board_id: $boardId, item_id: $itemId, column_values: $columnValues) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
variables: {
|
||||
boardId,
|
||||
itemId,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
JSON.parse(columnValues);
|
||||
} catch (e) {
|
||||
throw new Error('Custom Values must be a valid JSON');
|
||||
}
|
||||
body.variables.columnValues = JSON.stringify(JSON.parse(columnValues));
|
||||
|
||||
responseData = await mondayComApiRequest.call(this, body);
|
||||
responseData = responseData.data.change_multiple_column_values;
|
||||
}
|
||||
if (operation === 'create') {
|
||||
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
|
||||
const groupId = this.getNodeParameter('groupId', i) as string;
|
||||
|
||||
@@ -32,7 +32,18 @@ export class MongoDb implements INodeType {
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
if (operation === 'find') {
|
||||
if (operation === 'delete') {
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
|
||||
const { deletedCount } = await mdb
|
||||
.collection(this.getNodeParameter('collection', 0) as string)
|
||||
.deleteMany(JSON.parse(this.getNodeParameter('query', 0) as string));
|
||||
|
||||
returnItems = this.helpers.returnJsonArray([{ deletedCount }]);
|
||||
|
||||
} else if (operation === 'find') {
|
||||
// ----------------------------------
|
||||
// find
|
||||
// ----------------------------------
|
||||
|
||||
@@ -28,6 +28,11 @@ export const nodeDescription: INodeTypeDescription = {
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete documents.'
|
||||
},
|
||||
{
|
||||
name: 'Find',
|
||||
value: 'find',
|
||||
@@ -57,13 +62,36 @@ export const nodeDescription: INodeTypeDescription = {
|
||||
description: 'MongoDB Collection'
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// delete
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Delete Query (JSON format)',
|
||||
name: 'query',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 5
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'delete'
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '{}',
|
||||
placeholder: `{ "birth": { "$gt": "1950-01-01" } }`,
|
||||
required: true,
|
||||
description: 'MongoDB Delete query.'
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// find
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Query (JSON format)',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 5
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@ export class Msg91 implements INodeType {
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'From',
|
||||
displayName: 'Sender ID',
|
||||
name: 'from',
|
||||
type: 'string',
|
||||
default: '',
|
||||
|
||||
63
packages/nodes-base/nodes/NextCloud/GenericFunctions.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
/**
|
||||
* Make an API request to NextCloud
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function nextCloudApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object | string | Buffer, headers?: object, encoding?: null | undefined, query?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const options : OptionsWithUri = {
|
||||
headers,
|
||||
method,
|
||||
body,
|
||||
qs: {},
|
||||
uri: '',
|
||||
json: false,
|
||||
};
|
||||
|
||||
if (encoding === null) {
|
||||
options.encoding = null;
|
||||
}
|
||||
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
try {
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
const credentials = this.getCredentials('nextCloudApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.auth = {
|
||||
user: credentials.user as string,
|
||||
pass: credentials.password as string,
|
||||
};
|
||||
|
||||
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
|
||||
|
||||
return await this.helpers.request(options);
|
||||
} else {
|
||||
const credentials = this.getCredentials('nextCloudOAuth2Api');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.uri = `${credentials.webDavUrl}/${encodeURI(endpoint)}`;
|
||||
|
||||
return await this.helpers.requestOAuth2!.call(this, 'nextCloudOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`NextCloud Error. Status Code: ${error.statusCode}. Message: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
@@ -9,9 +10,13 @@ import {
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { parseString } from 'xml2js';
|
||||
import { OptionsWithUri } from 'request';
|
||||
import {
|
||||
parseString,
|
||||
} from 'xml2js';
|
||||
|
||||
import {
|
||||
nextCloudApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class NextCloud implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -24,7 +29,7 @@ export class NextCloud implements INodeType {
|
||||
description: 'Access data on NextCloud',
|
||||
defaults: {
|
||||
name: 'NextCloud',
|
||||
color: '#22BB44',
|
||||
color: '#1cafff',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
@@ -32,9 +37,44 @@ export class NextCloud implements INodeType {
|
||||
{
|
||||
name: 'nextCloudApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'nextCloudOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
@@ -446,7 +486,14 @@ export class NextCloud implements INodeType {
|
||||
const items = this.getInputData().slice();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const credentials = this.getCredentials('nextCloudApi');
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
let credentials;
|
||||
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
credentials = this.getCredentials('nextCloudApi');
|
||||
} else {
|
||||
credentials = this.getCredentials('nextCloudOAuth2Api');
|
||||
}
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
@@ -562,26 +609,14 @@ export class NextCloud implements INodeType {
|
||||
webDavUrl = webDavUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
auth: {
|
||||
user: credentials.user as string,
|
||||
pass: credentials.password as string,
|
||||
},
|
||||
headers,
|
||||
method: requestMethod,
|
||||
body,
|
||||
qs: {},
|
||||
uri: `${credentials.webDavUrl}/${encodeURI(endpoint)}`,
|
||||
json: false,
|
||||
};
|
||||
|
||||
let encoding = undefined;
|
||||
if (resource === 'file' && operation === 'download') {
|
||||
// Return the data as a buffer
|
||||
options.encoding = null;
|
||||
encoding = null;
|
||||
}
|
||||
|
||||
try {
|
||||
responseData = await this.helpers.request(options);
|
||||
responseData = await nextCloudApiRequest.call(this, requestMethod, endpoint, body, headers, encoding);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail() === true) {
|
||||
returnData.push({ error });
|
||||
|
||||
@@ -213,20 +213,20 @@ export class OpenWeatherMap implements INodeType {
|
||||
// Set base data
|
||||
qs = {
|
||||
APPID: credentials.accessToken,
|
||||
units: this.getNodeParameter('format', 0) as string
|
||||
units: this.getNodeParameter('format', i) as string
|
||||
};
|
||||
|
||||
// Get the location
|
||||
locationSelection = this.getNodeParameter('locationSelection', 0) as string;
|
||||
locationSelection = this.getNodeParameter('locationSelection', i) as string;
|
||||
if (locationSelection === 'cityName') {
|
||||
qs.q = this.getNodeParameter('cityName', 0) as string;
|
||||
qs.q = this.getNodeParameter('cityName', i) as string;
|
||||
} else if (locationSelection === 'cityId') {
|
||||
qs.id = this.getNodeParameter('cityId', 0) as number;
|
||||
qs.id = this.getNodeParameter('cityId', i) as number;
|
||||
} else if (locationSelection === 'coordinates') {
|
||||
qs.lat = this.getNodeParameter('latitude', 0) as string;
|
||||
qs.lon = this.getNodeParameter('longitude', 0) as string;
|
||||
qs.lat = this.getNodeParameter('latitude', i) as string;
|
||||
qs.lon = this.getNodeParameter('longitude', i) as string;
|
||||
} else if (locationSelection === 'zipCode') {
|
||||
qs.zip = this.getNodeParameter('zipCode', 0) as string;
|
||||
qs.zip = this.getNodeParameter('zipCode', i) as string;
|
||||
} else {
|
||||
throw new Error(`The locationSelection "${locationSelection}" is not known!`);
|
||||
}
|
||||
|
||||
@@ -19,16 +19,11 @@ import {
|
||||
|
||||
export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const credentials = this.getCredentials('pagerDutyApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
Accept: 'application/vnd.pagerduty+json;version=2',
|
||||
Authorization: `Token token=${credentials.apiToken}`,
|
||||
Accept: 'application/vnd.pagerduty+json;version=2'
|
||||
},
|
||||
method,
|
||||
body,
|
||||
@@ -39,15 +34,30 @@ export async function pagerDutyApiRequest(this: IExecuteFunctions | IWebhookFunc
|
||||
arrayFormat: 'brackets',
|
||||
},
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.form;
|
||||
}
|
||||
if (!Object.keys(query).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
options.headers = Object.assign({}, options.headers, headers);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'apiToken') {
|
||||
const credentials = this.getCredentials('pagerDutyApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers!['Authorization'] = `Token token=${credentials.apiToken}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2!.call(this, 'pagerDutyOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response && error.response.body && error.response.body.error && error.response.body.error.errors) {
|
||||
// Try to return the error prettier
|
||||
|
||||
@@ -66,9 +66,43 @@ export class PagerDuty implements INodeType {
|
||||
{
|
||||
name: 'pagerDutyApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'apiToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pagerDutyOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Token',
|
||||
value: 'apiToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiToken',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
|
||||
@@ -5,10 +5,16 @@ import {
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { OptionsWithUri } from 'request';
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> master
|
||||
|
||||
export interface ICustomInterface {
|
||||
name: string;
|
||||
@@ -32,10 +38,27 @@ export interface ICustomProperties {
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, formData?: IDataObject, downloadFile?: boolean): Promise<any> { // tslint:disable-line:no-any
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
=======
|
||||
export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, formData?: IDataObject, downloadFile?: boolean): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('pipedriveApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
if (query === undefined) {
|
||||
query = {};
|
||||
}
|
||||
|
||||
query.api_token = credentials.apiToken;
|
||||
>>>>>>> master
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
method,
|
||||
qs: query,
|
||||
uri: `https://api.pipedrive.com/v1${endpoint}`,
|
||||
@@ -62,7 +85,8 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
|
||||
let responseData;
|
||||
|
||||
try {
|
||||
if (authenticationMethod === 'basicAuth') {
|
||||
<<<<<<< HEAD
|
||||
if (authenticationMethod === 'basicAuth' || authenticationMethod === 'apiToken') {
|
||||
|
||||
const credentials = this.getCredentials('pipedriveApi');
|
||||
if (credentials === undefined) {
|
||||
@@ -76,6 +100,10 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
|
||||
} else {
|
||||
responseData = await this.helpers.requestOAuth2!.call(this, 'pipedriveOAuth2Api', options);
|
||||
}
|
||||
=======
|
||||
//@ts-ignore
|
||||
const responseData = await this.helpers.request(options);
|
||||
>>>>>>> master
|
||||
|
||||
if (downloadFile === true) {
|
||||
return {
|
||||
@@ -99,7 +127,7 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio
|
||||
|
||||
if (error.response && error.response.body && error.response.body.error) {
|
||||
// Try to return the error prettier
|
||||
let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error}`;
|
||||
let errorMessage = `Pipedrive error response [${error.statusCode}]: ${error.response.body.error.message}`;
|
||||
if (error.response.body.error_info) {
|
||||
errorMessage += ` - ${error.response.body.error_info}`;
|
||||
}
|
||||
@@ -128,7 +156,7 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
|
||||
if (query === undefined) {
|
||||
query = {};
|
||||
}
|
||||
query.limit = 500;
|
||||
query.limit = 100;
|
||||
query.start = 0;
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
@@ -137,7 +165,12 @@ export async function pipedriveApiRequestAllItems(this: IHookFunctions | IExecut
|
||||
|
||||
do {
|
||||
responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query);
|
||||
returnData.push.apply(returnData, responseData.data);
|
||||
// the search path returns data diferently
|
||||
if (responseData.data.items) {
|
||||
returnData.push.apply(returnData, responseData.data.items);
|
||||
} else {
|
||||
returnData.push.apply(returnData, responseData.data);
|
||||
}
|
||||
|
||||
query.start = responseData.additionalData.pagination.next_start;
|
||||
} while (
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodePropertyOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
@@ -23,7 +25,6 @@ interface CustomProperty {
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add the additional fields to the body
|
||||
*
|
||||
@@ -64,7 +65,7 @@ export class Pipedrive implements INodeType {
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'basicAuth',
|
||||
'apiToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -88,19 +89,15 @@ export class Pipedrive implements INodeType {
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth'
|
||||
name: 'API Token',
|
||||
value: 'apiToken'
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
],
|
||||
default: 'basicAuth',
|
||||
default: 'apiToken',
|
||||
description: 'Method of authentication.',
|
||||
},
|
||||
{
|
||||
@@ -399,6 +396,11 @@ export class Pipedrive implements INodeType {
|
||||
value: 'getAll',
|
||||
description: 'Get data of all persons',
|
||||
},
|
||||
{
|
||||
name: 'Search',
|
||||
value: 'search',
|
||||
description: 'Search all persons',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
@@ -2058,6 +2060,7 @@ export class Pipedrive implements INodeType {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'search',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -2072,6 +2075,7 @@ export class Pipedrive implements INodeType {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'search',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
@@ -2086,9 +2090,143 @@ export class Pipedrive implements INodeType {
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// person:getAll
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'person',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Filter ID',
|
||||
name: 'filterId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getFilters',
|
||||
},
|
||||
default: '',
|
||||
description: 'ID of the filter to use.',
|
||||
},
|
||||
{
|
||||
displayName: 'First Char',
|
||||
name: 'firstChar',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'If supplied, only persons whose name starts with the specified letter will be returned ',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// person:search
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Term',
|
||||
name: 'term',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'search',
|
||||
],
|
||||
resource: [
|
||||
'person',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The search term to look for. Minimum 2 characters (or 1 if using exact_match).',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'search',
|
||||
],
|
||||
resource: [
|
||||
'person',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Exact Match',
|
||||
name: 'exactMatch',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'When enabled, only full exact matches against the given term are returned. It is not case sensitive.',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields',
|
||||
name: 'fields',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A comma-separated string array. The fields to perform the search from. Defaults to all of them.',
|
||||
},
|
||||
{
|
||||
displayName: 'Include Fields',
|
||||
name: 'includeFields',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Supports including optional fields in the results which are not provided by default.',
|
||||
},
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Will filter Deals by the provided Organization ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'RAW Data',
|
||||
name: 'rawData',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `Returns the data exactly in the way it got received from the API.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the filters to display them to user so that he can
|
||||
// select them easily
|
||||
async getFilters(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { data } = await pipedriveApiRequest.call(this, 'GET', '/filters', {}, { type: 'people' });
|
||||
for (const filter of data) {
|
||||
const filterName = filter.name;
|
||||
const filterId = filter.id;
|
||||
returnData.push({
|
||||
name: filterName,
|
||||
value: filterId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
@@ -2492,8 +2630,51 @@ export class Pipedrive implements INodeType {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
if (additionalFields.filterId) {
|
||||
qs.filter_id = additionalFields.filterId as string;
|
||||
}
|
||||
|
||||
if (additionalFields.firstChar) {
|
||||
qs.first_char = additionalFields.firstChar as string;
|
||||
}
|
||||
|
||||
endpoint = `/persons`;
|
||||
|
||||
} else if (operation === 'search') {
|
||||
// ----------------------------------
|
||||
// persons:search
|
||||
// ----------------------------------
|
||||
|
||||
requestMethod = 'GET';
|
||||
|
||||
qs.term = this.getNodeParameter('term', i) as string;
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
if (returnAll === false) {
|
||||
qs.limit = this.getNodeParameter('limit', i) as number;
|
||||
}
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
if (additionalFields.fields) {
|
||||
qs.fields = additionalFields.fields as string;
|
||||
}
|
||||
|
||||
if (additionalFields.exactMatch) {
|
||||
qs.exact_match = additionalFields.exactMatch as boolean;
|
||||
}
|
||||
|
||||
if (additionalFields.organizationId) {
|
||||
qs.organization_id = parseInt(additionalFields.organizationId as string, 10);
|
||||
}
|
||||
|
||||
if (additionalFields.includeFields) {
|
||||
qs.include_fields = additionalFields.includeFields as string;
|
||||
}
|
||||
|
||||
endpoint = `/persons/search`;
|
||||
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// person:update
|
||||
@@ -2530,7 +2711,9 @@ export class Pipedrive implements INodeType {
|
||||
|
||||
let responseData;
|
||||
if (returnAll === true) {
|
||||
|
||||
responseData = await pipedriveApiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
} else {
|
||||
|
||||
if (customProperties !== undefined) {
|
||||
@@ -2538,6 +2721,13 @@ export class Pipedrive implements INodeType {
|
||||
}
|
||||
|
||||
responseData = await pipedriveApiRequest.call(this, requestMethod, endpoint, body, qs, formData, downloadFile);
|
||||
|
||||
<<<<<<< HEAD
|
||||
if (responseData.data === null) {
|
||||
responseData.data = [];
|
||||
}
|
||||
=======
|
||||
>>>>>>> master
|
||||
}
|
||||
|
||||
if (resource === 'file' && operation === 'download') {
|
||||
@@ -2559,6 +2749,24 @@ export class Pipedrive implements INodeType {
|
||||
|
||||
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(responseData.data);
|
||||
} else {
|
||||
|
||||
if (responseData.data === null) {
|
||||
responseData.data = [];
|
||||
}
|
||||
|
||||
if (operation === 'search' && responseData.data && responseData.data.items) {
|
||||
responseData.data = responseData.data.items;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
if (additionalFields.rawData !== true) {
|
||||
responseData.data = responseData.data.map((item: { result_score: number, item: object }) => {
|
||||
return {
|
||||
result_score: item.result_score,
|
||||
...item.item,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData.data)) {
|
||||
returnData.push.apply(returnData, responseData.data as IDataObject[]);
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
} from './GenericFunctions';
|
||||
|
||||
import * as basicAuth from 'basic-auth';
|
||||
import { Response } from 'express';
|
||||
|
||||
import {
|
||||
Response,
|
||||
} from 'express';
|
||||
|
||||
function authorizationError(resp: Response, realm: string, responseCode: number, message?: string) {
|
||||
if (message === undefined) {
|
||||
@@ -52,6 +54,10 @@ export class PipedriveTrigger implements INodeType {
|
||||
{
|
||||
name: 'pipedriveApi',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'httpBasicAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
@@ -60,18 +66,6 @@ export class PipedriveTrigger implements INodeType {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pipedriveOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -91,17 +85,13 @@ export class PipedriveTrigger implements INodeType {
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth'
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
value: 'none'
|
||||
},
|
||||
],
|
||||
default: 'basicAuth',
|
||||
description: 'Method of authentication.',
|
||||
default: 'none',
|
||||
description: 'If authentication should be activated for the webhook (makes it more scure).',
|
||||
},
|
||||
{
|
||||
displayName: 'Action',
|
||||
@@ -191,7 +181,6 @@ export class PipedriveTrigger implements INodeType {
|
||||
description: 'Type of object to receive notifications about.',
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
// @ts-ignore (because of request)
|
||||
@@ -288,8 +277,6 @@ export class PipedriveTrigger implements INodeType {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const req = this.getRequestObject();
|
||||
const resp = this.getResponseObject();
|
||||
|
||||
129
packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import pgPromise = require('pg-promise');
|
||||
import pg = require('pg-promise/typescript/pg-subset');
|
||||
|
||||
/**
|
||||
* Returns of copy of the items which only contains the json data and
|
||||
* of that only the define properties
|
||||
*
|
||||
* @param {INodeExecutionData[]} items The items to copy
|
||||
* @param {string[]} properties The properties it should include
|
||||
* @returns
|
||||
*/
|
||||
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
|
||||
// Prepare the data to insert and copy it to be returned
|
||||
let newItem: IDataObject;
|
||||
return items.map(item => {
|
||||
newItem = {};
|
||||
for (const property of properties) {
|
||||
if (item.json[property] === undefined) {
|
||||
newItem[property] = null;
|
||||
} else {
|
||||
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
|
||||
}
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given SQL query on the database.
|
||||
*
|
||||
* @param {Function} getNodeParam The getter for the Node's parameters
|
||||
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
|
||||
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
|
||||
* @param {input[]} input The Node's input data
|
||||
* @returns Promise<Array<object>>
|
||||
*/
|
||||
export function pgQuery(
|
||||
getNodeParam: Function,
|
||||
pgp: pgPromise.IMain<{}, pg.IClient>,
|
||||
db: pgPromise.IDatabase<{}, pg.IClient>,
|
||||
input: INodeExecutionData[],
|
||||
): Promise<object[]> {
|
||||
const queries: string[] = [];
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
queries.push(getNodeParam('query', i) as string);
|
||||
}
|
||||
|
||||
return db.any(pgp.helpers.concat(queries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given items into the database.
|
||||
*
|
||||
* @param {Function} getNodeParam The getter for the Node's parameters
|
||||
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
|
||||
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
|
||||
* @param {INodeExecutionData[]} items The items to be inserted
|
||||
* @returns Promise<Array<IDataObject>>
|
||||
*/
|
||||
export async function pgInsert(
|
||||
getNodeParam: Function,
|
||||
pgp: pgPromise.IMain<{}, pg.IClient>,
|
||||
db: pgPromise.IDatabase<{}, pg.IClient>,
|
||||
items: INodeExecutionData[],
|
||||
): Promise<IDataObject[][]> {
|
||||
const table = getNodeParam('table', 0) as string;
|
||||
const schema = getNodeParam('schema', 0) as string;
|
||||
let returnFields = (getNodeParam('returnFields', 0) as string).split(',') as string[];
|
||||
const columnString = getNodeParam('columns', 0) as string;
|
||||
const columns = columnString.split(',').map(column => column.trim());
|
||||
|
||||
const cs = new pgp.helpers.ColumnSet(columns);
|
||||
|
||||
const te = new pgp.helpers.TableName({ table, schema });
|
||||
|
||||
// Prepare the data to insert and copy it to be returned
|
||||
const insertItems = getItemCopy(items, columns);
|
||||
|
||||
// Generate the multi-row insert query and return the id of new row
|
||||
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
|
||||
const query =
|
||||
pgp.helpers.insert(insertItems, cs, te) +
|
||||
(returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
|
||||
|
||||
// Executing the query to insert the data
|
||||
const insertData = await db.manyOrNone(query);
|
||||
|
||||
return [insertData, insertItems];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given items in the database.
|
||||
*
|
||||
* @param {Function} getNodeParam The getter for the Node's parameters
|
||||
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
|
||||
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
|
||||
* @param {INodeExecutionData[]} items The items to be updated
|
||||
* @returns Promise<Array<IDataObject>>
|
||||
*/
|
||||
export async function pgUpdate(
|
||||
getNodeParam: Function,
|
||||
pgp: pgPromise.IMain<{}, pg.IClient>,
|
||||
db: pgPromise.IDatabase<{}, pg.IClient>,
|
||||
items: INodeExecutionData[],
|
||||
): Promise<IDataObject[]> {
|
||||
const table = getNodeParam('table', 0) as string;
|
||||
const updateKey = getNodeParam('updateKey', 0) as string;
|
||||
const columnString = getNodeParam('columns', 0) as string;
|
||||
|
||||
const columns = columnString.split(',').map(column => column.trim());
|
||||
|
||||
// Make sure that the updateKey does also get queried
|
||||
if (!columns.includes(updateKey)) {
|
||||
columns.unshift(updateKey);
|
||||
}
|
||||
|
||||
// Prepare the data to update and copy it to be returned
|
||||
const updateItems = getItemCopy(items, columns);
|
||||
|
||||
// Generate the multi-row update query
|
||||
const query =
|
||||
pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
|
||||
|
||||
// Executing the query to update the data
|
||||
await db.none(query);
|
||||
|
||||
return updateItems;
|
||||
}
|
||||
@@ -3,36 +3,12 @@ import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypeDescription
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import * as pgPromise from 'pg-promise';
|
||||
|
||||
|
||||
/**
|
||||
* Returns of copy of the items which only contains the json data and
|
||||
* of that only the define properties
|
||||
*
|
||||
* @param {INodeExecutionData[]} items The items to copy
|
||||
* @param {string[]} properties The properties it should include
|
||||
* @returns
|
||||
*/
|
||||
function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
|
||||
// Prepare the data to insert and copy it to be returned
|
||||
let newItem: IDataObject;
|
||||
return items.map((item) => {
|
||||
newItem = {};
|
||||
for (const property of properties) {
|
||||
if (item.json[property] === undefined) {
|
||||
newItem[property] = null;
|
||||
} else {
|
||||
newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
|
||||
}
|
||||
}
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
import { pgInsert, pgQuery, pgUpdate } from './Postgres.node.functions';
|
||||
|
||||
export class Postgres implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -52,7 +28,7 @@ export class Postgres implements INodeType {
|
||||
{
|
||||
name: 'postgres',
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
@@ -63,17 +39,17 @@ export class Postgres implements INodeType {
|
||||
{
|
||||
name: 'Execute Query',
|
||||
value: 'executeQuery',
|
||||
description: 'Executes a SQL query.',
|
||||
description: 'Execute an SQL query',
|
||||
},
|
||||
{
|
||||
name: 'Insert',
|
||||
value: 'insert',
|
||||
description: 'Insert rows in database.',
|
||||
description: 'Insert rows in database',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Updates rows in database.',
|
||||
description: 'Update rows in database',
|
||||
},
|
||||
],
|
||||
default: 'insert',
|
||||
@@ -92,9 +68,7 @@ export class Postgres implements INodeType {
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'executeQuery'
|
||||
],
|
||||
operation: ['executeQuery'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
@@ -103,7 +77,6 @@ export class Postgres implements INodeType {
|
||||
description: 'The SQL query to execute.',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
@@ -113,9 +86,7 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'insert'
|
||||
],
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: 'public',
|
||||
@@ -128,9 +99,7 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'insert'
|
||||
],
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
@@ -143,14 +112,13 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'insert'
|
||||
],
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'id,name,description',
|
||||
description: 'Comma separated list of the properties which should used as columns for the new rows.',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for the new rows.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Fields',
|
||||
@@ -158,16 +126,13 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'insert'
|
||||
],
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '*',
|
||||
description: 'Comma separated list of the fields that the operation will return',
|
||||
},
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
@@ -177,9 +142,7 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update'
|
||||
],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
@@ -192,14 +155,13 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update'
|
||||
],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: 'id',
|
||||
required: true,
|
||||
description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
description:
|
||||
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
@@ -207,22 +169,18 @@ export class Postgres implements INodeType {
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'update'
|
||||
],
|
||||
operation: ['update'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'name,description',
|
||||
description: 'Comma separated list of the properties which should used as columns for rows to update.',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for rows to update.',
|
||||
},
|
||||
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
|
||||
const credentials = this.getCredentials('postgres');
|
||||
|
||||
if (credentials === undefined) {
|
||||
@@ -238,7 +196,7 @@ export class Postgres implements INodeType {
|
||||
user: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
|
||||
sslmode: credentials.ssl as string || 'disable',
|
||||
sslmode: (credentials.ssl as string) || 'disable',
|
||||
};
|
||||
|
||||
const db = pgp(config);
|
||||
@@ -253,39 +211,15 @@ export class Postgres implements INodeType {
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const queries: string[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
queries.push(this.getNodeParameter('query', i) as string);
|
||||
}
|
||||
|
||||
const queryResult = await db.any(pgp.helpers.concat(queries));
|
||||
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
|
||||
|
||||
} else if (operation === 'insert') {
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
|
||||
const table = this.getNodeParameter('table', 0) as string;
|
||||
const schema = this.getNodeParameter('schema', 0) as string;
|
||||
let returnFields = (this.getNodeParameter('returnFields', 0) as string).split(',') as string[];
|
||||
const columnString = this.getNodeParameter('columns', 0) as string;
|
||||
const columns = columnString.split(',').map(column => column.trim());
|
||||
|
||||
const cs = new pgp.helpers.ColumnSet(columns);
|
||||
|
||||
const te = new pgp.helpers.TableName({ table, schema });
|
||||
|
||||
// Prepare the data to insert and copy it to be returned
|
||||
const insertItems = getItemCopy(items, columns);
|
||||
|
||||
// Generate the multi-row insert query and return the id of new row
|
||||
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
|
||||
const query = pgp.helpers.insert(insertItems, cs, te) + (returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
|
||||
|
||||
// Executing the query to insert the data
|
||||
const insertData = await db.manyOrNone(query);
|
||||
const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
// Add the id to the data
|
||||
for (let i = 0; i < insertData.length; i++) {
|
||||
@@ -293,37 +227,17 @@ export class Postgres implements INodeType {
|
||||
json: {
|
||||
...insertData[i],
|
||||
...insertItems[i],
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
} else if (operation === 'update') {
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
|
||||
const table = this.getNodeParameter('table', 0) as string;
|
||||
const updateKey = this.getNodeParameter('updateKey', 0) as string;
|
||||
const columnString = this.getNodeParameter('columns', 0) as string;
|
||||
|
||||
const columns = columnString.split(',').map(column => column.trim());
|
||||
|
||||
// Make sure that the updateKey does also get queried
|
||||
if (!columns.includes(updateKey)) {
|
||||
columns.unshift(updateKey);
|
||||
}
|
||||
|
||||
// Prepare the data to update and copy it to be returned
|
||||
const updateItems = getItemCopy(items, columns);
|
||||
|
||||
// Generate the multi-row update query
|
||||
const query = pgp.helpers.update(updateItems, columns, table) + ' WHERE v.' + updateKey + ' = t.' + updateKey;
|
||||
|
||||
// Executing the query to update the data
|
||||
await db.none(query);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(updateItems as IDataObject[]);
|
||||
const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(updateItems);
|
||||
} else {
|
||||
await pgp.end();
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
|
||||
93
packages/nodes-base/nodes/Postmark/GenericFunctions.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
IHookFunctions,
|
||||
IWebhookFunctions
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export async function postmarkApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method : string, endpoint : string, body: any = {}, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('postmarkApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Postmark-Server-Token' : credentials.serverToken
|
||||
},
|
||||
method,
|
||||
body,
|
||||
uri: 'https://api.postmarkapp.com' + endpoint,
|
||||
json: true
|
||||
};
|
||||
if (body === {}) {
|
||||
delete options.body;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
throw new Error(`Postmark: ${error.statusCode} Message: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: no-any
|
||||
export function convertTriggerObjectToStringArray (webhookObject : any) : string[] {
|
||||
const triggers = webhookObject.Triggers;
|
||||
const webhookEvents : string[] = [];
|
||||
|
||||
// Translate Webhook trigger settings to string array
|
||||
if (triggers.Open.Enabled) {
|
||||
webhookEvents.push('open');
|
||||
}
|
||||
if (triggers.Open.PostFirstOpenOnly) {
|
||||
webhookEvents.push('firstOpen');
|
||||
}
|
||||
if (triggers.Click.Enabled) {
|
||||
webhookEvents.push('click');
|
||||
}
|
||||
if (triggers.Delivery.Enabled) {
|
||||
webhookEvents.push('delivery');
|
||||
}
|
||||
if (triggers.Bounce.Enabled) {
|
||||
webhookEvents.push('bounce');
|
||||
}
|
||||
if (triggers.Bounce.IncludeContent) {
|
||||
webhookEvents.push('includeContent');
|
||||
}
|
||||
if (triggers.SpamComplaint.Enabled) {
|
||||
webhookEvents.push('spamComplaint');
|
||||
}
|
||||
if (triggers.SpamComplaint.IncludeContent) {
|
||||
if (!webhookEvents.includes('IncludeContent')) {
|
||||
webhookEvents.push('includeContent');
|
||||
}
|
||||
}
|
||||
if (triggers.SubscriptionChange.Enabled) {
|
||||
webhookEvents.push('subscriptionChange');
|
||||
}
|
||||
|
||||
return webhookEvents;
|
||||
}
|
||||
|
||||
export function eventExists (currentEvents : string[], webhookEvents: string[]) {
|
||||
for (const currentEvent of currentEvents) {
|
||||
if (!webhookEvents.includes(currentEvent)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
256
packages/nodes-base/nodes/Postmark/PostmarkTrigger.node.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
convertTriggerObjectToStringArray,
|
||||
eventExists,
|
||||
postmarkApiRequest
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class PostmarkTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Postmark Trigger',
|
||||
name: 'postmarkTrigger',
|
||||
icon: 'file:postmark.png',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when Postmark events occur.',
|
||||
defaults: {
|
||||
name: 'Postmark Trigger',
|
||||
color: '#fedd00',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'postmarkApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Events',
|
||||
name: 'events',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: 'Bounce',
|
||||
value: 'bounce',
|
||||
description: 'Trigger on bounce.',
|
||||
},
|
||||
{
|
||||
name: 'Click',
|
||||
value: 'click',
|
||||
description: 'Trigger on click.',
|
||||
},
|
||||
{
|
||||
name: 'Delivery',
|
||||
value: 'delivery',
|
||||
description: 'Trigger on delivery.',
|
||||
},
|
||||
{
|
||||
name: 'Open',
|
||||
value: 'open',
|
||||
description: 'Trigger webhook on open.',
|
||||
},
|
||||
{
|
||||
name: 'Spam Complaint',
|
||||
value: 'spamComplaint',
|
||||
description: 'Trigger on spam complaint.',
|
||||
},
|
||||
{
|
||||
name: 'Subscription Change',
|
||||
value: 'subscriptionChange',
|
||||
description: 'Trigger on subscription change.',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
required: true,
|
||||
description: 'Webhook events that will be enabled for that endpoint.',
|
||||
},
|
||||
{
|
||||
displayName: 'First Open',
|
||||
name: 'firstOpen',
|
||||
description: 'Only fires on first open for event "Open".',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
events: [
|
||||
'open',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Include Content',
|
||||
name: 'includeContent',
|
||||
description: 'Includes message content for events "Bounce" and "Spam Complaint".',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
events: [
|
||||
'bounce',
|
||||
'spamComplaint',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
// @ts-ignore (because of request)
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
const events = this.getNodeParameter('events') as string[];
|
||||
if (this.getNodeParameter('includeContent') as boolean) {
|
||||
events.push('includeContent');
|
||||
}
|
||||
if (this.getNodeParameter('firstOpen') as boolean) {
|
||||
events.push('firstOpen');
|
||||
}
|
||||
|
||||
// Get all webhooks
|
||||
const endpoint = `/webhooks`;
|
||||
|
||||
const responseData = await postmarkApiRequest.call(this, 'GET', endpoint, {});
|
||||
|
||||
// No webhooks exist
|
||||
if (responseData.Webhooks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If webhooks exist, check if any match current settings
|
||||
for (const webhook of responseData.Webhooks) {
|
||||
if (webhook.Url === webhookUrl && eventExists(events, convertTriggerObjectToStringArray(webhook))) {
|
||||
webhookData.webhookId = webhook.ID;
|
||||
// webhook identical to current settings. re-assign webhook id to found webhook.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
const endpoint = `/webhooks`;
|
||||
|
||||
// tslint:disable-next-line: no-any
|
||||
const body : any = {
|
||||
Url: webhookUrl,
|
||||
Triggers: {
|
||||
Open:{
|
||||
Enabled: false,
|
||||
PostFirstOpenOnly: false
|
||||
},
|
||||
Click:{
|
||||
Enabled: false
|
||||
},
|
||||
Delivery:{
|
||||
Enabled: false
|
||||
},
|
||||
Bounce:{
|
||||
Enabled: false,
|
||||
IncludeContent: false
|
||||
},
|
||||
SpamComplaint:{
|
||||
Enabled: false,
|
||||
IncludeContent: false
|
||||
},
|
||||
SubscriptionChange: {
|
||||
Enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const events = this.getNodeParameter('events') as string[];
|
||||
|
||||
if (events.includes('open')) {
|
||||
body.Triggers.Open.Enabled = true;
|
||||
body.Triggers.Open.PostFirstOpenOnly = this.getNodeParameter('firstOpen') as boolean;
|
||||
}
|
||||
if (events.includes('click')) {
|
||||
body.Triggers.Click.Enabled = true;
|
||||
}
|
||||
if (events.includes('delivery')) {
|
||||
body.Triggers.Delivery.Enabled = true;
|
||||
}
|
||||
if (events.includes('bounce')) {
|
||||
body.Triggers.Bounce.Enabled = true;
|
||||
body.Triggers.Bounce.IncludeContent = this.getNodeParameter('includeContent') as boolean;
|
||||
}
|
||||
if (events.includes('spamComplaint')) {
|
||||
body.Triggers.SpamComplaint.Enabled = true;
|
||||
body.Triggers.SpamComplaint.IncludeContent = this.getNodeParameter('includeContent') as boolean;
|
||||
}
|
||||
if (events.includes('subscriptionChange')) {
|
||||
body.Triggers.SubscriptionChange.Enabled = true;
|
||||
}
|
||||
|
||||
const responseData = await postmarkApiRequest.call(this, 'POST', endpoint, body);
|
||||
|
||||
if (responseData.ID === undefined) {
|
||||
// Required data is missing so was not successful
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
webhookData.webhookId = responseData.ID as string;
|
||||
|
||||
return true;
|
||||
},
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
const endpoint = `/webhooks/${webhookData.webhookId}`;
|
||||
const body = {};
|
||||
|
||||
try {
|
||||
await postmarkApiRequest.call(this, 'DELETE', endpoint, body);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from the static workflow data so that it is clear
|
||||
// that no webhooks are registred anymore
|
||||
delete webhookData.webhookId;
|
||||
delete webhookData.webhookEvents;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const req = this.getRequestObject();
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(req.body)
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Postmark/postmark.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
246
packages/nodes-base/nodes/QuestDb/QuestDb.node.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { IExecuteFunctions } from 'n8n-core';
|
||||
import { IDataObject, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
import * as pgPromise from 'pg-promise';
|
||||
|
||||
import { pgInsert, pgQuery, pgUpdate } from '../Postgres/Postgres.node.functions';
|
||||
import { table } from 'console';
|
||||
|
||||
export class QuestDb implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'QuestDB',
|
||||
name: 'questDb',
|
||||
icon: 'file:questdb.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Gets, add and update data in QuestDB.',
|
||||
defaults: {
|
||||
name: 'QuestDB',
|
||||
color: '#2C4A79',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'questDb',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Execute Query',
|
||||
value: 'executeQuery',
|
||||
description: 'Executes a SQL query.',
|
||||
},
|
||||
{
|
||||
name: 'Insert',
|
||||
value: 'insert',
|
||||
description: 'Insert rows in database.',
|
||||
}
|
||||
],
|
||||
default: 'insert',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['executeQuery'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'SELECT id, name FROM product WHERE id < 40',
|
||||
required: true,
|
||||
description: 'The SQL query to execute.',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Schema',
|
||||
name: 'schema',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: 'public',
|
||||
required: true,
|
||||
description: 'Name of the schema the table belongs to',
|
||||
},
|
||||
{
|
||||
displayName: 'Table',
|
||||
name: 'table',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
description: 'Name of the table in which to insert data to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'id,name,description',
|
||||
description:
|
||||
'Comma separated list of the properties which should used as columns for the new rows.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Fields',
|
||||
name: 'returnFields',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['insert'],
|
||||
},
|
||||
},
|
||||
default: '*',
|
||||
description: 'Comma separated list of the fields that the operation will return',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// update
|
||||
// ----------------------------------
|
||||
// {
|
||||
// displayName: 'Table',
|
||||
// name: 'table',
|
||||
// type: 'string',
|
||||
// displayOptions: {
|
||||
// show: {
|
||||
// operation: ['update'],
|
||||
// },
|
||||
// },
|
||||
// default: '',
|
||||
// required: true,
|
||||
// description: 'Name of the table in which to update data in',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Update Key',
|
||||
// name: 'updateKey',
|
||||
// type: 'string',
|
||||
// displayOptions: {
|
||||
// show: {
|
||||
// operation: ['update'],
|
||||
// },
|
||||
// },
|
||||
// default: 'id',
|
||||
// required: true,
|
||||
// description:
|
||||
// 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Columns',
|
||||
// name: 'columns',
|
||||
// type: 'string',
|
||||
// displayOptions: {
|
||||
// show: {
|
||||
// operation: ['update'],
|
||||
// },
|
||||
// },
|
||||
// default: '',
|
||||
// placeholder: 'name,description',
|
||||
// description:
|
||||
// 'Comma separated list of the properties which should used as columns for rows to update.',
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = this.getCredentials('questDb');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const pgp = pgPromise();
|
||||
|
||||
const config = {
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
database: credentials.database as string,
|
||||
user: credentials.user as string,
|
||||
password: credentials.password as string,
|
||||
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
|
||||
sslmode: (credentials.ssl as string) || 'disable',
|
||||
};
|
||||
|
||||
const db = pgp(config);
|
||||
|
||||
let returnItems = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
if (operation === 'executeQuery') {
|
||||
// ----------------------------------
|
||||
// executeQuery
|
||||
// ----------------------------------
|
||||
|
||||
const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
|
||||
} else if (operation === 'insert') {
|
||||
// ----------------------------------
|
||||
// insert
|
||||
// ----------------------------------
|
||||
const tableName = this.getNodeParameter('table', 0) as string;
|
||||
const returnFields = this.getNodeParameter('returnFields', 0) as string;
|
||||
|
||||
let queries : string[] = [];
|
||||
items.map(item => {
|
||||
let columns = Object.keys(item.json);
|
||||
|
||||
let values : string = columns.map((col : string) => {
|
||||
if (typeof item.json[col] === 'string') {
|
||||
return `\'${item.json[col]}\'`;
|
||||
} else {
|
||||
return item.json[col];
|
||||
}
|
||||
}).join(',');
|
||||
|
||||
let query = `INSERT INTO ${tableName} (${columns.join(',')}) VALUES (${values});`;
|
||||
queries.push(query);
|
||||
});
|
||||
|
||||
await db.any(pgp.helpers.concat(queries));
|
||||
|
||||
let returnedItems = await db.any(`SELECT ${returnFields} from ${tableName}`);
|
||||
|
||||
returnItems = this.helpers.returnJsonArray(returnedItems as IDataObject[]);
|
||||
} else {
|
||||
await pgp.end();
|
||||
throw new Error(`The operation "${operation}" is not supported!`);
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
await pgp.end();
|
||||
|
||||
return this.prepareOutputData(returnItems);
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/QuestDb/questdb.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
@@ -402,6 +402,7 @@ export class Redis implements INodeType {
|
||||
} else if (type === 'hash') {
|
||||
const clientHset = util.promisify(client.hset).bind(client);
|
||||
for (const key of Object.keys(value)) {
|
||||
// @ts-ignore
|
||||
await clientHset(keyName, key, (value as IDataObject)[key]!.toString());
|
||||
}
|
||||
} else if (type === 'list') {
|
||||
|
||||
@@ -48,15 +48,15 @@ interface IPostMessageBody {
|
||||
|
||||
export class Rocketchat implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Rocketchat',
|
||||
displayName: 'RocketChat',
|
||||
name: 'rocketchat',
|
||||
icon: 'file:rocketchat.png',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
||||
description: 'Consume Rocketchat API',
|
||||
description: 'Consume RocketChat API',
|
||||
defaults: {
|
||||
name: 'Rocketchat',
|
||||
name: 'RocketChat',
|
||||
color: '#c02428',
|
||||
},
|
||||
inputs: ['main'],
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin
|
||||
}
|
||||
}
|
||||
|
||||
export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
export async function salesforceApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
|
||||
52
packages/nodes-base/nodes/Signl4/GenericFunctions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
/**
|
||||
* Make an API request to SIGNL4
|
||||
*
|
||||
* @param {IHookFunctions | IExecuteFunctions} this
|
||||
* @param {object} message
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
|
||||
export async function SIGNL4ApiRequest(this: IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
uri: uri || ``,
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
if (!Object.keys(query).length) {
|
||||
delete options.qs;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
|
||||
if (error.response && error.response.body && error.response.body.details) {
|
||||
throw new Error(`SIGNL4 error response [${error.statusCode}]: ${error.response.body.details}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
325
packages/nodes-base/nodes/Signl4/Signl4.node.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
BINARY_ENCODING,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IBinaryKeyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
SIGNL4ApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class Signl4 implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'SIGNL4',
|
||||
name: 'signl4',
|
||||
icon: 'file:signl4.png',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume SIGNL4 API.',
|
||||
defaults: {
|
||||
name: 'SIGNL4',
|
||||
color: '#53afe8',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'signl4Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Alert',
|
||||
value: 'alert',
|
||||
},
|
||||
],
|
||||
default: 'alert',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'alert',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Send',
|
||||
value: 'send',
|
||||
description: 'Send an alert.',
|
||||
},
|
||||
],
|
||||
default: 'send',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
default: '',
|
||||
required: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'send',
|
||||
],
|
||||
resource: [
|
||||
'alert',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'A more detailed description for the alert.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'send',
|
||||
],
|
||||
resource: [
|
||||
'alert',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Alerting Scenario',
|
||||
name: 'alertingScenario',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Single ACK',
|
||||
value: 'single_ack',
|
||||
description: 'In case only one person needs to confirm this Signl.'
|
||||
},
|
||||
{
|
||||
name: 'Multi ACK',
|
||||
value: 'multi_ack',
|
||||
description: 'in case this alert must be confirmed by the number of people who are on duty at the time this Singl is raised',
|
||||
},
|
||||
],
|
||||
default: 'single_ack',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
displayName: 'Attachments',
|
||||
name: 'attachmentsUi',
|
||||
placeholder: 'Add Attachments',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'attachmentsBinary',
|
||||
displayName: 'Attachments Binary',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Property Name',
|
||||
name: 'property',
|
||||
type: 'string',
|
||||
placeholder: 'data',
|
||||
default: '',
|
||||
description: 'Name of the binary properties which contain data which should be added as attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
default: {},
|
||||
},
|
||||
{
|
||||
displayName: 'External ID',
|
||||
name: 'externalId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `If the event originates from a record in a 3rd party system, use this parameter to pass <br/>
|
||||
the unique ID of that record. That ID will be communicated in outbound webhook notifications from SIGNL4,<br/>
|
||||
which is great for correlation/synchronization of that record with the alert.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Filtering',
|
||||
name: 'filtering',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: `Specify a boolean value of true or false to apply event filtering for this event, or not. <br/>
|
||||
If set to true, the event will only trigger a notification to the team, if it contains at least one keyword <br/>
|
||||
from one of your services and system categories (i.e. it is whitelisted)`,
|
||||
},
|
||||
{
|
||||
displayName: 'Location',
|
||||
name: 'locationFieldsUi',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Location',
|
||||
default: {},
|
||||
description: 'Transmit location information (\'latitude, longitude\') with your event and display a map in the mobile app.',
|
||||
options: [
|
||||
{
|
||||
name: 'locationFieldsValues',
|
||||
displayName: 'Location',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Latitude',
|
||||
name: 'latitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location latitude.',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Longitude',
|
||||
name: 'longitude',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The location longitude.',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Service',
|
||||
name: 'service',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Assigns the alert to the service/system category with the specified name.',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = (items.length as unknown) as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (resource === 'alert') {
|
||||
//https://connect.signl4.com/webhook/docs/index.html
|
||||
if (operation === 'send') {
|
||||
const message = this.getNodeParameter('message', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields',i) as IDataObject;
|
||||
|
||||
const data: IDataObject = {
|
||||
message,
|
||||
};
|
||||
|
||||
if (additionalFields.alertingScenario) {
|
||||
data['X-S4-AlertingScenario'] = additionalFields.alertingScenario as string;
|
||||
}
|
||||
if (additionalFields.externalId) {
|
||||
data['X-S4-ExternalID'] = additionalFields.externalId as string;
|
||||
}
|
||||
if (additionalFields.filtering) {
|
||||
data['X-S4-Filtering'] = (additionalFields.filtering as boolean).toString();
|
||||
}
|
||||
if (additionalFields.locationFieldsUi) {
|
||||
const locationUi = (additionalFields.locationFieldsUi as IDataObject).locationFieldsValues as IDataObject;
|
||||
if (locationUi) {
|
||||
data['X-S4-Location'] = `${locationUi.latitude},${locationUi.longitude}`;
|
||||
}
|
||||
}
|
||||
if (additionalFields.service) {
|
||||
data['X-S4-Service'] = additionalFields.service as string;
|
||||
}
|
||||
if (additionalFields.title) {
|
||||
data['title'] = additionalFields.title as string;
|
||||
}
|
||||
|
||||
const attachments = additionalFields.attachmentsUi as IDataObject;
|
||||
|
||||
if (attachments) {
|
||||
if (attachments.attachmentsBinary && items[i].binary) {
|
||||
|
||||
const propertyName = (attachments.attachmentsBinary as IDataObject).property as string;
|
||||
|
||||
const binaryProperty = (items[i].binary as IBinaryKeyData)[propertyName];
|
||||
|
||||
if (binaryProperty) {
|
||||
|
||||
const supportedFileExtension = ['png', 'jpg', 'txt'];
|
||||
|
||||
if (!supportedFileExtension.includes(binaryProperty.fileExtension as string)) {
|
||||
|
||||
throw new Error(`Invalid extension, just ${supportedFileExtension.join(',')} are supported}`);
|
||||
}
|
||||
|
||||
data['file'] = {
|
||||
value: Buffer.from(binaryProperty.data, BINARY_ENCODING),
|
||||
options: {
|
||||
filename: binaryProperty.fileName,
|
||||
contentType: binaryProperty.mimeType,
|
||||
},
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new Error(`Binary property ${propertyName} does not exist on input`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = this.getCredentials('signl4Api');
|
||||
|
||||
const endpoint = `https://connect.signl4.com/webhook/${credentials?.teamSecret}`;
|
||||
|
||||
responseData = await SIGNL4ApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'',
|
||||
{},
|
||||
{},
|
||||
endpoint,
|
||||
{
|
||||
formData: {
|
||||
...data,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else if (responseData !== undefined) {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Signl4/signl4.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@@ -91,7 +91,7 @@ export const messageFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Post the message as authenticated user instead of bot.',
|
||||
description: 'Post the message as authenticated user instead of bot. Works only with user token.',
|
||||
},
|
||||
{
|
||||
displayName: 'User Name',
|
||||
@@ -111,7 +111,7 @@ export const messageFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Set the bot\'s user name.',
|
||||
description: 'Set the bot\'s user name. This field will be ignored if you are using a user token.',
|
||||
},
|
||||
{
|
||||
displayName: 'JSON parameters',
|
||||
@@ -504,7 +504,7 @@ export const messageFields = [
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Pass true to update the message as the authed user. Bot users in this context are considered authed users.',
|
||||
description: 'Pass true to update the message as the authed user. Works only with user token.',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
|
||||
@@ -289,7 +289,7 @@ export class Slack implements INodeType {
|
||||
if (operation === 'get') {
|
||||
const channel = this.getNodeParameter('channelId', i) as string;
|
||||
qs.channel = channel,
|
||||
responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs);
|
||||
responseData = await slackApiRequest.call(this, 'POST', '/conversations.info', {}, qs);
|
||||
responseData = responseData.channel;
|
||||
}
|
||||
//https://api.slack.com/methods/conversations.list
|
||||
@@ -452,11 +452,12 @@ export class Slack implements INodeType {
|
||||
}
|
||||
if (body.as_user === false) {
|
||||
body.username = this.getNodeParameter('username', i) as string;
|
||||
delete body.as_user;
|
||||
}
|
||||
|
||||
if (!jsonParameters) {
|
||||
const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[];
|
||||
const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[];
|
||||
const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[];
|
||||
|
||||
// The node does save the fields data differently than the API
|
||||
// expects so fix the data befre we send the request
|
||||
@@ -482,7 +483,7 @@ export class Slack implements INodeType {
|
||||
block.block_id = blockUi.blockId as string;
|
||||
block.type = blockUi.type as string;
|
||||
if (block.type === 'actions') {
|
||||
const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[];
|
||||
const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[];
|
||||
if (elementsUi) {
|
||||
for (const elementUi of elementsUi) {
|
||||
const element: Element = {};
|
||||
@@ -498,7 +499,7 @@ export class Slack implements INodeType {
|
||||
text: elementUi.text as string,
|
||||
type: 'plain_text',
|
||||
emoji: elementUi.emoji as boolean,
|
||||
};
|
||||
};
|
||||
if (elementUi.url) {
|
||||
element.url = elementUi.url as string;
|
||||
}
|
||||
@@ -508,13 +509,13 @@ export class Slack implements INodeType {
|
||||
if (elementUi.style !== 'default') {
|
||||
element.style = elementUi.style as string;
|
||||
}
|
||||
const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject;
|
||||
if (confirmUi) {
|
||||
const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject;
|
||||
if (confirmUi) {
|
||||
const confirm: Confirm = {};
|
||||
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
|
||||
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
|
||||
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
|
||||
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
|
||||
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
|
||||
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
|
||||
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
|
||||
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
|
||||
const style = confirmUi.style as string;
|
||||
if (titleUi) {
|
||||
confirm.title = {
|
||||
@@ -548,13 +549,13 @@ export class Slack implements INodeType {
|
||||
confirm.style = style as string;
|
||||
}
|
||||
element.confirm = confirm;
|
||||
}
|
||||
elements.push(element);
|
||||
}
|
||||
elements.push(element);
|
||||
}
|
||||
block.elements = elements;
|
||||
}
|
||||
} else if (block.type === 'section') {
|
||||
const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject;
|
||||
const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject;
|
||||
if (textUi) {
|
||||
const text: Text = {};
|
||||
if (textUi.type === 'plainText') {
|
||||
@@ -569,7 +570,7 @@ export class Slack implements INodeType {
|
||||
} else {
|
||||
throw new Error('Property text must be defined');
|
||||
}
|
||||
const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[];
|
||||
const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[];
|
||||
if (fieldsUi) {
|
||||
const fields: Text[] = [];
|
||||
for (const fieldUi of fieldsUi) {
|
||||
@@ -589,7 +590,7 @@ export class Slack implements INodeType {
|
||||
block.fields = fields;
|
||||
}
|
||||
}
|
||||
const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject;
|
||||
const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject;
|
||||
if (accessoryUi) {
|
||||
const accessory: Element = {};
|
||||
if (accessoryUi.type === 'button') {
|
||||
@@ -608,46 +609,46 @@ export class Slack implements INodeType {
|
||||
if (accessoryUi.style !== 'default') {
|
||||
accessory.style = accessoryUi.style as string;
|
||||
}
|
||||
const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject;
|
||||
const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject;
|
||||
if (confirmUi) {
|
||||
const confirm: Confirm = {};
|
||||
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
|
||||
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
|
||||
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
|
||||
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
|
||||
const style = confirmUi.style as string;
|
||||
if (titleUi) {
|
||||
confirm.title = {
|
||||
type: 'plain_text',
|
||||
text: titleUi.text as string,
|
||||
emoji: titleUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (textUi) {
|
||||
confirm.text = {
|
||||
type: 'plain_text',
|
||||
text: textUi.text as string,
|
||||
emoji: textUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (confirmTextUi) {
|
||||
confirm.confirm = {
|
||||
type: 'plain_text',
|
||||
text: confirmTextUi.text as string,
|
||||
emoji: confirmTextUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (denyUi) {
|
||||
confirm.deny = {
|
||||
type: 'plain_text',
|
||||
text: denyUi.text as string,
|
||||
emoji: denyUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (style !== 'default') {
|
||||
confirm.style = style as string;
|
||||
}
|
||||
accessory.confirm = confirm;
|
||||
const confirm: Confirm = {};
|
||||
const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject;
|
||||
const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject;
|
||||
const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject;
|
||||
const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject;
|
||||
const style = confirmUi.style as string;
|
||||
if (titleUi) {
|
||||
confirm.title = {
|
||||
type: 'plain_text',
|
||||
text: titleUi.text as string,
|
||||
emoji: titleUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (textUi) {
|
||||
confirm.text = {
|
||||
type: 'plain_text',
|
||||
text: textUi.text as string,
|
||||
emoji: textUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (confirmTextUi) {
|
||||
confirm.confirm = {
|
||||
type: 'plain_text',
|
||||
text: confirmTextUi.text as string,
|
||||
emoji: confirmTextUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (denyUi) {
|
||||
confirm.deny = {
|
||||
type: 'plain_text',
|
||||
text: denyUi.text as string,
|
||||
emoji: denyUi.emoji as boolean,
|
||||
};
|
||||
}
|
||||
if (style !== 'default') {
|
||||
confirm.style = style as string;
|
||||
}
|
||||
accessory.confirm = confirm;
|
||||
}
|
||||
}
|
||||
block.accessory = accessory;
|
||||
@@ -691,10 +692,6 @@ export class Slack implements INodeType {
|
||||
ts,
|
||||
};
|
||||
|
||||
if (authentication === 'accessToken') {
|
||||
body.as_user = this.getNodeParameter('as_user', i) as boolean;
|
||||
}
|
||||
|
||||
// The node does save the fields data differently than the API
|
||||
// expects so fix the data befre we send the request
|
||||
for (const attachment of attachments) {
|
||||
@@ -790,8 +787,8 @@ export class Slack implements INodeType {
|
||||
if (binaryData) {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
if (items[i].binary === undefined
|
||||
//@ts-ignore
|
||||
|| items[i].binary[binaryPropertyName] === undefined) {
|
||||
//@ts-ignore
|
||||
|| items[i].binary[binaryPropertyName] === undefined) {
|
||||
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
|
||||
}
|
||||
body.file = {
|
||||
@@ -804,7 +801,7 @@ export class Slack implements INodeType {
|
||||
contentType: items[i].binary[binaryPropertyName].mimeType,
|
||||
}
|
||||
};
|
||||
responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body });
|
||||
responseData = await slackApiRequest.call(this, 'POST', '/files.upload', {}, qs, { 'Content-Type': 'multipart/form-data' }, { formData: body });
|
||||
responseData = responseData.file;
|
||||
} else {
|
||||
const fileContent = this.getNodeParameter('fileContent', i) as string;
|
||||
|
||||
84
packages/nodes-base/nodes/Spotify/GenericFunctions.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { OptionsWithUri } from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Make an API request to Spotify
|
||||
*
|
||||
* @param {IHookFunctions} this
|
||||
* @param {string} method
|
||||
* @param {string} url
|
||||
* @param {object} body
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions,
|
||||
method: string, endpoint: string, body: object, query?: object, uri?: string): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'n8n',
|
||||
'Content-Type': 'text/plain',
|
||||
'Accept': ' application/json',
|
||||
},
|
||||
body,
|
||||
qs: query,
|
||||
uri: uri || `https://api.spotify.com/v1${endpoint}`,
|
||||
json: true
|
||||
};
|
||||
|
||||
try {
|
||||
const credentials = this.getCredentials('spotifyOAuth2Api');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
throw new Error('The Spotify credentials are not valid!');
|
||||
}
|
||||
|
||||
if (error.error && error.error.error && error.error.error.message) {
|
||||
// Try to return the error prettier
|
||||
throw new Error(`Spotify error response [${error.error.error.status}]: ${error.error.error.message}`);
|
||||
}
|
||||
|
||||
// If that data does not exist for some reason return the actual error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteFunctions,
|
||||
propertyName: string, method: string, endpoint: string, body: object, query?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
|
||||
let uri: string | undefined;
|
||||
|
||||
do {
|
||||
responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri);
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
uri = responseData.next;
|
||||
|
||||
} while (
|
||||
responseData['next'] !== null
|
||||
);
|
||||
|
||||
return returnData;
|
||||
}
|
||||
816
packages/nodes-base/nodes/Spotify/Spotify.node.ts
Normal file
@@ -0,0 +1,816 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
spotifyApiRequest,
|
||||
spotifyApiRequestAllItems,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class Spotify implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Spotify',
|
||||
name: 'spotify',
|
||||
icon: 'file:spotify.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Access public song data via the Spotify API.',
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
defaults: {
|
||||
name: 'Spotify',
|
||||
color: '#1DB954',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'spotifyOAuth2Api',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
// ----------------------------------------------------------
|
||||
// Resource to Operate on
|
||||
// Player, Album, Artisits, Playlists, Tracks
|
||||
// ----------------------------------------------------------
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Album',
|
||||
value: 'album',
|
||||
},
|
||||
{
|
||||
name: 'Artist',
|
||||
value: 'artist',
|
||||
},
|
||||
{
|
||||
name: 'Player',
|
||||
value: 'player',
|
||||
},
|
||||
{
|
||||
name: 'Playlist',
|
||||
value: 'playlist',
|
||||
},
|
||||
{
|
||||
name: 'Track',
|
||||
value: 'track',
|
||||
},
|
||||
],
|
||||
default: 'player',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
// Player Operations
|
||||
// Pause, Play, Get Recently Played, Get Currently Playing, Next Song, Previous Song, Add to Queue
|
||||
// --------------------------------------------------------------------------------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'player',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Song to Queue',
|
||||
value: 'addSongToQueue',
|
||||
description: 'Add a song to your queue.'
|
||||
},
|
||||
{
|
||||
name: 'Currently Playing',
|
||||
value: 'currentlyPlaying',
|
||||
description: 'Get your currently playing track.'
|
||||
},
|
||||
{
|
||||
name: 'Next Song',
|
||||
value: 'nextSong',
|
||||
description: 'Skip to your next track.'
|
||||
},
|
||||
{
|
||||
name: 'Pause',
|
||||
value: 'pause',
|
||||
description: 'Pause your music.',
|
||||
},
|
||||
{
|
||||
name: 'Previous Song',
|
||||
value: 'previousSong',
|
||||
description: 'Skip to your previous song.'
|
||||
},
|
||||
{
|
||||
name: 'Recently Played',
|
||||
value: 'recentlyPlayed',
|
||||
description: 'Get your recently played tracks.'
|
||||
},
|
||||
{
|
||||
name: 'Start Music',
|
||||
value: 'startMusic',
|
||||
description: 'Start playing a playlist, artist, or album.'
|
||||
},
|
||||
],
|
||||
default: 'addSongToQueue',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'player'
|
||||
],
|
||||
operation: [
|
||||
'startMusic',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp',
|
||||
description: `Enter a playlist, artist, or album URI or ID.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'player'
|
||||
],
|
||||
operation: [
|
||||
'addSongToQueue',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
|
||||
description: `Enter a track URI or ID.`,
|
||||
},
|
||||
// -----------------------------------------------
|
||||
// Album Operations
|
||||
// Get an Album, Get an Album's Tracks
|
||||
// -----------------------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'album',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get an album by URI or ID.',
|
||||
},
|
||||
{
|
||||
name: `Get Tracks`,
|
||||
value: 'getTracks',
|
||||
description: `Get an album's tracks by URI or ID.`,
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Album ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'album',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp',
|
||||
description: `The album's Spotify URI or ID.`,
|
||||
},
|
||||
// -------------------------------------------------------------------------------------------------------------
|
||||
// Artist Operations
|
||||
// Get an Artist, Get an Artist's Related Artists, Get an Artist's Top Tracks, Get an Artist's Albums
|
||||
// -------------------------------------------------------------------------------------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'artist',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get an artist by URI or ID.',
|
||||
},
|
||||
{
|
||||
name: `Get Albums`,
|
||||
value: 'getAlbums',
|
||||
description: `Get an artist's albums by URI or ID.`,
|
||||
},
|
||||
{
|
||||
name: `Get Related Artists`,
|
||||
value: 'getRelatedArtists',
|
||||
description: `Get an artist's related artists by URI or ID.`,
|
||||
},
|
||||
{
|
||||
name: `Get Top Tracks`,
|
||||
value: 'getTopTracks',
|
||||
description: `Get an artist's top tracks by URI or ID.`,
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Artist ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'artist',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U',
|
||||
description: `The artist's Spotify URI or ID.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Country',
|
||||
name: 'country',
|
||||
type: 'string',
|
||||
default: 'US',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'artist'
|
||||
],
|
||||
operation: [
|
||||
'getTopTracks',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'US',
|
||||
description: `Top tracks in which country? Enter the postal abbriviation.`,
|
||||
},
|
||||
// -------------------------------------------------------------------------------------------------------------
|
||||
// Playlist Operations
|
||||
// Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists
|
||||
// -------------------------------------------------------------------------------------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'playlist',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add an Item',
|
||||
value: 'add',
|
||||
description: 'Add tracks from a playlist by track and playlist URI or ID.',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a playlist by URI or ID.',
|
||||
},
|
||||
{
|
||||
name: 'Get Tracks',
|
||||
value: 'getTracks',
|
||||
description: `Get a playlist's tracks by URI or ID.`,
|
||||
},
|
||||
{
|
||||
name: `Get the User's Playlists`,
|
||||
value: 'getUserPlaylists',
|
||||
description: `Get a user's playlists.`,
|
||||
},
|
||||
{
|
||||
name: 'Remove an Item',
|
||||
value: 'delete',
|
||||
description: 'Remove tracks from a playlist by track and playlist URI or ID.',
|
||||
},
|
||||
],
|
||||
default: 'add',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Playlist ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'playlist',
|
||||
],
|
||||
operation: [
|
||||
'add',
|
||||
'delete',
|
||||
'get',
|
||||
'getTracks',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:playlist:37i9dQZF1DWUhI3iC1khPH',
|
||||
description: `The playlist's Spotify URI or its ID.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
name: 'trackID',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'playlist',
|
||||
],
|
||||
operation: [
|
||||
'add',
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
|
||||
description: `The track's Spotify URI or its ID. The track to add/delete from the playlist.`,
|
||||
},
|
||||
// -----------------------------------------------------
|
||||
// Track Operations
|
||||
// Get a Track, Get a Track's Audio Features
|
||||
// -----------------------------------------------------
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'track',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a track by its URI or ID.',
|
||||
},
|
||||
{
|
||||
name: 'Get Audio Features',
|
||||
value: 'getAudioFeatures',
|
||||
description: 'Get audio features for a track by URI or ID.',
|
||||
},
|
||||
],
|
||||
default: 'track',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
{
|
||||
displayName: 'Track ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'track',
|
||||
],
|
||||
},
|
||||
},
|
||||
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
|
||||
description: `The track's Spotify URI or ID.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'album',
|
||||
'artist',
|
||||
'playlist',
|
||||
],
|
||||
operation: [
|
||||
'getTracks',
|
||||
'getAlbums',
|
||||
'getUserPlaylists',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: `The number of items to return.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'album',
|
||||
'artist',
|
||||
'playlist'
|
||||
],
|
||||
operation: [
|
||||
'getTracks',
|
||||
'getAlbums',
|
||||
'getUserPlaylists',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
description: `The number of items to return.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'player',
|
||||
],
|
||||
operation: [
|
||||
'recentlyPlayed',
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 50,
|
||||
},
|
||||
description: `The number of items to return.`,
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
// Get all of the incoming input data to loop through
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
// For Post
|
||||
let body: IDataObject;
|
||||
// For Query string
|
||||
let qs: IDataObject;
|
||||
|
||||
let requestMethod: string;
|
||||
let endpoint: string;
|
||||
let returnAll: boolean;
|
||||
let propertyName = '';
|
||||
let responseData;
|
||||
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
|
||||
// Set initial values
|
||||
requestMethod = 'GET';
|
||||
endpoint = '';
|
||||
body = {};
|
||||
qs = {};
|
||||
returnAll = false;
|
||||
|
||||
for(let i = 0; i < items.length; i++) {
|
||||
// -----------------------------
|
||||
// Player Operations
|
||||
// -----------------------------
|
||||
if( resource === 'player' ) {
|
||||
if(operation === 'pause') {
|
||||
requestMethod = 'PUT';
|
||||
|
||||
endpoint = `/me/player/pause`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
} else if(operation === 'recentlyPlayed') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = `/me/player/recently-played`;
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
qs = {
|
||||
limit,
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.items;
|
||||
|
||||
} else if(operation === 'currentlyPlaying') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = `/me/player/currently-playing`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
} else if(operation === 'nextSong') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
endpoint = `/me/player/next`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
} else if(operation === 'previousSong') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
endpoint = `/me/player/previous`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
} else if(operation === 'startMusic') {
|
||||
requestMethod = 'PUT';
|
||||
|
||||
endpoint = `/me/player/play`;
|
||||
|
||||
const id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
body.context_uri = id;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
} else if(operation === 'addSongToQueue') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
endpoint = `/me/player/queue`;
|
||||
|
||||
const id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
qs = {
|
||||
uri: id
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
}
|
||||
// -----------------------------
|
||||
// Album Operations
|
||||
// -----------------------------
|
||||
} else if( resource === 'album') {
|
||||
const uri = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const id = uri.replace('spotify:album:', '');
|
||||
|
||||
requestMethod = 'GET';
|
||||
|
||||
if(operation === 'get') {
|
||||
endpoint = `/albums/${id}`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
} else if(operation === 'getTracks') {
|
||||
endpoint = `/albums/${id}/tracks`;
|
||||
|
||||
propertyName = 'tracks';
|
||||
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
propertyName = 'items';
|
||||
|
||||
if(!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
qs = {
|
||||
limit,
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.items;
|
||||
}
|
||||
}
|
||||
// -----------------------------
|
||||
// Artist Operations
|
||||
// -----------------------------
|
||||
} else if( resource === 'artist') {
|
||||
const uri = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const id = uri.replace('spotify:artist:', '');
|
||||
|
||||
if(operation === 'getAlbums') {
|
||||
|
||||
endpoint = `/artists/${id}/albums`;
|
||||
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
propertyName = 'items';
|
||||
|
||||
if(!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
qs = {
|
||||
limit,
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.items;
|
||||
}
|
||||
} else if(operation === 'getRelatedArtists') {
|
||||
|
||||
endpoint = `/artists/${id}/related-artists`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.artists;
|
||||
|
||||
} else if(operation === 'getTopTracks'){
|
||||
const country = this.getNodeParameter('country', i) as string;
|
||||
|
||||
qs = {
|
||||
country,
|
||||
};
|
||||
|
||||
endpoint = `/artists/${id}/top-tracks`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.tracks;
|
||||
|
||||
} else if (operation === 'get') {
|
||||
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = `/artists/${id}`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
}
|
||||
// -----------------------------
|
||||
// Playlist Operations
|
||||
// -----------------------------
|
||||
} else if( resource === 'playlist') {
|
||||
if(['delete', 'get', 'getTracks', 'add'].includes(operation)) {
|
||||
const uri = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const id = uri.replace('spotify:playlist:', '');
|
||||
|
||||
if(operation === 'delete') {
|
||||
requestMethod = 'DELETE';
|
||||
const trackId = this.getNodeParameter('trackID', i) as string;
|
||||
|
||||
body.tracks = [
|
||||
{
|
||||
uri: `${trackId}`,
|
||||
positions: [ 0 ],
|
||||
},
|
||||
];
|
||||
|
||||
endpoint = `/playlists/${id}/tracks`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
} else if(operation === 'get') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = `/playlists/${id}`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
} else if(operation === 'getTracks') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = `/playlists/${id}/tracks`;
|
||||
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
propertyName = 'items';
|
||||
|
||||
if(!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
qs = {
|
||||
'limit': limit
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.items;
|
||||
}
|
||||
} else if(operation === 'add') {
|
||||
requestMethod = 'POST';
|
||||
|
||||
const trackId = this.getNodeParameter('trackID', i) as string;
|
||||
|
||||
qs = {
|
||||
uris: trackId
|
||||
};
|
||||
|
||||
endpoint = `/playlists/${id}/tracks`;
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
}
|
||||
} else if(operation === 'getUserPlaylists') {
|
||||
requestMethod = 'GET';
|
||||
|
||||
endpoint = '/me/playlists';
|
||||
|
||||
returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
propertyName = 'items';
|
||||
|
||||
if(!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
qs = {
|
||||
limit,
|
||||
};
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
|
||||
responseData = responseData.items;
|
||||
}
|
||||
}
|
||||
// -----------------------------
|
||||
// Track Operations
|
||||
// -----------------------------
|
||||
} else if( resource === 'track') {
|
||||
const uri = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const id = uri.replace('spotify:track:', '');
|
||||
|
||||
requestMethod = 'GET';
|
||||
|
||||
if(operation === 'getAudioFeatures') {
|
||||
endpoint = `/audio-features/${id}`;
|
||||
} else if(operation === 'get') {
|
||||
endpoint = `/tracks/${id}`;
|
||||
}
|
||||
|
||||
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
|
||||
}
|
||||
|
||||
if(returnAll) {
|
||||
responseData = await spotifyApiRequestAllItems.call(this, propertyName, requestMethod, endpoint, body, qs);
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
BIN
packages/nodes-base/nodes/Spotify/spotify.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
@@ -14,19 +14,13 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const credentials = this.getCredentials('surveyMonkeyApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
const endpoint = 'https://api.surveymonkey.com/v3';
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `bearer ${credentials.accessToken}`,
|
||||
},
|
||||
method,
|
||||
body,
|
||||
@@ -34,6 +28,7 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF
|
||||
uri: uri || `${endpoint}${resource}`,
|
||||
json: true
|
||||
};
|
||||
|
||||
if (!Object.keys(body).length) {
|
||||
delete options.body;
|
||||
}
|
||||
@@ -41,8 +36,22 @@ export async function surveyMonkeyApiRequest(this: IExecuteFunctions | IWebhookF
|
||||
delete options.qs;
|
||||
}
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if ( authenticationMethod === 'accessToken') {
|
||||
const credentials = this.getCredentials('surveyMonkeyApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
// @ts-ignore
|
||||
options.headers['Authorization'] = `bearer ${credentials.accessToken}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2?.call(this, 'surveyMonkeyOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error.response.body.error.message;
|
||||
if (errorMessage !== undefined) {
|
||||
|
||||
@@ -49,6 +49,24 @@ export class SurveyMonkeyTrigger implements INodeType {
|
||||
{
|
||||
name: 'surveyMonkeyApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'surveyMonkeyOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
@@ -66,6 +84,23 @@ export class SurveyMonkeyTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'Method of authentication.',
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'objectType',
|
||||
@@ -453,11 +488,18 @@ export class SurveyMonkeyTrigger implements INodeType {
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const objectType = this.getNodeParameter('objectType') as string;
|
||||
const credentials = this.getCredentials('surveyMonkeyApi') as IDataObject;
|
||||
const authenticationMethod = this.getNodeParameter('authentication') as string;
|
||||
let credentials : IDataObject;
|
||||
const headerData = this.getHeaderData() as IDataObject;
|
||||
const req = this.getRequestObject();
|
||||
const webhookName = this.getWebhookName();
|
||||
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
credentials = this.getCredentials('surveyMonkeyApi') as IDataObject;
|
||||
} else {
|
||||
credentials = this.getCredentials('surveyMonkeyOAuth2Api') as IDataObject;
|
||||
}
|
||||
|
||||
if (webhookName === 'setup') {
|
||||
// It is a create webhook confirmation request
|
||||
return {};
|
||||
|
||||
@@ -29,7 +29,7 @@ export const attachmentOperations = [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get the data of an attachments',
|
||||
description: 'Get the data of an attachment',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
|
||||
@@ -44,17 +44,17 @@ export const checklistOperations = [
|
||||
{
|
||||
name: 'Get Checklist Items',
|
||||
value: 'getCheckItem',
|
||||
description: 'Get a specific Checklist on a card',
|
||||
description: 'Get a specific checklist on a card',
|
||||
},
|
||||
{
|
||||
name: 'Get Completed Checklist Items',
|
||||
value: 'completedCheckItems',
|
||||
description: 'Get the completed Checklist items on a card',
|
||||
description: 'Get the completed checklist items on a card',
|
||||
},
|
||||
{
|
||||
name: 'Update Checklist Item',
|
||||
value: 'updateCheckItem',
|
||||
description: 'Update an item in a checklist on a card.',
|
||||
description: 'Update an item in a checklist on a card',
|
||||
},
|
||||
],
|
||||
default: 'getAll',
|
||||
|
||||
@@ -39,7 +39,7 @@ export const labelOperations = [
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Returns all label for the board',
|
||||
description: 'Returns all labels for the board',
|
||||
},
|
||||
{
|
||||
name: 'Remove From Card',
|
||||
|
||||
@@ -219,7 +219,7 @@ export const tweetFields = [
|
||||
description: 'The entities node will not be included when set to false',
|
||||
},
|
||||
{
|
||||
displayName: 'Lang',
|
||||
displayName: 'Language',
|
||||
name: 'lang',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
|
||||
@@ -133,7 +133,15 @@ export class Twitter implements INodeType {
|
||||
let attachmentBody = {};
|
||||
let response: IDataObject = {};
|
||||
|
||||
if (binaryData[binaryPropertyName].mimeType.includes('image')) {
|
||||
const isAnimatedWebp = (Buffer.from(binaryData[binaryPropertyName].data, 'base64').toString().indexOf('ANMF') !== -1);
|
||||
|
||||
const isImage = binaryData[binaryPropertyName].mimeType.includes('image');
|
||||
|
||||
if (isImage && isAnimatedWebp) {
|
||||
throw new Error('Animated .webp images are not supported use .gif instead');
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
|
||||
const attachmentBody = {
|
||||
media_data: binaryData[binaryPropertyName].data,
|
||||
|
||||
@@ -45,18 +45,10 @@ export interface ITypeformAnswerField {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('typeformApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
query = query || {};
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Authorization': `bearer ${credentials.accessToken}`,
|
||||
},
|
||||
headers: {},
|
||||
method,
|
||||
body,
|
||||
qs: query,
|
||||
@@ -64,8 +56,23 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa
|
||||
json: true,
|
||||
};
|
||||
|
||||
query = query || {};
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
|
||||
const credentials = this.getCredentials('typeformApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
options.headers!['Authorization'] = `bearer ${credentials.accessToken}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2!.call(this, 'typeformOAuth2Api', options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode === 401) {
|
||||
// Return a clear error
|
||||
|
||||
@@ -37,7 +37,25 @@ export class TypeformTrigger implements INodeType {
|
||||
{
|
||||
name: 'typeformApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'typeformOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -48,6 +66,23 @@ export class TypeformTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Form',
|
||||
name: 'formId',
|
||||
|
||||
@@ -113,7 +113,9 @@ export class Uplead implements INodeType {
|
||||
if (Array.isArray(responseData.data)) {
|
||||
returnData.push.apply(returnData, responseData.data as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData.data as IDataObject);
|
||||
if (responseData.data !== null) {
|
||||
returnData.push(responseData.data as IDataObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { OptionsWithUri } from 'request';
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
@@ -6,17 +9,16 @@ import {
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function webflowApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('webflowApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
const authenticationMethod = this.getNodeParameter('authentication', 0);
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
authorization: `Bearer ${credentials.accessToken}`,
|
||||
'accept-version': '1.0.0',
|
||||
},
|
||||
method,
|
||||
@@ -31,14 +33,22 @@ export async function webflowApiRequest(this: IHookFunctions | IExecuteFunctions
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
if (authenticationMethod === 'accessToken') {
|
||||
const credentials = this.getCredentials('webflowApi');
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
let errorMessage = error.message;
|
||||
if (error.response.body && error.response.body.err) {
|
||||
errorMessage = error.response.body.err;
|
||||
options.headers!['authorization'] = `Bearer ${credentials.accessToken}`;
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
} else {
|
||||
return await this.helpers.requestOAuth2!.call(this, 'webflowOAuth2Api', options);
|
||||
}
|
||||
|
||||
throw new Error('Webflow Error: ' + errorMessage);
|
||||
} catch (error) {
|
||||
if (error.response.body.err) {
|
||||
throw new Error(`Webflow Error: [${error.statusCode}]: ${error.response.body.err}`);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,25 @@ export class WebflowTrigger implements INodeType {
|
||||
{
|
||||
name: 'webflowApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'accessToken',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'webflowOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
@@ -45,6 +63,23 @@ export class WebflowTrigger implements INodeType {
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Access Token',
|
||||
value: 'accessToken',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'accessToken',
|
||||
description: 'Method of authentication.',
|
||||
},
|
||||
{
|
||||
displayName: 'Site',
|
||||
name: 'site',
|
||||
|
||||
@@ -77,6 +77,7 @@ export class Webhook implements INodeType {
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: '={{$parameter["httpMethod"]}}',
|
||||
isFullPath: true,
|
||||
responseCode: '={{$parameter["responseCode"]}}',
|
||||
responseMode: '={{$parameter["responseMode"]}}',
|
||||
responseData: '={{$parameter["responseData"]}}',
|
||||
@@ -133,7 +134,7 @@ export class Webhook implements INodeType {
|
||||
default: '',
|
||||
placeholder: 'webhook',
|
||||
required: true,
|
||||
description: 'The path to listen to. Slashes("/") in the path are not allowed.',
|
||||
description: 'The path to listen to.',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Code',
|
||||
|
||||
838
packages/nodes-base/nodes/Xero/ContactDescription.ts
Normal file
@@ -0,0 +1,838 @@
|
||||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const contactOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'create a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a contact',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all contacts',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a contact',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const contactFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTenants',
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Full name of contact/organisation',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Account Number',
|
||||
name: 'accountNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A user defined account number',
|
||||
},
|
||||
// {
|
||||
// displayName: 'Addresses',
|
||||
// name: 'addressesUi',
|
||||
// type: 'fixedCollection',
|
||||
// typeOptions: {
|
||||
// multipleValues: true,
|
||||
// },
|
||||
// default: '',
|
||||
// placeholder: 'Add Address',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'addressesValues',
|
||||
// displayName: 'Address',
|
||||
// values: [
|
||||
// {
|
||||
// displayName: 'Type',
|
||||
// name: 'type',
|
||||
// type: 'options',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'PO Box',
|
||||
// value: 'POBOX',
|
||||
// },
|
||||
// {
|
||||
// name: 'Street',
|
||||
// value: 'STREET',
|
||||
// },
|
||||
// ],
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Line 1',
|
||||
// name: 'line1',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Line 2',
|
||||
// name: 'line2',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'City',
|
||||
// name: 'city',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Region',
|
||||
// name: 'region',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Postal Code',
|
||||
// name: 'postalCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Country',
|
||||
// name: 'country',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Attention To',
|
||||
// name: 'attentionTo',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
displayName: 'Bank Account Details',
|
||||
name: 'bankAccountDetails',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Bank account number of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Number',
|
||||
name: 'contactNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Status',
|
||||
name: 'contactStatus',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Active',
|
||||
value: 'ACTIVE',
|
||||
description: 'The Contact is active and can be used in transactions',
|
||||
},
|
||||
{
|
||||
name: 'Archived',
|
||||
value: 'ARCHIVED',
|
||||
description: 'The Contact is archived and can no longer be used in transactions',
|
||||
},
|
||||
{
|
||||
name: 'GDPR Request',
|
||||
value: 'GDPRREQUEST',
|
||||
description: 'The Contact is the subject of a GDPR erasure request',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Current status of a contact - see contact status types',
|
||||
},
|
||||
{
|
||||
displayName: 'Default Currency',
|
||||
name: 'defaultCurrency',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Default currency for raising invoices against contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'emailAddress',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'First name of contact person (max length = 255)',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Last name of contact person (max length = 255)',
|
||||
},
|
||||
// {
|
||||
// displayName: 'Phones',
|
||||
// name: 'phonesUi',
|
||||
// type: 'fixedCollection',
|
||||
// typeOptions: {
|
||||
// multipleValues: true,
|
||||
// },
|
||||
// default: '',
|
||||
// placeholder: 'Add Phone',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'phonesValues',
|
||||
// displayName: 'Phones',
|
||||
// values: [
|
||||
// {
|
||||
// displayName: 'Type',
|
||||
// name: 'type',
|
||||
// type: 'options',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'Default',
|
||||
// value: 'DEFAULT',
|
||||
// },
|
||||
// {
|
||||
// name: 'DDI',
|
||||
// value: 'DDI',
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile',
|
||||
// value: 'MOBILE',
|
||||
// },
|
||||
// {
|
||||
// name: 'Fax',
|
||||
// value: 'FAX',
|
||||
// },
|
||||
// ],
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Number',
|
||||
// name: 'phoneNumber',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Area Code',
|
||||
// name: 'phoneAreaCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Country Code',
|
||||
// name: 'phoneCountryCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
displayName: 'Purchase Default Account Code',
|
||||
name: 'purchasesDefaultAccountCode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountCodes',
|
||||
},
|
||||
default: '',
|
||||
description: 'The default purchases account code for contacts',
|
||||
},
|
||||
{
|
||||
displayName: 'Sales Default Account Code',
|
||||
name: 'salesDefaultAccountCode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountCodes',
|
||||
},
|
||||
default: '',
|
||||
description: 'The default sales account code for contacts',
|
||||
},
|
||||
{
|
||||
displayName: 'Skype',
|
||||
name: 'skypeUserName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Skype user name of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Tax Number',
|
||||
name: 'taxNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Tax number of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Xero Network Key',
|
||||
name: 'xeroNetworkKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Store XeroNetworkKey for contacts',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTenants',
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTenants',
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Include Archived',
|
||||
name: 'includeArchived',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `Contacts with a status of ARCHIVED will be included in the response`,
|
||||
},
|
||||
{
|
||||
displayName: 'Order By',
|
||||
name: 'orderBy',
|
||||
type: 'string',
|
||||
placeholder: 'contactID',
|
||||
default: '',
|
||||
description: 'Order by any element returned',
|
||||
},
|
||||
{
|
||||
displayName: 'Sort Order',
|
||||
name: 'sortOrder',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Asc',
|
||||
value: 'ASC',
|
||||
},
|
||||
{
|
||||
name: 'Desc',
|
||||
value: 'DESC',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Sort order',
|
||||
},
|
||||
{
|
||||
displayName: 'Where',
|
||||
name: 'where',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
alwaysOpenEditWindow: true,
|
||||
},
|
||||
placeholder: 'EmailAddress!=null&&EmailAddress.StartsWith("boom")',
|
||||
default: '',
|
||||
description: `The where parameter allows you to filter on endpoints and elements that don't have explicit parameters. <a href="https://developer.xero.com/documentation/api/requests-and-responses#get-modified" target="_blank">Examples Here</a>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* contact:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTenants',
|
||||
},
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Contact ID',
|
||||
name: 'contactId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'contact',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Account Number',
|
||||
name: 'accountNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A user defined account number',
|
||||
},
|
||||
// {
|
||||
// displayName: 'Addresses',
|
||||
// name: 'addressesUi',
|
||||
// type: 'fixedCollection',
|
||||
// typeOptions: {
|
||||
// multipleValues: true,
|
||||
// },
|
||||
// default: '',
|
||||
// placeholder: 'Add Address',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'addressesValues',
|
||||
// displayName: 'Address',
|
||||
// values: [
|
||||
// {
|
||||
// displayName: 'Type',
|
||||
// name: 'type',
|
||||
// type: 'options',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'PO Box',
|
||||
// value: 'POBOX',
|
||||
// },
|
||||
// {
|
||||
// name: 'Street',
|
||||
// value: 'STREET',
|
||||
// },
|
||||
// ],
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Line 1',
|
||||
// name: 'line1',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Line 2',
|
||||
// name: 'line2',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'City',
|
||||
// name: 'city',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Region',
|
||||
// name: 'region',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Postal Code',
|
||||
// name: 'postalCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Country',
|
||||
// name: 'country',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Attention To',
|
||||
// name: 'attentionTo',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
displayName: 'Bank Account Details',
|
||||
name: 'bankAccountDetails',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Bank account number of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Number',
|
||||
name: 'contactNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'This field is read only on the Xero contact screen, used to identify contacts in external systems',
|
||||
},
|
||||
{
|
||||
displayName: 'Contact Status',
|
||||
name: 'contactStatus',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Active',
|
||||
value: 'ACTIVE',
|
||||
description: 'The Contact is active and can be used in transactions',
|
||||
},
|
||||
{
|
||||
name: 'Archived',
|
||||
value: 'ARCHIVED',
|
||||
description: 'The Contact is archived and can no longer be used in transactions',
|
||||
},
|
||||
{
|
||||
name: 'GDPR Request',
|
||||
value: 'GDPRREQUEST',
|
||||
description: 'The Contact is the subject of a GDPR erasure request',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: 'Current status of a contact - see contact status types',
|
||||
},
|
||||
{
|
||||
displayName: 'Default Currency',
|
||||
name: 'defaultCurrency',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Default currency for raising invoices against contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'emailAddress',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Email address of contact person (umlauts not supported) (max length = 255)',
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'First name of contact person (max length = 255)',
|
||||
},
|
||||
{
|
||||
displayName: 'Last Name',
|
||||
name: 'lastName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Last name of contact person (max length = 255)',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Full name of contact/organisation',
|
||||
},
|
||||
// {
|
||||
// displayName: 'Phones',
|
||||
// name: 'phonesUi',
|
||||
// type: 'fixedCollection',
|
||||
// typeOptions: {
|
||||
// multipleValues: true,
|
||||
// },
|
||||
// default: '',
|
||||
// placeholder: 'Add Phone',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'phonesValues',
|
||||
// displayName: 'Phones',
|
||||
// values: [
|
||||
// {
|
||||
// displayName: 'Type',
|
||||
// name: 'type',
|
||||
// type: 'options',
|
||||
// options: [
|
||||
// {
|
||||
// name: 'Default',
|
||||
// value: 'DEFAULT',
|
||||
// },
|
||||
// {
|
||||
// name: 'DDI',
|
||||
// value: 'DDI',
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile',
|
||||
// value: 'MOBILE',
|
||||
// },
|
||||
// {
|
||||
// name: 'Fax',
|
||||
// value: 'FAX',
|
||||
// },
|
||||
// ],
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Number',
|
||||
// name: 'phoneNumber',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Area Code',
|
||||
// name: 'phoneAreaCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// {
|
||||
// displayName: 'Country Code',
|
||||
// name: 'phoneCountryCode',
|
||||
// type: 'string',
|
||||
// default: '',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
displayName: 'Purchase Default Account Code',
|
||||
name: 'purchasesDefaultAccountCode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountCodes',
|
||||
},
|
||||
default: '',
|
||||
description: 'The default purchases account code for contacts',
|
||||
},
|
||||
{
|
||||
displayName: 'Sales Default Account Code',
|
||||
name: 'salesDefaultAccountCode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getAccountCodes',
|
||||
},
|
||||
default: '',
|
||||
description: 'The default sales account code for contacts',
|
||||
},
|
||||
{
|
||||
displayName: 'Skype',
|
||||
name: 'skypeUserName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Skype user name of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Tax Number',
|
||||
name: 'taxNumber',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Tax number of contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Xero Network Key',
|
||||
name: 'xeroNetworkKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Store XeroNetworkKey for contacts',
|
||||
},
|
||||
],
|
||||
},
|
||||
] as INodeProperties[];
|
||||
76
packages/nodes-base/nodes/Xero/GenericFunctions.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function xeroApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://api.xero.com/api.xro/2.0${resource}`,
|
||||
json: true
|
||||
};
|
||||
try {
|
||||
if (body.organizationId) {
|
||||
options.headers = { ...options.headers, 'Xero-tenant-id': body.organizationId };
|
||||
delete body.organizationId;
|
||||
}
|
||||
if (Object.keys(headers).length !== 0) {
|
||||
options.headers = Object.assign({}, options.headers, headers);
|
||||
}
|
||||
if (Object.keys(body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2.call(this, 'xeroOAuth2Api', options);
|
||||
} catch (error) {
|
||||
let errorMessage;
|
||||
|
||||
if (error.response && error.response.body && error.response.body.Message) {
|
||||
|
||||
errorMessage = error.response.body.Message;
|
||||
|
||||
if (error.response.body.Elements) {
|
||||
const elementErrors = [];
|
||||
for (const element of error.response.body.Elements) {
|
||||
elementErrors.push(element.ValidationErrors.map((error: IDataObject) => error.Message).join('|'));
|
||||
}
|
||||
errorMessage = elementErrors.join('-');
|
||||
}
|
||||
// Try to return the error prettier
|
||||
throw new Error(`Xero error response [${error.statusCode}]: ${errorMessage}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function xeroApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
let responseData;
|
||||
query.page = 1;
|
||||
|
||||
do {
|
||||
responseData = await xeroApiRequest.call(this, method, endpoint, body, query);
|
||||
query.page++;
|
||||
returnData.push.apply(returnData, responseData[propertyName]);
|
||||
} while (
|
||||
responseData[propertyName].length !== 0
|
||||
);
|
||||
|
||||
return returnData;
|
||||
}
|
||||