Merge branch 'master' of github.com:n8n-io/n8n into n8n-2349-connectors

This commit is contained in:
Mutasem 2021-10-22 16:25:54 +02:00
commit 1c550d4e66
34 changed files with 1132 additions and 570 deletions

View file

@ -15,8 +15,6 @@ import {
IWorkflowExecutionDataProcess,
LoadNodesAndCredentials,
NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowHelpers,
WorkflowRunner,
} from '../src';

View file

@ -12,7 +12,7 @@ import { Command, flags } from '@oclif/command';
import { UserSettings } from 'n8n-core';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { INode, INodeExecutionData, ITaskData, LoggerProxy } from 'n8n-workflow';
import { INode, ITaskData, LoggerProxy } from 'n8n-workflow';
import { sep } from 'path';
@ -28,14 +28,10 @@ import {
CredentialTypes,
Db,
ExternalHooks,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IExecutionsCurrentSummary,
IWorkflowDb,
IWorkflowExecutionDataProcess,
LoadNodesAndCredentials,
NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner,
} from '../src';

View file

@ -22,8 +22,6 @@ import {
Db,
ExternalHooks,
GenericHelpers,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
IExecutionsCurrentSummary,
InternalHooksManager,
LoadNodesAndCredentials,
NodeTypes,
@ -183,10 +181,6 @@ export class Start extends Command {
const loadNodesAndCredentials = LoadNodesAndCredentials();
await loadNodesAndCredentials.init();
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init();
// Load all external hooks
const externalHooks = ExternalHooks();
await externalHooks.init();
@ -197,6 +191,10 @@ export class Start extends Command {
const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init();
// Wait till the database is ready
await startDbInitPromise;
@ -314,14 +312,16 @@ export class Start extends Command {
);
}
const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId);
await Server.start();
// Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
await activeWorkflowRunner.init();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const waitTracker = WaitTracker();
WaitTracker();
const editorUrl = GenericHelpers.getBaseUrl();
this.log(`\nEditor is now accessible via:\n${editorUrl}`);

View file

@ -4,8 +4,7 @@ import { Command, flags } from '@oclif/command';
import { IDataObject, LoggerProxy } from 'n8n-workflow';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Db, GenericHelpers } from '../../src';
import { Db } from '../../src';
import { getLogger } from '../../src/Logger';

View file

@ -18,10 +18,9 @@ import {
Db,
ExternalHooks,
GenericHelpers,
InternalHooksManager,
LoadNodesAndCredentials,
NodeTypes,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
TestWebhooks,
WebhookServer,
} from '../src';
@ -149,6 +148,9 @@ export class Webhook extends Command {
// Wait till the database is ready
await startDbInitPromise;
const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId);
if (config.get('executions.mode') === 'queue') {
const redisHost = config.get('queue.bull.redis.host');
const redisPassword = config.get('queue.bull.redis.password');

View file

@ -12,21 +12,12 @@ import * as PCancelable from 'p-cancelable';
import { Command, flags } from '@oclif/command';
import { UserSettings, WorkflowExecute } from 'n8n-core';
import {
IDataObject,
INodeTypes,
IRun,
IWorkflowExecuteHooks,
Workflow,
WorkflowHooks,
LoggerProxy,
} from 'n8n-workflow';
import { INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
import { FindOneOptions } from 'typeorm';
import * as Bull from 'bull';
import {
ActiveExecutions,
CredentialsOverwrites,
CredentialTypes,
Db,
@ -35,11 +26,10 @@ import {
IBullJobData,
IBullJobResponse,
IExecutionFlattedDb,
IExecutionResponse,
InternalHooksManager,
LoadNodesAndCredentials,
NodeTypes,
ResponseHelper,
WorkflowCredentials,
WorkflowExecuteAdditionalData,
} from '../src';
@ -203,7 +193,7 @@ export class Worker extends Command {
Worker.runningJobs[job.id] = workflowRun;
// Wait till the execution is finished
const runData = await workflowRun;
await workflowRun;
delete Worker.runningJobs[job.id];
@ -269,6 +259,9 @@ export class Worker extends Command {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId);
const versions = await GenericHelpers.getVersions();
console.info('\nn8n worker is now ready');

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.144.0",
"version": "0.145.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -110,10 +110,10 @@
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"mysql2": "~2.3.0",
"n8n-core": "~0.89.0",
"n8n-editor-ui": "~0.112.0",
"n8n-nodes-base": "~0.141.0",
"n8n-workflow": "~0.72.0",
"n8n-core": "~0.90.0",
"n8n-editor-ui": "~0.113.0",
"n8n-nodes-base": "~0.142.0",
"n8n-workflow": "~0.73.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^8.3.0",

View file

@ -1,31 +1,13 @@
import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle
import { CredentialsOverwrites, ICredentialsTypeData } from '.';
import { ICredentialsTypeData } from '.';
class CredentialTypesClass implements ICredentialTypesInterface {
credentialTypes: ICredentialsTypeData = {};
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
this.credentialTypes = credentialTypes;
// Load the credentials overwrites if any exist
const credentialsOverwrites = CredentialsOverwrites().getAll();
// eslint-disable-next-line no-restricted-syntax
for (const credentialType of Object.keys(credentialsOverwrites)) {
if (credentialTypes[credentialType] === undefined) {
// eslint-disable-next-line no-continue
continue;
}
// Add which properties got overwritten that the Editor-UI knows
// which properties it should hide
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
credentialTypes[credentialType].__overwrittenProperties = Object.keys(
credentialsOverwrites[credentialType],
);
}
}
getAll(): ICredentialType[] {

View file

@ -12,6 +12,9 @@ class CredentialsOverwritesClass {
private resolvedTypes: string[] = [];
async init(overwriteData?: ICredentialsOverwrite) {
// If data gets reinitialized reset the resolved types cache
this.resolvedTypes.length = 0;
if (overwriteData !== undefined) {
// If data is already given it can directly be set instead of
// loaded from environment
@ -41,6 +44,7 @@ class CredentialsOverwritesClass {
if (overwrites && Object.keys(overwrites).length) {
this.overwriteData[type] = overwrites;
credentialTypeData.__overwrittenProperties = Object.keys(overwrites);
}
}
}

View file

@ -308,7 +308,7 @@ export interface IDiagnosticInfo {
export interface IInternalHooksClass {
onN8nStop(): Promise<void>;
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void>;
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]>;
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(workflowId: string): Promise<void>;

View file

@ -11,7 +11,7 @@ import { Telemetry } from './telemetry';
export class InternalHooksClass implements IInternalHooksClass {
constructor(private telemetry: Telemetry) {}
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void> {
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]> {
const info = {
version_cli: diagnosticInfo.versionCli,
db_type: diagnosticInfo.databaseType,
@ -22,12 +22,15 @@ export class InternalHooksClass implements IInternalHooksClass {
execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType,
};
await this.telemetry.identify(info);
await this.telemetry.track('Instance started', info);
return Promise.all([
this.telemetry.identify(info),
this.telemetry.track('Instance started', info),
]);
}
async onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void> {
await this.telemetry.track('User responded to personalization questions', {
return this.telemetry.track('User responded to personalization questions', {
company_size: answers.companySize,
coding_skill: answers.codingSkill,
work_area: answers.workArea,
@ -36,20 +39,20 @@ export class InternalHooksClass implements IInternalHooksClass {
}
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
await this.telemetry.track('User created workflow', {
return this.telemetry.track('User created workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
});
}
async onWorkflowDeleted(workflowId: string): Promise<void> {
await this.telemetry.track('User deleted workflow', {
return this.telemetry.track('User deleted workflow', {
workflow_id: workflowId,
});
}
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
await this.telemetry.track('User saved workflow', {
return this.telemetry.track('User saved workflow', {
workflow_id: workflow.id,
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
});
@ -96,10 +99,16 @@ export class InternalHooksClass implements IInternalHooksClass {
}
}
void this.telemetry.trackWorkflowExecution(properties);
return this.telemetry.trackWorkflowExecution(properties);
}
async onN8nStop(): Promise<void> {
await this.telemetry.trackN8nStop();
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 3000);
});
return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}
}

View file

@ -27,16 +27,7 @@
import * as express from 'express';
import { readFileSync } from 'fs';
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
import {
FindManyOptions,
FindOneOptions,
getConnectionManager,
In,
IsNull,
LessThanOrEqual,
Like,
Not,
} from 'typeorm';
import { FindManyOptions, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
import * as bodyParser from 'body-parser';
import * as history from 'connect-history-api-fallback';
import * as os from 'os';
@ -47,7 +38,7 @@ import * as clientOAuth1 from 'oauth-1.0a';
import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native';
import { createHash, createHmac } from 'crypto';
import { createHmac } from 'crypto';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
@ -63,7 +54,6 @@ import {
import {
ICredentialsDecrypted,
ICredentialsEncrypted,
ICredentialType,
IDataObject,
INodeCredentials,
@ -73,17 +63,15 @@ import {
INodeType,
INodeTypeDescription,
INodeTypeNameVersion,
IRunData,
INodeVersionedType,
ITelemetryClientConfig,
ITelemetrySettings,
IWorkflowBase,
IWorkflowCredentials,
LoggerProxy,
NodeCredentialTestRequest,
NodeCredentialTestResult,
NodeHelpers,
Workflow,
ICredentialsEncrypted,
WorkflowExecuteMode,
} from 'n8n-workflow';
@ -134,7 +122,6 @@ import {
IWorkflowExecutionDataProcess,
IWorkflowResponse,
IPersonalizationSurveyAnswers,
LoadNodesAndCredentials,
NodeTypes,
Push,
ResponseHelper,
@ -325,8 +312,6 @@ class App {
this.frontendSettings.personalizationSurvey =
await PersonalizationSurvey.preparePersonalizationSurvey();
InternalHooksManager.init(this.frontendSettings.instanceId);
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
const excludeEndpoints = config.get('security.excludeEndpoints') as string;
@ -1832,12 +1817,6 @@ class App {
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
// Decrypt the currently saved credentials
const workflowCredentials: IWorkflowCredentials = {
[result.type]: {
[result.id.toString()]: result as ICredentialsEncrypted,
},
};
const mode: WorkflowExecuteMode = 'internal';
const credentialsHelper = new CredentialsHelper(encryptionKey);
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
@ -2056,13 +2035,6 @@ class App {
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
// Decrypt the currently saved credentials
const workflowCredentials: IWorkflowCredentials = {
[result.type]: {
[result.id.toString()]: result as ICredentialsEncrypted,
},
};
const mode: WorkflowExecuteMode = 'internal';
const credentialsHelper = new CredentialsHelper(encryptionKey);
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
@ -2795,16 +2767,10 @@ class App {
return;
}
const loadNodesAndCredentials = LoadNodesAndCredentials();
const credentialsOverwrites = CredentialsOverwrites();
await credentialsOverwrites.init(body);
const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
this.presetCredentialsLoaded = true;
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.89.0",
"version": "0.90.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -50,7 +50,7 @@
"form-data": "^4.0.0",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.72.0",
"n8n-workflow": "~0.73.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"qs": "^6.10.1",

View file

@ -194,7 +194,7 @@ export class ActiveWorkflows {
// The trigger function to execute when the cron-time got reached
const executeTrigger = async () => {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {
Logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, {
workflowName: workflow.name,
workflowId: workflow.id,
});

View file

@ -162,16 +162,19 @@ async function parseRequestObject(requestObject: IDataObject) {
// and also using formData. Request lib takes precedence for the formData.
// We will do the same.
// Merge body and form properties.
// @ts-ignore
axiosConfig.data =
typeof requestObject.body === 'string'
? requestObject.body
: new URLSearchParams(
Object.assign(requestObject.body || {}, requestObject.form || {}) as Record<
string,
string
>,
);
if (typeof requestObject.body === 'string') {
axiosConfig.data = requestObject.body;
} else {
const allData = Object.assign(requestObject.body || {}, requestObject.form || {}) as Record<
string,
string
>;
if (requestObject.useQuerystring === true) {
axiosConfig.data = stringify(allData, { arrayFormat: 'repeat' });
} else {
axiosConfig.data = stringify(allData);
}
}
} else if (contentType && contentType.includes('multipart/form-data') !== false) {
if (requestObject.formData !== undefined && requestObject.formData instanceof FormData) {
axiosConfig.data = requestObject.formData;

View file

@ -1,6 +1,6 @@
{
"name": "n8n-design-system",
"version": "0.4.0",
"version": "0.5.0",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
"author": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.112.0",
"version": "0.113.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -26,7 +26,7 @@
},
"dependencies": {
"@fontsource/open-sans": "^4.5.0",
"n8n-design-system": "~0.4.0",
"n8n-design-system": "~0.5.0",
"timeago.js": "^4.0.2",
"v-click-outside": "^3.1.2",
"vue-fragment": "^1.5.2"
@ -71,7 +71,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.72.0",
"n8n-workflow": "~0.73.0",
"sass": "^1.26.5",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.29.0",
"version": "0.30.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -60,8 +60,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "~0.89.0",
"n8n-workflow": "~0.72.0",
"n8n-core": "~0.90.0",
"n8n-workflow": "~0.73.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",

View file

@ -17,7 +17,7 @@ export class Aws implements ICredentialType {
default: 'us-east-1',
},
{
displayName: 'Access Key Id',
displayName: 'Access Key ID',
name: 'accessKeyId',
type: 'string',
default: '',

View file

@ -22,7 +22,7 @@ export class S3 implements ICredentialType {
default: 'us-east-1',
},
{
displayName: 'Access Key Id',
displayName: 'Access Key ID',
name: 'accessKeyId',
type: 'string',
default: '',

View file

@ -0,0 +1,163 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
import {
awsApiRequestREST,
IExpenseDocument,
simplify,
validateCrendetials,
} from './GenericFunctions';
export class AwsTextract implements INodeType {
description: INodeTypeDescription = {
displayName: 'AWS Textract',
name: 'awsTextract',
icon: 'file:textract.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Sends data to Amazon Textract',
defaults: {
name: 'AWS Textract',
color: '#5aa08d',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'aws',
required: true,
testedBy: 'awsTextractApiCredentialTest',
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Analyze Receipt or Invoice',
value: 'analyzeExpense',
},
],
default: 'analyzeExpense',
description: '',
},
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
displayOptions: {
show: {
operation: [
'analyzeExpense',
],
},
},
required: true,
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG',
},
{
displayName: 'Simplify Response',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'analyzeExpense',
],
},
},
default: true,
description: 'Return a simplified version of the response instead of the raw data.',
},
],
};
methods = {
credentialTest: {
async awsTextractApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject, 'sts');
} catch (error) {
return {
status: 'Error',
message: 'The security token included in the request is invalid',
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
let responseData;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
try {
//https://docs.aws.amazon.com/textract/latest/dg/API_AnalyzeExpense.html
if (operation === 'analyzeExpense') {
const binaryProperty = this.getNodeParameter('binaryPropertyName', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
if (items[i].binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
if ((items[i].binary as IBinaryKeyData)[binaryProperty] === undefined) {
throw new NodeOperationError(this.getNode(), `No binary data property "${binaryProperty}" does not exists on item!`);
}
const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryProperty];
const body: IDataObject = {
Document: {
Bytes: binaryPropertyData.data,
},
};
const action = 'Textract.AnalyzeExpense';
responseData = await awsApiRequestREST.call(this, 'textract', 'POST', '', JSON.stringify(body), { 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' }) as IExpenseDocument;
if (simple) {
responseData = simplify(responseData);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as unknown as IDataObject);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,156 @@
import {
URL,
} from 'url';
import {
Request,
sign,
} from 'aws4';
import {
OptionsWithUri,
} from 'request';
import {
parseString,
} from 'xml2js';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialTestFunctions,
NodeApiError,
NodeOperationError,
} from 'n8n-workflow';
function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string {
let endpoint;
if (service === 'lambda' && credentials.lambdaEndpoint) {
endpoint = credentials.lambdaEndpoint;
} else if (service === 'sns' && credentials.snsEndpoint) {
endpoint = credentials.snsEndpoint;
} else {
endpoint = `https://${service}.${credentials.region}.amazonaws.com`;
}
return (endpoint as string).replace('{region}', credentials.region as string);
}
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = await this.getCredentials('aws');
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
// Concatenate path and instantiate URL object so it parses correctly query strings
const endpoint = new URL(getEndpointForService(service, credentials) + path);
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const options: OptionsWithUri = {
headers: signOpts.headers,
method,
uri: endpoint.href,
body: signOpts.body,
};
try {
return await this.helpers.request!(options);
} catch (error) {
if (error?.response?.data || error?.response?.body) {
const errorMessage = error?.response?.data || error?.response?.body;
if (errorMessage.includes('AccessDeniedException')) {
const user = JSON.parse(errorMessage).Message.split(' ')[1];
throw new NodeApiError(this.getNode(), error, {
message: 'Unauthorized — please check your AWS policy configuration',
description: `Make sure an identity-based policy allows user ${user} to perform textract:AnalyzeExpense` });
}
}
throw new NodeApiError(this.getNode(), error); // no XML parsing needed
}
}
export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return JSON.parse(response);
} catch (error) {
return response;
}
}
export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
const response = await awsApiRequest.call(this, service, method, path, body, headers);
try {
return await new Promise((resolve, reject) => {
parseString(response, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
} catch (error) {
return response;
}
}
export function simplify(data: IExpenseDocument) {
const result: { [key: string]: string } = {};
for (const document of data.ExpenseDocuments) {
for (const field of document.SummaryFields) {
result[field?.Type?.Text || field?.LabelDetection?.Text] = field.ValueDetection.Text;
}
}
return result;
}
export interface IExpenseDocument {
ExpenseDocuments: [
{
SummaryFields: [
{
LabelDetection: { Text: string },
ValueDetection: { Text: string },
Type: { Text: string }
}]
}];
}
export async function validateCrendetials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject, service: string): Promise<any> { // tslint:disable-line:no-any
const credentials = decryptedCredentials;
// Concatenate path and instantiate URL object so it parses correctly query strings
const endpoint = new URL(getEndpointForService(service, credentials) + `?Action=GetCallerIdentity&Version=2011-06-15`);
// Sign AWS API request with the user credentials
const signOpts = { host: endpoint.host, method: 'POST', path: '?Action=GetCallerIdentity&Version=2011-06-15' } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const options: OptionsWithUri = {
headers: signOpts.headers,
method: 'POST',
uri: endpoint.href,
body: signOpts.body,
};
const response = await this.helpers.request!(options);
return await new Promise((resolve, reject) => {
parseString(response, { explicitArray: false }, (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
<title>Icon-Architecture/64/Arch_AWS-Textract_64</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
<stop stop-color="#055F4E" offset="0%"></stop>
<stop stop-color="#56C0A7" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Icon-Architecture/64/Arch_AWS-Textract_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-Architecture-BG/64/Machine-Learning" fill="url(#linearGradient-1)">
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
</g>
<path d="M22.0624102,50 C24.3763895,53.603 28.4103535,56 33.0003125,56 C40.1672485,56 45.9991964,50.168 45.9991964,43 C45.9991964,35.832 40.1672485,30 33.0003125,30 C27.6033607,30 22.9664021,33.307 21.0024196,38 L23.2143999,38 C25.0393836,34.444 28.7363506,32 33.0003125,32 C39.0652583,32 43.9992143,36.935 43.9992143,43 C43.9992143,49.065 39.0652583,54 33.0003125,54 C29.5913429,54 26.5413702,52.441 24.5213882,50 L22.0624102,50 Z M37.0002768,45 L37.0002768,43 L41.9992321,43 C41.9992321,38.038 37.9622682,34 33.0003125,34 C28.0373568,34 23.9993929,38.038 23.9993929,43 L28.9993482,43 L28.9993482,45 L24.2313908,45 C25.1443826,49.002 28.7253507,52 33.0003125,52 C35.1362934,52 37.0992759,51.249 38.6442621,50 L34.0003036,50 L34.0003036,48 L40.4782457,48 C41.0812403,47.102 41.5202364,46.087 41.7682342,45 L37.0002768,45 Z M21.0024196,48 L23.2143999,48 C22.4434068,46.498 22.0004107,44.801 22.0004107,43 C22.0004107,41.959 22.1554093,40.955 22.4264069,40 L20.3634253,40 C20.1344274,40.965 19.9994286,41.966 19.9994286,43 C19.9994286,44.771 20.3584254,46.46 21.0024196,48 L21.0024196,48 Z M19.7434309,50 L17.0004554,50 L17.0004554,48 L18.8744386,48 C18.5344417,47.04 18.2894438,46.038 18.1494451,45 L15.4144695,45 L16.707458,46.293 L15.2924706,47.707 L12.2924974,44.707 C11.9025009,44.316 11.9025009,43.684 12.2924974,43.293 L15.2924706,40.293 L16.707458,41.707 L15.4144695,43 L18.0004464,43 C18.0004464,41.973 18.1044455,40.97 18.3024437,40 L17.0004554,40 L17.0004554,38 L18.8744386,38 C20.9404202,32.184 26.4833707,28 33.0003125,28 C37.427273,28 41.4002375,29.939 44.148213,33 L59.0000804,33 L59.0000804,35 L45.6661994,35 C47.1351863,37.318 47.9991786,40.058 47.9991786,43 L59.0000804,43 L59.0000804,45 L47.8501799,45 C46.8681887,52.327 40.5912447,58 33.0003125,58 C27.2563638,58 22.2624084,54.752 19.7434309,50 L19.7434309,50 Z M37.0002768,39 C37.0002768,38.448 36.5522808,38 36.0002857,38 L29.9993482,38 C29.4473442,38 28.9993482,38.448 28.9993482,39 L28.9993482,41 L31.0003304,41 L31.0003304,40 L32.0003214,40 L32.0003214,43 L31.0003304,43 L31.0003304,45 L35.0002946,45 L35.0002946,43 L34.0003036,43 L34.0003036,40 L35.0002946,40 L35.0002946,41 L37.0002768,41 L37.0002768,39 Z M49.0001696,40 L59.0000804,40 L59.0000804,38 L49.0001696,38 L49.0001696,40 Z M49.0001696,50 L59.0000804,50 L59.0000804,48 L49.0001696,48 L49.0001696,50 Z M57.0000982,27 L60.5850662,27 L57.0000982,23.414 L57.0000982,27 Z M63.7070383,27.293 C63.8940367,27.48 64.0000357,27.735 64.0000357,28 L64.0000357,63 C64.0000357,63.552 63.5520397,64 63.0000446,64 L32.0003304,64 C31.4473264,64 31.0003304,63.552 31.0003304,63 L31.0003304,59 L33.0003125,59 L33.0003125,62 L62.0000536,62 L62.0000536,29 L56.0001071,29 C55.4471121,29 55.0001161,28.552 55.0001161,28 L55.0001161,22 L33.0003125,22 L33.0003125,27 L31.0003304,27 L31.0003304,21 C31.0003304,20.448 31.4473264,20 32.0003304,20 L56.0001071,20 C56.2651048,20 56.5191025,20.105 56.7071008,20.293 L63.7070383,27.293 Z M68,24.166 L68,61 C68,61.552 67.552004,62 67.0000089,62 L65.0000268,62 L65.0000268,60 L66.0000179,60 L66.0000179,24.612 L58.6170838,18 L36.0002857,18 L36.0002857,19 L34.0003036,19 L34.0003036,17 C34.0003036,16.448 34.4472996,16 35.0003036,16 L59.0000804,16 C59.2460782,16 59.483076,16.091 59.6660744,16.255 L67.666003,23.42 C67.8780011,23.61 68,23.881 68,24.166 L68,24.166 Z" id="Amazon-Textract_Icon_64_Squid" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,15 +1,20 @@
import { OptionsWithUri } from 'request';
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -35,6 +40,7 @@ export class BitbucketTrigger implements INodeType {
{
name: 'bitbucketApi',
required: true,
testedBy: 'bitbucketApiTest',
},
],
webhooks: [
@ -52,58 +58,36 @@ export class BitbucketTrigger implements INodeType {
type: 'options',
required: true,
options: [
{
name: 'User',
value: 'user',
},
{
name: 'Team',
value: 'team',
},
{
name: 'Repository',
value: 'repository',
},
{
name: 'Workspace',
value: 'workspace',
},
],
default: 'user',
default: 'workspace',
description: 'The resource to operate on.',
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
displayOptions: {
show: {
resource: [
'user',
],
},
},
typeOptions: {
loadOptionsMethod: 'getUsersEvents',
},
options: [],
required: true,
default: [],
description: 'The events to listen to.',
},
{
displayName: 'Team',
name: 'team',
displayName: 'Workspace',
name: 'workspace',
type: 'options',
displayOptions: {
show: {
resource: [
'team',
'workspace',
'repository',
],
},
},
typeOptions: {
loadOptionsMethod: 'getTeams',
loadOptionsMethod: 'getWorkspaces',
},
required: true,
default: '',
description: 'The team of which to listen to the events.',
description: 'The repository of which to listen to the events.',
},
{
displayName: 'Events',
@ -112,12 +96,12 @@ export class BitbucketTrigger implements INodeType {
displayOptions: {
show: {
resource: [
'team',
'workspace',
],
},
},
typeOptions: {
loadOptionsMethod: 'getTeamEvents',
loadOptionsMethod: 'getWorkspaceEvents',
},
options: [],
required: true,
@ -137,6 +121,9 @@ export class BitbucketTrigger implements INodeType {
},
typeOptions: {
loadOptionsMethod: 'getRepositories',
loadOptionsDependsOn: [
'workspace',
],
},
required: true,
default: '',
@ -166,90 +153,86 @@ export class BitbucketTrigger implements INodeType {
};
methods = {
credentialTest: {
async bitbucketApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
const credentials = credential.data;
const options: OptionsWithUri = {
method: 'GET',
auth: {
user: credentials!.username as string,
password: credentials!.appPassword as string,
},
uri: 'https://api.bitbucket.org/2.0/user',
json: true,
timeout: 5000,
};
try {
const response = await this.helpers.request(options);
if (!response.username) {
return {
status: 'Error',
message: `Token is not valid: ${response.error}`,
};
}
} catch (error) {
return {
status: 'Error',
message: `Settings are not valid: ${error}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
},
},
loadOptions: {
// Get all the events to display them to user so that he can
// select them easily
async getUsersEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
async getWorkspaceEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/user');
const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/workspace');
for (const event of events) {
const eventName = event.event;
const eventId = event.event;
const eventDescription = event.description;
returnData.push({
name: eventName,
value: eventId,
description: eventDescription,
name: event.event,
value: event.event,
description: event.description,
});
}
return returnData;
},
// Get all the events to display them to user so that he can
// select them easily
async getTeamEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/team');
for (const event of events) {
const eventName = event.event;
const eventId = event.event;
const eventDescription = event.description;
returnData.push({
name: eventName,
value: eventId,
description: eventDescription,
});
}
return returnData;
},
// Get all the events to display them to user so that he can
// select them easily
async getRepositoriesEvents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const events = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/hook_events/repository');
for (const event of events) {
const eventName = event.event;
const eventId = event.event;
const eventDescription = event.description;
returnData.push({
name: eventName,
value: eventId,
description: eventDescription,
name: event.event,
value: event.event,
description: event.description,
});
}
return returnData;
},
// Get all the repositories to display them to user so that he can
// select them easily
async getRepositories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const credentials = await this.getCredentials('bitbucketApi');
const returnData: INodePropertyOptions[] = [];
const repositories = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', `/repositories/${credentials!.username}`);
const workspace = this.getCurrentNodeParameter('workspace') as string;
const repositories = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', `/repositories/${workspace}`);
for (const repository of repositories) {
const repositoryName = repository.slug;
const repositoryId = repository.slug;
const repositoryDescription = repository.description;
returnData.push({
name: repositoryName,
value: repositoryId,
description: repositoryDescription,
name: repository.slug,
value: repository.slug,
description: repository.description,
});
}
return returnData;
},
// Get all the teams to display them to user so that he can
// select them easily
async getTeams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
async getWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const qs: IDataObject = {
role: 'member',
};
const teams = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', '/teams', {}, qs);
for (const team of teams) {
const teamName = team.display_name;
const teamId = team.username;
const workspaces = await bitbucketApiRequestAllItems.call(this, 'values', 'GET', `/workspaces`);
for (const workspace of workspaces) {
returnData.push({
name: teamName,
value: teamId,
name: workspace.name,
value: workspace.slug,
});
}
return returnData;
@ -261,29 +244,25 @@ export class BitbucketTrigger implements INodeType {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
let endpoint = '';
const credentials = await this.getCredentials('bitbucketApi');
const resource = this.getNodeParameter('resource', 0) as string;
const workspace = this.getNodeParameter('workspace', 0) as string;
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
return false;
}
if (resource === 'user') {
endpoint = `/users/${credentials!.username}/hooks/${webhookData.webhookId}`;
}
if (resource === 'team') {
const team = this.getNodeParameter('team', 0) as string;
endpoint = `/teams/${team}/hooks/${webhookData.webhookId}`;
if (resource === 'workspace') {
endpoint = `/workspaces/${workspace}/hooks`;
}
if (resource === 'repository') {
const repository = this.getNodeParameter('repository', 0) as string;
endpoint = `/repositories/${credentials!.username}/${repository}/hooks/${webhookData.webhookId}`;
endpoint = `/repositories/${workspace}/${repository}/hooks`;
}
try {
await bitbucketApiRequest.call(this, 'GET', endpoint);
} catch (error) {
return false;
const { values: hooks } = await bitbucketApiRequest.call(this, 'GET', endpoint);
for (const hook of hooks) {
if (webhookUrl === hook.url && hook.active === true) {
webhookData.webhookId = hook.uuid.replace('{', '').replace('}', '');
return true;
}
}
return true;
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
let responseData;
@ -292,18 +271,14 @@ export class BitbucketTrigger implements INodeType {
const webhookData = this.getWorkflowStaticData('node');
const events = this.getNodeParameter('events') as string[];
const resource = this.getNodeParameter('resource', 0) as string;
const credentials = await this.getCredentials('bitbucketApi');
const workspace = this.getNodeParameter('workspace', 0) as string;
if (resource === 'user') {
endpoint = `/users/${credentials!.username}/hooks`;
}
if (resource === 'team') {
const team = this.getNodeParameter('team', 0) as string;
endpoint = `/teams/${team}/hooks`;
if (resource === 'workspace') {
endpoint = `/workspaces/${workspace}/hooks`;
}
if (resource === 'repository') {
const repository = this.getNodeParameter('repository', 0) as string;
endpoint = `/repositories/${credentials!.username}/${repository}/hooks`;
endpoint = `/repositories/${workspace}/${repository}/hooks`;
}
const body: IDataObject = {
description: 'n8n webhook',
@ -318,22 +293,18 @@ export class BitbucketTrigger implements INodeType {
async delete(this: IHookFunctions): Promise<boolean> {
let endpoint = '';
const webhookData = this.getWorkflowStaticData('node');
const credentials = await this.getCredentials('bitbucketApi');
const workspace = this.getNodeParameter('workspace', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string;
if (resource === 'user') {
endpoint = `/users/${credentials!.username}/hooks/${webhookData.webhookId}`;
}
if (resource === 'team') {
const team = this.getNodeParameter('team', 0) as string;
endpoint = `/teams/${team}/hooks/${webhookData.webhookId}`;
if (resource === 'workspace') {
endpoint = `/workspaces/${workspace}/hooks/${webhookData.webhookId}`;
}
if (resource === 'repository') {
const repository = this.getNodeParameter('repository', 0) as string;
endpoint = `/repositories/${credentials!.username}/${repository}/hooks/${webhookData.webhookId}`;
endpoint = `/repositories/${workspace}/${repository}/hooks/${webhookData.webhookId}`;
}
try {
await bitbucketApiRequest.call(this, 'DELETE', endpoint);
} catch(error) {
} catch (error) {
return false;
}
delete webhookData.webhookId;

View file

@ -1,12 +1,17 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
} from 'n8n-core';
import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -39,6 +44,7 @@ export class Github implements INodeType {
{
name: 'githubApi',
required: true,
testedBy: 'githubApiTest',
displayOptions: {
show: {
authentication: [
@ -1698,6 +1704,43 @@ export class Github implements INodeType {
],
};
methods = {
credentialTest: {
async githubApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
const credentials = credential.data;
const baseUrl = credentials!.server as string || 'https://api.github.com/user';
const options: OptionsWithUri = {
method: 'GET',
headers: {
'User-Agent': 'n8n',
Authorization: `token ${credentials!.accessToken}`,
},
uri: baseUrl,
json: true,
timeout: 5000,
};
try {
const response = await this.helpers.request(options);
if (!response.id) {
return {
status: 'Error',
message: `Token is not valid: ${response.error}`,
};
}
} catch (error) {
return {
status: 'Error',
message: `Settings are not valid: ${error}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();

View file

@ -9,14 +9,17 @@ import {
} from 'n8n-core';
import {
IDataObject, NodeApiError, NodeOperationError,
IDataObject,
IPollFunctions,
NodeApiError,
NodeOperationError,
} 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
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, 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 = {
@ -29,7 +32,9 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
uri: uri || `https://www.googleapis.com${resource}`,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
@ -59,17 +64,16 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
}
}
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, 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;
query.pageSize = 100;
query.maxResults = query.maxResults || 100;
query.pageSize = query.pageSize || 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 &&
@ -79,7 +83,7 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp
return returnData;
}
function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise<IDataObject> {
function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, credentials: IDataObject): Promise<IDataObject> {
//https://developers.google.com/identity/protocols/oauth2/service-account#httprest
const scopes = [
@ -125,3 +129,17 @@ function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoa
return this.helpers.request!(options);
}
export function extractId(url: string): string {
if (url.includes('/d/')) {
//https://docs.google.com/document/d/1TUJGUf5HUv9e6MJBzcOsPruxXDeGMnGYTBWfkMagcg4/edit
const data = url.match(/[-\w]{25,}/);
if (Array.isArray(data)) {
return data[0];
}
} else if (url.includes('/folders/')) {
//https://drive.google.com/drive/u/0/folders/19MqnruIXju5sAWYD3J71im1d2CBJkZzy
return url.split('/folders/')[1];
}
return url;
}

View file

@ -1,294 +1,438 @@
// import { google } from 'googleapis';
// import {
// IHookFunctions,
// IWebhookFunctions,
// } from 'n8n-core';
// import {
// IDataObject,
// INodeTypeDescription,
// INodeType,
// IWebhookResponseData,
// NodeOperationError,
// } from 'n8n-workflow';
// import { getAuthenticationClient } from './GoogleApi';
// export class GoogleDriveTrigger implements INodeType {
// description: INodeTypeDescription = {
// displayName: 'Google Drive Trigger',
// name: 'googleDriveTrigger',
// icon: 'file:googleDrive.png',
// group: ['trigger'],
// version: 1,
// subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}',
// description: 'Starts the workflow when a file on Google Drive is changed',
// defaults: {
// name: 'Google Drive Trigger',
// color: '#3f87f2',
// },
// inputs: [],
// outputs: ['main'],
// credentials: [
// {
// name: 'googleApi',
// required: true,
// }
// ],
// webhooks: [
// {
// name: 'default',
// httpMethod: 'POST',
// responseMode: 'onReceived',
// path: 'webhook',
// },
// ],
// properties: [
// {
// displayName: 'Resource Id',
// name: 'resourceId',
// type: 'string',
// default: '',
// required: true,
// placeholder: '',
// description: 'ID of the resource to watch, for example a file ID.',
// },
// ],
// };
// // @ts-ignore (because of request)
// webhookMethods = {
// default: {
// async checkExists(this: IHookFunctions): Promise<boolean> {
// // const webhookData = this.getWorkflowStaticData('node');
// // if (webhookData.webhookId === undefined) {
// // // No webhook id is set so no webhook can exist
// // return false;
// // }
// // // 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 = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
// // try {
// // await githubApiRequest.call(this, 'GET', endpoint, {});
// // } catch (error) {
// // if (error.message.includes('[404]:')) {
// // // Webhook does not exist
// // delete webhookData.webhookId;
// // delete webhookData.webhookEvents;
// // return false;
// // }
// // // Some error occured
// // throw e;
// // }
// // If it did not error then the webhook exists
// // return true;
// return false;
// },
// async create(this: IHookFunctions): Promise<boolean> {
// const webhookUrl = this.getNodeWebhookUrl('default');
// const resourceId = this.getNodeParameter('resourceId') as string;
// const credentials = await this.getCredentials('googleApi');
// if (credentials === undefined) {
// throw new NodeOperationError(this.getNode(), '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',
// auth: client,
// });
// const accessToken = await client.getAccessToken();
// console.log('accessToken: ');
// console.log(accessToken);
// const asdf = await drive.changes.getStartPageToken();
// // console.log('asdf: ');
// // console.log(asdf);
// const response = await drive.changes.watch({
// //
// pageToken: asdf.data.startPageToken,
// requestBody: {
// id: 'asdf-test-2',
// address: webhookUrl,
// resourceId,
// type: 'web_hook',
// // page_token: '',
// }
// });
// console.log('...response...CREATE');
// console.log(JSON.stringify(response, null, 2));
// // const endpoint = `/repos/${owner}/${repository}/hooks`;
// // const body = {
// // name: 'web',
// // config: {
// // url: webhookUrl,
// // content_type: 'json',
// // // secret: '...later...',
// // insecure_ssl: '1', // '0' -> not allow inscure ssl | '1' -> allow insercure SSL
// // },
// // events,
// // active: true,
// // };
// // let responseData;
// // try {
// // responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
// // } catch (error) {
// // if (error.message.includes('[422]:')) {
// // throw new NodeOperationError(this.getNode(), 'A webhook with the identical URL exists already. Please delete it manually on Github!');
// // }
// // throw e;
// // }
// // if (responseData.id === undefined || responseData.active !== true) {
// // // Required data is missing so was not successful
// // throw new NodeOperationError(this.getNode(), 'Github webhook creation response did not contain the expected data.');
// // }
// // const webhookData = this.getWorkflowStaticData('node');
// // webhookData.webhookId = responseData.id as string;
// // webhookData.webhookEvents = responseData.events as string[];
// return true;
// },
// async delete(this: IHookFunctions): Promise<boolean> {
// const webhookUrl = this.getNodeWebhookUrl('default');
// const resourceId = this.getNodeParameter('resourceId') as string;
// const credentials = await this.getCredentials('googleApi');
// if (credentials === undefined) {
// throw new NodeOperationError(this.getNode(), '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',
// auth: client,
// });
// // Remove channel
// const response = await drive.channels.stop({
// requestBody: {
// id: 'asdf-test-2',
// address: webhookUrl,
// resourceId,
// type: 'web_hook',
// }
// });
// console.log('...response...DELETE');
// console.log(JSON.stringify(response, null, 2));
// // const webhookData = this.getWorkflowStaticData('node');
// // if (webhookData.webhookId !== undefined) {
// // const owner = this.getNodeParameter('owner') as string;
// // const repository = this.getNodeParameter('repository') as string;
// // const endpoint = `/repos/${owner}/${repository}/hooks/${webhookData.webhookId}`;
// // const body = {};
// // try {
// // await githubApiRequest.call(this, 'DELETE', endpoint, body);
// // } catch (error) {
// // 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 bodyData = this.getBodyData();
// console.log('');
// console.log('');
// console.log('GOT WEBHOOK CALL');
// console.log(JSON.stringify(bodyData, null, 2));
// // Check if the webhook is only the ping from Github to confirm if it workshook_id
// if (bodyData.hook_id !== undefined && bodyData.action === undefined) {
// // Is only the ping and not an actual webhook call. So return 'OK'
// // but do not start the workflow.
// return {
// webhookResponse: 'OK'
// };
// }
// // Is a regular webhoook call
// // TODO: Add headers & requestPath
// const returnData: IDataObject[] = [];
// returnData.push(
// {
// body: bodyData,
// headers: this.getHeaderData(),
// query: this.getQueryData(),
// }
// );
// return {
// workflowData: [
// this.helpers.returnJsonArray(returnData)
// ],
// };
// }
// }
import {
IPollFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeApiError,
} from 'n8n-workflow';
import {
extractId,
googleApiRequest,
googleApiRequestAllItems,
} from './GenericFunctions';
import * as moment from 'moment';
export class GoogleDriveTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Drive Trigger',
name: 'googleDriveTrigger',
icon: 'file:googleDrive.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Google Drive events occur',
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'Google Drive Trigger',
color: '#4285F4',
},
credentials: [
{
name: 'googleApi',
required: true,
displayOptions: {
show: {
authentication: [
'serviceAccount',
],
},
},
},
{
name: 'googleDriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
polling: true,
inputs: [],
outputs: ['main'],
properties: [
{
displayName: 'Credential Type',
name: 'authentication',
type: 'options',
options: [
{
name: 'Service Account',
value: 'serviceAccount',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'oAuth2',
},
{
displayName: 'Trigger On',
name: 'triggerOn',
type: 'options',
required: true,
default: '',
options: [
{
name: 'Changes to a Specific File',
value: 'specificFile',
},
{
name: 'Changes Involving a Specific Folder',
value: 'specificFolder',
},
// {
// name: 'Changes To Any File/Folder',
// value: 'anyFileFolder',
// },
],
description: '',
},
{
displayName: 'File URL or ID',
name: 'fileToWatch',
type: 'string',
displayOptions: {
show: {
triggerOn: [
'specificFile',
],
},
},
default: '',
description: 'The address of this file when you view it in your browser (or just the ID contained within the URL)',
required: true,
},
{
displayName: 'Watch For',
name: 'event',
type: 'options',
displayOptions: {
show: {
triggerOn: [
'specificFile',
],
},
},
required: true,
default: 'fileUpdated',
options: [
{
name: 'File Updated',
value: 'fileUpdated',
},
],
description: 'When to trigger this node',
},
{
displayName: 'Folder URL or ID',
name: 'folderToWatch',
type: 'string',
displayOptions: {
show: {
triggerOn: [
'specificFolder',
],
},
},
default: '',
description: 'The address of this folder when you view it in your browser (or just the ID contained within the URL)',
required: true,
},
{
displayName: 'Watch For',
name: 'event',
type: 'options',
displayOptions: {
show: {
triggerOn: [
'specificFolder',
],
},
},
required: true,
default: '',
options: [
{
name: 'File Created',
value: 'fileCreated',
description: 'When a file is created in the watched folder',
},
{
name: 'File Updated',
value: 'fileUpdated',
description: 'When a file is updated in the watched folder',
},
{
name: 'Folder Created',
value: 'folderCreated',
description: 'When a folder is created in the watched folder',
},
{
name: 'Folder Updated',
value: 'folderUpdated',
description: 'When a folder is updated in the watched folder',
},
{
name: 'Watch Folder Updated',
value: 'watchFolderUpdated',
description: 'When the watched folder itself is modified',
},
],
},
{
displayName: 'Changes within subfolders won\'t trigger this node',
name: 'asas',
type: 'notice',
displayOptions: {
show: {
triggerOn: [
'specificFolder',
],
},
hide: {
event: [
'watchFolderUpdated',
],
},
},
default: '',
},
{
displayName: 'Drive To Watch',
name: 'driveToWatch',
type: 'options',
displayOptions: {
show: {
triggerOn: [
'anyFileFolder',
],
},
},
typeOptions: {
loadOptionsMethod: 'getDrives',
},
default: 'root',
required: true,
description: 'The drive to monitor',
},
{
displayName: 'Watch For',
name: 'event',
type: 'options',
displayOptions: {
show: {
triggerOn: [
'anyFileFolder',
],
},
},
required: true,
default: 'fileCreated',
options: [
{
name: 'File Created',
value: 'fileCreated',
description: 'When a file is created in the watched drive',
},
{
name: 'File Updated',
value: 'fileUpdated',
description: 'When a file is updated in the watched drive',
},
{
name: 'Folder Created',
value: 'folderCreated',
description: 'When a folder is created in the watched drive',
},
{
name: 'Folder Updated',
value: 'folderUpdated',
description: 'When a folder is updated in the watched drive',
},
],
description: 'When to trigger this node',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
event: [
'fileCreated',
'fileUpdated',
],
},
hide: {
triggerOn: [
'specificFile',
],
},
},
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'File Type',
name: 'fileType',
type: 'options',
options: [
{
name: '[All]',
value: 'all',
},
{
name: 'Audio',
value: 'application/vnd.google-apps.audio',
},
{
name: 'Google Docs',
value: 'application/vnd.google-apps.document',
},
{
name: 'Google Drawings',
value: 'application/vnd.google-apps.drawing',
},
{
name: 'Google Slides',
value: 'application/vnd.google-apps.presentation',
},
{
name: 'Google Spreadsheets',
value: 'application/vnd.google-apps.spreadsheet',
},
{
name: 'Photos and Images',
value: 'application/vnd.google-apps.photo',
},
{
name: 'Videos',
value: 'application/vnd.google-apps.video',
},
],
default: 'all',
description: 'Triggers only when the file is this type',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the calendars to display them to user so that he can
// select them easily
async getDrives(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const drives = await googleApiRequestAllItems.call(this, 'drives', 'GET', `/drive/v3/drives`);
returnData.push({
name: 'Root',
value: 'root',
});
for (const drive of drives) {
returnData.push({
name: drive.name,
value: drive.id,
});
}
return returnData;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const triggerOn = this.getNodeParameter('triggerOn') as string;
const event = this.getNodeParameter('event') as string;
const webhookData = this.getWorkflowStaticData('node');
const options = this.getNodeParameter('options', {}) as IDataObject;
const qs: IDataObject = {};
const now = moment().utc().format();
const startDate = webhookData.lastTimeChecked as string || now;
const endDate = now;
const query = [
'trashed = false',
];
if (triggerOn === 'specificFolder' && event !== 'watchFolderUpdated') {
const folderToWatch = extractId(this.getNodeParameter('folderToWatch') as string);
query.push(`'${folderToWatch}' in parents`);
}
// if (triggerOn === 'anyFileFolder') {
// const driveToWatch = this.getNodeParameter('driveToWatch');
// query.push(`'${driveToWatch}' in parents`);
// }
if (event.startsWith('file')) {
query.push(`mimeType != 'application/vnd.google-apps.folder'`);
} else {
query.push(`mimeType = 'application/vnd.google-apps.folder'`);
}
if (options.fileType && options.fileType !== 'all') {
query.push(`mimeType = '${options.fileType}'`);
}
if (this.getMode() !== 'manual') {
if (event.includes('Created')) {
query.push(`createdTime > '${startDate}'`);
} else {
query.push(`modifiedTime > '${startDate}'`);
}
}
qs.q = query.join(' AND ');
qs.fields = 'nextPageToken, files(*)';
let files;
if (this.getMode() === 'manual') {
qs.pageSize = 1;
files = await googleApiRequest.call(this, 'GET', `/drive/v3/files`, {}, qs);
files = files.files;
} else {
files = await googleApiRequestAllItems.call(this, 'files', 'GET', `/drive/v3/files`, {}, qs);
}
if (triggerOn === 'specificFile' && this.getMode() !== 'manual') {
const fileToWatch = extractId(this.getNodeParameter('fileToWatch') as string);
files = files.filter((file: { id: string }) => file.id === fileToWatch);
}
if (triggerOn === 'specificFolder' && event === 'watchFolderUpdated' && this.getMode() !== 'manual') {
const folderToWatch = extractId(this.getNodeParameter('folderToWatch') as string);
files = files.filter((file: { id: string }) => file.id === folderToWatch);
}
webhookData.lastTimeChecked = endDate;
if (Array.isArray(files) && files.length) {
return [this.helpers.returnJsonArray(files)];
}
if (this.getMode() === 'manual') {
throw new NodeApiError(this.getNode(), { message: 'No data with the current filter could be found' });
}
return null;
}
}

View file

@ -3,10 +3,13 @@ import {
} from 'n8n-core';
import {
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
@ -71,6 +74,7 @@ export class HomeAssistant implements INodeType {
{
name: 'homeAssistantApi',
required: true,
testedBy: 'homeAssistantApiTest',
},
],
properties: [
@ -133,6 +137,43 @@ export class HomeAssistant implements INodeType {
],
};
methods = {
credentialTest: {
async homeAssistantApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
const credentials = credential.data;
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${credentials!.accessToken}`,
},
uri: `${credentials!.ssl === true ? 'https' : 'http'}://${credentials!.host}:${ credentials!.port || '8123' }/api/`,
json: true,
timeout: 5000,
};
try {
const response = await this.helpers.request(options);
if (!response.message) {
return {
status: 'Error',
message: `Token is not valid: ${response.error}`,
};
}
} catch (error) {
return {
status: 'Error',
message: `${error.statusCode === 401 ? 'Token is' : 'Settings are'} not valid: ${error}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];

View file

@ -1,3 +1,7 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
} from 'n8n-core';
@ -5,12 +9,15 @@ import {
import {
IBinaryData,
IBinaryKeyData,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
NodeCredentialTestResult,
NodeOperationError,
} from 'n8n-workflow';
@ -74,6 +81,7 @@ export class Jira implements INodeType {
],
},
},
testedBy: 'jiraSoftwareApiTest',
},
{
name: 'jiraSoftwareServerApi',
@ -85,6 +93,7 @@ export class Jira implements INodeType {
],
},
},
testedBy: 'jiraSoftwareApiTest',
},
],
properties: [
@ -145,6 +154,40 @@ export class Jira implements INodeType {
};
methods = {
credentialTest: {
async jiraSoftwareApiTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
const credentials = credential.data;
const data = Buffer.from(`${credentials!.email}:${credentials!.password || credentials!.apiToken}`).toString('base64');
const options: OptionsWithUri = {
headers: {
Authorization: `Basic ${data}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Atlassian-Token': 'no-check',
},
method: 'GET',
uri: `${credentials!.domain}/rest/api/2/project`,
qs: {
recent: 0,
},
json: true,
timeout: 5000,
};
try {
await this.helpers.request!(options);
} catch (err) {
return {
status: 'Error',
message: `Connection details not valid; ${err.message}`,
};
}
return {
status: 'OK',
message: 'Authentication successful!',
};
},
},
loadOptions: {
// Get all the projects to display them to user so that he can
// select them easily
@ -370,7 +413,7 @@ export class Jira implements INodeType {
}
const res = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/createmeta?projectIds=${projectId}&issueTypeIds=${issueTypeId}&expand=projects.issuetypes.fields`, 'GET');
// tslint:disable-next-line: no-any
const fields = res.projects.find((o: any) => o.id === projectId).issuetypes.find((o: any) => o.id === issueTypeId).fields;
for (const key of Object.keys(fields)) {
@ -378,7 +421,7 @@ export class Jira implements INodeType {
if (field.schema && Object.keys(field.schema).includes('customId')) {
returnData.push({
name: field.name,
value: field.key,
value: field.key || field.fieldId,
});
}
}

View file

@ -56,6 +56,7 @@ export const folderFields = [
name: 'name',
required: true,
type: 'string',
placeholder: '/Pictures/2021',
displayOptions: {
show: {
operation: [
@ -67,7 +68,7 @@ export const folderFields = [
},
},
default: '',
description: `Folder's name`,
description: 'The name or path of the folder',
},
{
displayName: 'Options',

View file

@ -97,7 +97,7 @@ export class MicrosoftOneDrive implements INodeType {
body.name = additionalFields.name as string;
}
responseData = await microsoftApiRequest.call(this, 'POST', `/drive/items/${fileId}/copy`, body, {}, undefined, {}, { json: true, resolveWithFullResponse: true });
responseData = { location : responseData.headers.location };
responseData = { location: responseData.headers.location };
returnData.push(responseData as IDataObject);
}
//https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete?view=odsp-graph-online
@ -193,7 +193,7 @@ export class MicrosoftOneDrive implements INodeType {
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
const body = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body, {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length }, {} );
responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName || binaryData.fileName}:/content`, body, {}, undefined, { 'Content-Type': binaryData.mimeType, 'Content-length': body.length }, {});
returnData.push(JSON.parse(responseData) as IDataObject);
} else {
@ -201,7 +201,7 @@ export class MicrosoftOneDrive implements INodeType {
if (fileName === '') {
throw new NodeOperationError(this.getNode(), 'File name must be set!');
}
responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}:/content`, body , {}, undefined, { 'Content-Type': 'text/plain' } );
responseData = await microsoftApiRequest.call(this, 'PUT', `/drive/items/${parentId}:/${fileName}:/content`, body, {}, undefined, { 'Content-Type': 'text/plain' });
returnData.push(responseData as IDataObject);
}
}
@ -209,17 +209,24 @@ export class MicrosoftOneDrive implements INodeType {
if (resource === 'folder') {
//https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const names = (this.getNodeParameter('name', i) as string).split('/').filter(s => s.trim() !== '');
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {
name,
folder: {},
};
let endpoint = '/drive/root/children';
if (options.parentFolderId) {
endpoint = `/drive/items/${options.parentFolderId}/children`;
let parentFolderId = options.parentFolderId ? options.parentFolderId : null;
for (const name of names) {
const body: IDataObject = {
name,
folder: {},
};
let endpoint = '/drive/root/children';
if (parentFolderId) {
endpoint = `/drive/items/${parentFolderId}/children`;
}
responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
if (!responseData.id) {
break;
}
parentFolderId = responseData.id;
}
responseData = await microsoftApiRequest.call(this, 'POST', endpoint, body);
returnData.push(responseData);
}
//https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete?view=odsp-graph-online

View file

@ -25,6 +25,9 @@ export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFun
qs,
uri: uri || `${credentials.url}${resource}`,
json: true,
qsStringifyOptions: {
arrayFormat: 'indice',
},
};
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.141.0",
"version": "0.142.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -330,6 +330,7 @@
"dist/nodes/Aws/S3/AwsS3.node.js",
"dist/nodes/Aws/SES/AwsSes.node.js",
"dist/nodes/Aws/SQS/AwsSqs.node.js",
"dist/nodes/Aws/Textract/AwsTextract.node.js",
"dist/nodes/Aws/Transcribe/AwsTranscribe.node.js",
"dist/nodes/Aws/AwsSns.node.js",
"dist/nodes/Aws/AwsSnsTrigger.node.js",
@ -419,6 +420,7 @@
"dist/nodes/Google/Contacts/GoogleContacts.node.js",
"dist/nodes/Google/Docs/GoogleDocs.node.js",
"dist/nodes/Google/Drive/GoogleDrive.node.js",
"dist/nodes/Google/Drive/GoogleDriveTrigger.node.js",
"dist/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.js",
"dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js",
"dist/nodes/Google/Gmail/Gmail.node.js",
@ -663,7 +665,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^26.4.2",
"n8n-workflow": "~0.72.0",
"n8n-workflow": "~0.73.0",
"nodelinter": "^0.1.9",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
@ -703,7 +705,7 @@
"mssql": "^6.2.0",
"mysql2": "~2.3.0",
"node-ssh": "^12.0.0",
"n8n-core": "~0.89.0",
"n8n-core": "~0.90.0",
"nodemailer": "^6.5.0",
"pdf-parse": "^1.1.1",
"pg": "^8.3.0",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.72.0",
"version": "0.73.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",