From 837094071387c8d471398ee5b523f9fab3b625f3 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 25 Jul 2020 13:58:38 -0400 Subject: [PATCH] :sparkles: Box Node and Trigger (#765) * :sparkles: Box Node and Trigger * :zap: Improvements * :zap: small fix * :bug: Add missing interface * :zap: add search operation --- packages/core/src/Interfaces.ts | 15 +- packages/core/src/NodeExecuteFunctions.ts | 47 +- .../credentials/BoxOAuth2Api.credentials.ts | 46 ++ packages/nodes-base/nodes/Box/Box.node.ts | 361 +++++++++++ .../nodes-base/nodes/Box/BoxTrigger.node.ts | 354 +++++++++++ .../nodes-base/nodes/Box/FileDescription.ts | 596 ++++++++++++++++++ .../nodes-base/nodes/Box/FolderDescription.ts | 417 ++++++++++++ .../nodes-base/nodes/Box/GenericFunctions.ts | 85 +++ packages/nodes-base/nodes/Box/box.png | Bin 0 -> 10224 bytes .../nodes/Hubspot/GenericFunctions.ts | 2 +- .../nodes/Mailchimp/GenericFunctions.ts | 2 +- .../nodes/Slack/GenericFunctions.ts | 11 +- packages/nodes-base/package.json | 3 + packages/workflow/src/Interfaces.ts | 6 +- 14 files changed, 1916 insertions(+), 29 deletions(-) create mode 100644 packages/nodes-base/credentials/BoxOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Box/Box.node.ts create mode 100644 packages/nodes-base/nodes/Box/BoxTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Box/FileDescription.ts create mode 100644 packages/nodes-base/nodes/Box/FolderDescription.ts create mode 100644 packages/nodes-base/nodes/Box/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Box/box.png diff --git a/packages/core/src/Interfaces.ts b/packages/core/src/Interfaces.ts index 5aa0e10a64..483cc1c0cd 100644 --- a/packages/core/src/Interfaces.ts +++ b/packages/core/src/Interfaces.ts @@ -15,6 +15,7 @@ import { ITriggerResponse, IWebhookFunctions as IWebhookFunctionsBase, IWorkflowSettings as IWorkflowSettingsWorkflow, + IOAuth2Options, } from 'n8n-workflow'; @@ -36,7 +37,7 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; @@ -47,7 +48,7 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -57,7 +58,7 @@ export interface IPollFunctions extends IPollFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; @@ -73,7 +74,7 @@ export interface ITriggerFunctions extends ITriggerFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; @@ -98,7 +99,7 @@ export interface IUserSettings { export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { helpers: { request?: requestPromise.RequestPromiseAPI, - requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise, // tslint:disable-line:no-any + requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options) => Promise, // tslint:disable-line:no-any requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -107,7 +108,7 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase { export interface IHookFunctions extends IHookFunctionsBase { helpers: { request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any }; } @@ -117,7 +118,7 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase { helpers: { prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise; request: requestPromise.RequestPromiseAPI, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise, // tslint:disable-line:no-any requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise, // tslint:disable-line:no-any returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[]; }; diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 804aa5ca2d..be0cf64975 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -34,6 +34,7 @@ import { Workflow, WorkflowDataProxy, WorkflowExecuteMode, + IOAuth2Options, } from 'n8n-workflow'; import * as clientOAuth1 from 'oauth-1.0a'; @@ -124,9 +125,10 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m * @param {(OptionsWithUri | requestPromise.RequestPromiseOptions)} requestOptions * @param {INode} node * @param {IWorkflowExecuteAdditionalData} additionalData + * * @returns */ -export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) { +export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, oAuth2Options?: IOAuth2Options) { const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject; if (credentials === undefined) { @@ -145,7 +147,7 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin const oauthTokenData = credentials.oauthTokenData as clientOAuth2.Data; - const token = oAuthClient.createToken(get(oauthTokenData, property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, tokenType || oauthTokenData.tokenType, oauthTokenData); + const token = oAuthClient.createToken(get(oauthTokenData, oAuth2Options?.property as string) || oauthTokenData.accessToken, oauthTokenData.refreshToken, oAuth2Options?.tokenType || oauthTokenData.tokenType, oauthTokenData); // Signs the request by adding authorization headers or query parameters depending // on the token-type used. const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject); @@ -156,7 +158,18 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin if (error.statusCode === 401) { // TODO: Whole refresh process is not tested yet // Token is probably not valid anymore. So try refresh it. - const newToken = await token.refresh(); + + const tokenRefreshOptions: IDataObject = {}; + + if (oAuth2Options?.includeCredentialsOnRefreshOnBody) { + const body: IDataObject = { + client_id: credentials.clientId as string, + client_secret: credentials.clientSecret as string, + }; + tokenRefreshOptions.body = body; + } + + const newToken = await token.refresh(tokenRefreshOptions); credentials.oauthTokenData = newToken.data; @@ -543,8 +556,8 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -606,8 +619,8 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -702,8 +715,8 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -800,8 +813,8 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -856,8 +869,8 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -923,8 +936,8 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); @@ -1017,8 +1030,8 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise { // tslint:disable-line:no-any - return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property); + requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, oAuth2Options?: IOAuth2Options): Promise { // tslint:disable-line:no-any + return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, oAuth2Options); }, requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth1.call(this, credentialsType, requestOptions); diff --git a/packages/nodes-base/credentials/BoxOAuth2Api.credentials.ts b/packages/nodes-base/credentials/BoxOAuth2Api.credentials.ts new file mode 100644 index 0000000000..ccf39e7b5d --- /dev/null +++ b/packages/nodes-base/credentials/BoxOAuth2Api.credentials.ts @@ -0,0 +1,46 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class BoxOAuth2Api implements ICredentialType { + name = 'boxOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Box OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://account.box.com/api/oauth2/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.box.com/oauth2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Box/Box.node.ts b/packages/nodes-base/nodes/Box/Box.node.ts new file mode 100644 index 0000000000..b402ba0dbd --- /dev/null +++ b/packages/nodes-base/nodes/Box/Box.node.ts @@ -0,0 +1,361 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + IBinaryKeyData, +} from 'n8n-workflow'; + +import { + boxApiRequest, + boxApiRequestAllItems, +} from './GenericFunctions'; + +import { + fileFields, + fileOperations, +} from './FileDescription'; + +import { + folderFields, + folderOperations, +} from './FolderDescription'; + +import * as moment from 'moment-timezone'; + +export class Box implements INodeType { + description: INodeTypeDescription = { + displayName: 'Box', + name: 'box', + icon: 'file:box.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Box API', + defaults: { + name: 'Box', + color: '#00aeef', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'boxOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'File', + value: 'file', + }, + { + name: 'Folder', + value: 'folder', + }, + ], + default: 'file', + description: 'The resource to operate on.', + }, + ...fileOperations, + ...fileFields, + ...folderOperations, + ...folderFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'file') { + //https://developer.box.com/reference/post-files-id-copy + if (operation === 'copy') { + const fileId = this.getNodeParameter('fileId', i) as string; + const parentId = this.getNodeParameter('parentId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = {}; + if (additionalFields.name) { + body.name = additionalFields.name as string; + } + if (parentId) { + body.parent = { id: parentId }; + } else { + body.parent = { id: 0 }; + } + if (additionalFields.fields) { + qs.fields = additionalFields.fields as string; + } + if (additionalFields.version) { + body.version = additionalFields.version as string; + } + responseData = await boxApiRequest.call(this, 'POST', `/files/${fileId}/copy`, body, qs); + + returnData.push(responseData as IDataObject); + } + //https://developer.box.com/reference/delete-files-id + if (operation === 'delete') { + const fileId = this.getNodeParameter('fileId', i) as string; + responseData = await boxApiRequest.call(this, 'DELETE', `/files/${fileId}`); + responseData = { success: true }; + returnData.push(responseData as IDataObject); + } + //https://developer.box.com/reference/get-files-id-content + if (operation === 'download') { + const fileId = this.getNodeParameter('fileId', i) as string; + const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; + responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}`); + + const fileName = responseData.name; + + let mimeType: string | undefined; + + responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}/content`, {}, {}, undefined, { resolveWithFullResponse: true }); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (mimeType === undefined && responseData.headers['content-type']) { + mimeType = responseData.headers['content-type']; + } + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + const data = Buffer.from(responseData.body); + + items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType); + } + //https://developer.box.com/reference/get-files-id + if (operation === 'get') { + const fileId = this.getNodeParameter('fileId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + if (additionalFields.fields) { + qs.fields = additionalFields.fields as string; + } + responseData = await boxApiRequest.call(this, 'GET', `/files/${fileId}`, {}, qs); + returnData.push(responseData as IDataObject); + } + //https://developer.box.com/reference/get-search/ + if (operation === 'search') { + const query = this.getNodeParameter('query', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const timezone = this.getTimezone(); + qs.type = 'file'; + qs.query = query; + Object.assign(qs, additionalFields); + + if (qs.content_types) { + qs.content_types = (qs.content_types as string).split(','); + } + + if (additionalFields.createdRangeUi) { + const createdRangeValues = (additionalFields.createdRangeUi as IDataObject).createdRangeValuesUi as IDataObject; + if (createdRangeValues) { + qs.created_at_range = `${moment.tz(createdRangeValues.from, timezone).format()},${moment.tz(createdRangeValues.to, timezone).format()}`; + } + delete qs.createdRangeUi; + } + + if (additionalFields.updatedRangeUi) { + const updateRangeValues = (additionalFields.updatedRangeUi as IDataObject).updatedRangeValuesUi as IDataObject; + if (updateRangeValues) { + qs.updated_at_range = `${moment.tz(updateRangeValues.from, timezone).format()},${moment.tz(updateRangeValues.to, timezone).format()}`; + } + delete qs.updatedRangeUi; + } + + if (returnAll) { + responseData = await boxApiRequestAllItems.call(this, 'entries', 'GET', `/search`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await boxApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.entries; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + //https://developer.box.com/reference/post-files-content + if (operation === 'upload') { + const parentId = this.getNodeParameter('parentId', i) as string; + const isBinaryData = this.getNodeParameter('binaryData', i) as boolean; + const fileName = this.getNodeParameter('fileName', i) as string; + + const attributes: IDataObject = {}; + + if (parentId !== '') { + attributes['parent'] = { id: parentId }; + } else { + // if not parent defined save it on the root directory + attributes['parent'] = { id: 0 }; + } + + if (isBinaryData) { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[binaryPropertyName] === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName]; + + const body: IDataObject = {}; + + attributes['name'] = fileName || binaryData.fileName; + + body['attributes'] = JSON.stringify(attributes); + + body['file'] = { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }; + + responseData = await boxApiRequest.call(this, 'POST', '', {}, {}, 'https://upload.box.com/api/2.0/files/content', { formData: body }); + + returnData.push.apply(returnData, responseData.entries as IDataObject[]); + + } else { + const content = this.getNodeParameter('fileContent', i) as string; + + if (fileName === '') { + throw new Error('File name must be set!'); + } + + attributes['name'] = fileName; + + const body: IDataObject = {}; + + body['attributes'] = JSON.stringify(attributes); + + body['file'] = { + value: Buffer.from(content), + options: { + filename: fileName, + contentType: 'text/plain', + }, + }; + responseData = await boxApiRequest.call(this, 'POST', '', {} , {}, 'https://upload.box.com/api/2.0/files/content', { formData: body }); + + returnData.push.apply(returnData, responseData.entries as IDataObject[]); + } + } + } + if (resource === 'folder') { + //https://developer.box.com/reference/post-folders + if (operation === 'create') { + const name = this.getNodeParameter('name', i) as string; + const parentId = this.getNodeParameter('parentId', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const body: IDataObject = { + name, + }; + + if (parentId) { + body.parent = { id: parentId }; + } else { + body.parent = { id: 0 }; + } + + if (options.access) { + body.folder_upload_email = { + access: options.access as string, + }; + } + + if (options.fields) { + qs.fields = options.fields as string; + } + + responseData = await boxApiRequest.call(this, 'POST', '/folders', body, qs); + + returnData.push(responseData); + } + //https://developer.box.com/reference/delete-folders-id + if (operation === 'delete') { + const folderId = this.getNodeParameter('folderId', i) as string; + const recursive = this.getNodeParameter('recursive', i) as boolean; + qs.recursive = recursive; + + responseData = await boxApiRequest.call(this, 'DELETE', `/folders/${folderId}`, qs); + responseData = { success: true }; + returnData.push(responseData as IDataObject); + } + //https://developer.box.com/reference/get-search/ + if (operation === 'search') { + const query = this.getNodeParameter('query', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const timezone = this.getTimezone(); + qs.type = 'folder'; + qs.query = query; + Object.assign(qs, additionalFields); + + if (qs.content_types) { + qs.content_types = (qs.content_types as string).split(','); + } + + if (additionalFields.createdRangeUi) { + const createdRangeValues = (additionalFields.createdRangeUi as IDataObject).createdRangeValuesUi as IDataObject; + if (createdRangeValues) { + qs.created_at_range = `${moment.tz(createdRangeValues.from, timezone).format()},${moment.tz(createdRangeValues.to, timezone).format()}`; + } + delete qs.createdRangeUi; + } + + if (additionalFields.updatedRangeUi) { + const updateRangeValues = (additionalFields.updatedRangeUi as IDataObject).updatedRangeValuesUi as IDataObject; + if (updateRangeValues) { + qs.updated_at_range = `${moment.tz(updateRangeValues.from, timezone).format()},${moment.tz(updateRangeValues.to, timezone).format()}`; + } + delete qs.updatedRangeUi; + } + + if (returnAll) { + responseData = await boxApiRequestAllItems.call(this, 'entries', 'GET', `/search`, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await boxApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.entries; + } + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + } + if (resource === 'file' && operation === 'download') { + // For file downloads the files get attached to the existing items + return this.prepareOutputData(items); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Box/BoxTrigger.node.ts b/packages/nodes-base/nodes/Box/BoxTrigger.node.ts new file mode 100644 index 0000000000..f11225b2f6 --- /dev/null +++ b/packages/nodes-base/nodes/Box/BoxTrigger.node.ts @@ -0,0 +1,354 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + boxApiRequest, + boxApiRequestAllItems, +} from './GenericFunctions'; + +export class BoxTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Box Trigger', + name: 'boxTrigger', + icon: 'file:box.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a Github events occurs.', + defaults: { + name: 'Box Trigger', + color: '#00aeef', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'boxOAuth2Api', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: 'Collaboration Created', + value: 'COLLABORATION.CREATED', + description: 'A collaboration is created', + }, + { + name: 'Collaboration Accepted', + value: 'COLLABORATION.ACCEPTED', + description: 'A collaboration has been accepted', + }, + { + name: 'Collaboration Rejected', + value: 'COLLABORATION.REJECTED', + description: 'A collaboration has been rejected', + }, + { + name: 'Collaboration Removed', + value: 'COLLABORATION.REMOVED', + description: 'A collaboration has been removed', + }, + { + name: 'Collaboration Updated', + value: 'COLLABORATION.UPDATED', + description: 'A collaboration has been updated.', + }, + { + name: 'Comment Created', + value: 'COMMENT.CREATED', + description: 'A comment object is created', + }, + { + name: 'Comment Updated', + value: 'COMMENT.UPDATED', + description: 'A comment object is edited', + }, + { + name: 'Comment Deleted', + value: 'COMMENT.DELETED', + description: 'A comment object is removed', + }, + { + name: 'File Uploaded', + value: 'FILE.UPLOADED', + description: 'A file is uploaded to or moved to this folder', + }, + { + name: 'File Previewed', + value: 'FILE.PREVIEWED', + description: 'A file is previewed', + }, + { + name: 'File Downloaded', + value: 'FILE.DOWNLOADED', + description: 'A file is downloaded', + }, + { + name: 'File Trashed', + value: 'FILE.TRASHED', + description: 'A file is moved to the trash', + }, + { + name: 'File Deleted', + value: 'FILE.DELETED', + description: 'A file is moved to the trash', + }, + { + name: 'File Restored', + value: 'FILE.RESTORED', + description: 'A file is restored from the trash', + }, + { + name: 'File Copied', + value: 'FILE.COPIED', + description: 'A file is copied', + }, + { + name: 'File Moved', + value: 'FILE.MOVED', + description: 'A file is moved from one folder to another', + }, + { + name: 'File Locked', + value: 'FILE.LOCKED', + description: 'A file is locked', + }, + { + name: 'File Unlocked', + value: 'FILE.UNLOCKED', + description: 'A file is unlocked', + }, + { + name: 'File Renamed', + value: 'FILE.RENAMED', + description: 'A file was renamed.', + }, + { + name: 'Folder Created', + value: 'FOLDER.CREATED', + description: 'A folder is created', + }, + { + name: 'Folder Renamed', + value: 'FOLDER.RENAMED', + description: 'A folder was renamed.', + }, + { + name: 'Folder Downloaded', + value: 'FOLDER.DOWNLOADED', + description: 'A folder is downloaded', + }, + { + name: 'Folder Restored', + value: 'FOLDER.RESTORED', + description: 'A folder is restored from the trash', + }, + { + name: 'Folder Deleted', + value: 'FOLDER.DELETED', + description: 'A folder is permanently removed', + }, + { + name: 'Folder Copied', + value: 'FOLDER.COPIED', + description: 'A copy of a folder is made', + }, + { + name: 'Folder Moved', + value: 'FOLDER.MOVED', + description: 'A folder is moved to a different folder', + }, + { + name: 'Folder Trashed', + value: 'FOLDER.TRASHED', + description: 'A folder is moved to the trash', + }, + { + name: 'Metadata Instance Created', + value: 'METADATA_INSTANCE.CREATED', + description: 'A new metadata template instance is associated with a file or folder', + }, + { + name: 'Metadata Instance Updated', + value: 'METADATA_INSTANCE.UPDATED', + description: 'An attribute (value) is updated/deleted for an existing metadata template instance associated with a file or folder', + }, + { + name: 'Metadata Instance Deleted', + value: 'METADATA_INSTANCE.DELETED', + description: 'An existing metadata template instance associated with a file or folder is deleted', + }, + { + name: 'Sharedlink Deleted', + value: 'SHARED_LINK.DELETED', + description: 'A shared link was deleted', + }, + { + name: 'Sharedlink Created', + value: 'SHARED_LINK.CREATED', + description: 'A shared link was created', + }, + { + name: 'Sharedlink UPDATED', + value: 'SHARED_LINK.UPDATED', + description: 'A shared link was updated', + }, + { + name: 'Task Assignment Created', + value: 'TASK_ASSIGNMENT.CREATED', + description: 'A task is created', + }, + { + name: 'Task Assignment Updated', + value: 'TASK_ASSIGNMENT.UPDATED', + description: 'A task is updated', + }, + { + name: 'Webhook Deleted', + value: 'WEBHOOK.DELETED', + description: 'When a webhook is deleted', + }, + ], + required: true, + default: [], + description: 'The events to listen to.', + }, + { + displayName: 'Target Type', + name: 'targetType', + type: 'options', + options: [ + { + name: 'File', + value: 'file', + }, + { + name: 'Folder', + value: 'folder', + }, + ], + default: '', + description: 'The type of item to trigger a webhook', + }, + { + displayName: 'Target ID', + name: 'targetId', + type: 'string', + required: false, + default: '', + description: 'The ID of the item to trigger a webhook', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const events = this.getNodeParameter('events') as string; + const targetId = this.getNodeParameter('targetId') as string; + const targetType = this.getNodeParameter('targetType') as string; + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/webhooks'; + const webhooks = await boxApiRequestAllItems.call(this, 'entries', 'GET', endpoint, {}); + + console.log(webhooks); + + for (const webhook of webhooks) { + if (webhook.address === webhookUrl && + webhook.target.id === targetId && + webhook.target.type === targetType) { + for (const event of events) { + if (!webhook.triggers.includes(event)) { + return false; + } + } + } + // Set webhook-id to be sure that it can be deleted + webhookData.webhookId = webhook.id as string; + return true; + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events') as string; + const targetId = this.getNodeParameter('targetId') as string; + const targetType = this.getNodeParameter('targetType') as string; + + const endpoint = '/webhooks'; + + const body = { + address: webhookUrl, + triggers: events, + target: { + id: targetId, + type: targetType, + } + }; + + const responseData = await boxApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/webhooks/${webhookData.webhookId}`; + + try { + await boxApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + + return { + workflowData: [ + this.helpers.returnJsonArray(bodyData) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Box/FileDescription.ts b/packages/nodes-base/nodes/Box/FileDescription.ts new file mode 100644 index 0000000000..71ab3ce35c --- /dev/null +++ b/packages/nodes-base/nodes/Box/FileDescription.ts @@ -0,0 +1,596 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const fileOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'file', + ], + }, + }, + options: [ + { + name: 'Copy', + value: 'copy', + description: 'Copy a file', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + }, + { + name: 'Download', + value: 'download', + description: 'Download a file', + }, + { + name: 'Get', + value: 'get', + description: 'Get a file', + }, + { + name: 'Search', + value: 'search', + description: 'Search files', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a file', + }, + ], + default: 'upload', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fileFields = [ + +/* -------------------------------------------------------------------------- */ +/* file:copy */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'File ID', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + description: 'The ID of folder to copy the file to. If not defined will be copied to the root folder', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'copy', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'An optional new name for the copied file.', + }, + { + displayName: 'Version', + name: 'version', + type: 'string', + default: '', + description: 'An optional ID of the specific file version to copy.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* file:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'Field ID', + }, +/* -------------------------------------------------------------------------- */ +/* file:download */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'download', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'File ID', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'download' + ], + resource: [ + 'file', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read file.', + }, +/* -------------------------------------------------------------------------- */ +/* file:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'Field ID', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* file:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'The string to search for. This query is matched against item names, descriptions, text content of files, and various other fields of the different item types.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'file', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'file', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'file', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Content Types', + name: 'contet_types', + type: 'string', + default: '', + description: `Limits search results to items with the given content types.
+ Content types are defined as a comma separated lists of Box recognized content types.`, + }, + { + displayName: 'Created At Range', + name: 'createdRangeUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range', + name: 'createdRangeValuesUi', + values: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + description: 'Defines the direction in which search results are ordered. Default value is DESC.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'File Extensions', + name: 'file_extensions', + type: 'string', + default: '', + placeholder: 'pdf,png,gif', + description: 'Limits search results to a comma-separated list of file extensions.', + }, + { + displayName: 'Folder IDs', + name: 'ancestor_folder_ids', + type: 'string', + default: '', + description: `Limits search results to items within the given list of folders.
+ Folders are defined as a comma separated lists of folder IDs.`, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'options', + options: [ + { + name: 'User Content', + value: 'user_content', + }, + { + name: 'Enterprise Content', + value: 'enterprise_content', + }, + ], + default: '', + description: 'Limits search results to a user scope.', + }, + { + displayName: 'Size Range', + name: 'size_range', + type: 'string', + default: '', + placeholder: '1000000,5000000', + description: `Limits search results to items within a given file size range.
+ File size ranges are defined as comma separated byte sizes.`, + }, + { + displayName: 'Sort', + name: 'sort', + type: 'options', + options: [ + { + name: 'Relevance', + value: 'relevance', + }, + { + name: 'Modified At', + value: 'modified_at', + }, + ], + default: 'relevance', + description: 'returns the results ordered in descending order by date at which the item was last modified.', + }, + { + displayName: 'Trash Content', + name: 'trash_content', + type: 'options', + options: [ + { + name: 'Non Trashed Only', + value: 'non_trashed_only', + }, + { + name: 'Trashed Only', + value: 'trashed_only', + }, + ], + default: 'non_trashed_only', + description: 'Controls if search results include the trash.', + }, + { + displayName: 'Update At Range', + name: 'updatedRangeUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range', + name: 'updatedRangeValuesUi', + values: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'User IDs', + name: 'owner_user_ids', + type: 'string', + default: '', + description: `Limits search results to items owned by the given list of owners..
+ Owners are defined as a comma separated list of user IDs.`, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* file:upload */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + placeholder: 'photo.png', + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'The name the file should be saved as.', + }, + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + description: 'If the data to upload should be taken from binary field.', + }, + { + displayName: 'File Content', + name: 'fileContent', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + binaryData: [ + false, + ], + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + + }, + placeholder: '', + description: 'The text content of the file.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + binaryData: [ + true, + ], + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file.', + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'upload', + ], + resource: [ + 'file', + ], + }, + }, + default: '', + description: 'ID of the parent folder that will contain the file. If not it will be uploaded to the root folder', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Box/FolderDescription.ts b/packages/nodes-base/nodes/Box/FolderDescription.ts new file mode 100644 index 0000000000..0d97ae173d --- /dev/null +++ b/packages/nodes-base/nodes/Box/FolderDescription.ts @@ -0,0 +1,417 @@ +import { + INodeProperties, + } from 'n8n-workflow'; + +export const folderOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'folder', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a folder', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a folder', + }, + { + name: 'Search', + value: 'search', + description: 'Search files', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const folderFields = [ + +/* -------------------------------------------------------------------------- */ +/* folder:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: `Folder's name`, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'ID of the folder you want to create the new folder in. if not defined it will be created on the root folder', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'folder', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Access', + name: 'access', + type: 'options', + options: [ + { + name: 'Collaborators', + value: 'collaborators', + description: 'Only emails from registered email addresses for collaborators will be accepted.', + }, + { + name: 'Open', + value: 'open', + description: 'It will accept emails from any email addres', + }, + ], + default: '', + description: 'ID of the folder you want to create the new folder in', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* folder:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'Folder ID', + }, + { + displayName: 'Recursive', + name: 'recursive', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource: [ + 'folder', + ], + }, + }, + default: false, + description: 'Delete a folder that is not empty by recursively deleting the folder and all of its content.', + }, +/* -------------------------------------------------------------------------- */ +/* file:search */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Query', + name: 'query', + type: 'string', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: '', + description: 'The string to search for. This query is matched against item names, descriptions, text content of files, and various other fields of the different item types.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'search', + ], + resource: [ + 'folder', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Content Types', + name: 'contet_types', + type: 'string', + default: '', + description: `Limits search results to items with the given content types.
+ Content types are defined as a comma separated lists of Box recognized content types.`, + }, + { + displayName: 'Created At Range', + name: 'createdRangeUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range', + name: 'createdRangeValuesUi', + values: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + description: 'Defines the direction in which search results are ordered. Default value is DESC.', + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of attributes to include in the response. This can be used to request fields that are not normally returned in a standard response.', + }, + { + displayName: 'File Extensions', + name: 'file_extensions', + type: 'string', + default: '', + placeholder: 'pdf,png,gif', + description: 'Limits search results to a comma-separated list of file extensions.', + }, + { + displayName: 'Folder IDs', + name: 'ancestor_folder_ids', + type: 'string', + default: '', + description: `Limits search results to items within the given list of folders.
+ Folders are defined as a comma separated lists of folder IDs.`, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'options', + options: [ + { + name: 'User Content', + value: 'user_content', + }, + { + name: 'Enterprise Content', + value: 'enterprise_content', + }, + ], + default: '', + description: 'Limits search results to a user scope.', + }, + { + displayName: 'Size Range', + name: 'size_range', + type: 'string', + default: '', + placeholder: '1000000,5000000', + description: `Limits search results to items within a given file size range.
+ File size ranges are defined as comma separated byte sizes.`, + }, + { + displayName: 'Sort', + name: 'sort', + type: 'options', + options: [ + { + name: 'Relevance', + value: 'relevance', + }, + { + name: 'Modified At', + value: 'modified_at', + }, + ], + default: 'relevance', + description: 'returns the results ordered in descending order by date at which the item was last modified.', + }, + { + displayName: 'Trash Content', + name: 'trash_content', + type: 'options', + options: [ + { + name: 'Non Trashed Only', + value: 'non_trashed_only', + }, + { + name: 'Trashed Only', + value: 'trashed_only', + }, + ], + default: 'non_trashed_only', + description: 'Controls if search results include the trash.', + }, + { + displayName: 'Update At Range', + name: 'updatedRangeUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Range', + default: {}, + options: [ + { + displayName: 'Range', + name: 'updatedRangeValuesUi', + values: [ + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'User IDs', + name: 'owner_user_ids', + type: 'string', + default: '', + description: `Limits search results to items owned by the given list of owners..
+ Owners are defined as a comma separated list of user IDs.`, + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Box/GenericFunctions.ts b/packages/nodes-base/nodes/Box/GenericFunctions.ts new file mode 100644 index 0000000000..69d9adc6e1 --- /dev/null +++ b/packages/nodes-base/nodes/Box/GenericFunctions.ts @@ -0,0 +1,85 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IOAuth2Options, +} from 'n8n-workflow'; + +export async function boxApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.box.com/2.0${resource}`, + json: true, + }; + options = Object.assign({}, options, option); + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + const oAuth2Options: IOAuth2Options = { + includeCredentialsOnRefreshOnBody: true, + }; + + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'boxOAuth2Api', options, oAuth2Options); + + } catch (error) { + + let errorMessage; + + if (error.response && error.response.body) { + + if (error.response.body.context_info && error.response.body.context_info.errors) { + + const errors = error.response.body.context_info.errors; + + errorMessage = errors.map((e: IDataObject) => e.message); + + errorMessage = errorMessage.join('|'); + + } else if (error.response.body.message) { + + errorMessage = error.response.body.message; + + } + + throw new Error(`Box error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} + +export async function boxApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.limit = 100; + query.offset = 0; + do { + responseData = await boxApiRequest.call(this, method, endpoint, body, query); + query.offset = responseData['offset'] + query.limit; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData[propertyName].length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Box/box.png b/packages/nodes-base/nodes/Box/box.png new file mode 100644 index 0000000000000000000000000000000000000000..a85a7fb9f7cb08578c2f6788a22eaa1d61713830 GIT binary patch literal 10224 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 BKO6u6 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index d8d68abfb6..b47956d24f 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -45,7 +45,7 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions return await this.helpers.request!(options); } else { // @ts-ignore - return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, 'Bearer'); + return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer' }); } } catch (error) { let errorMessages; diff --git a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts index 99c0af67fd..442e6ac011 100644 --- a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts @@ -57,7 +57,7 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio options.url = `${api_endpoint}/3.0${endpoint}`; //@ts-ignore - return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, 'Bearer'); + return await this.helpers.requestOAuth2!.call(this, 'mailchimpOAuth2Api', options, { tokenType: 'Bearer' }); } } catch (error) { if (error.respose && error.response.body && error.response.body.detail) { diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index ad04536b36..5686d9c0e9 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -9,8 +9,10 @@ import { } from 'n8n-core'; import { - IDataObject + IDataObject, + IOAuth2Options, } from 'n8n-workflow'; + import * as _ from 'lodash'; export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: object = {}, query: object = {}, headers: {} | undefined = undefined, option: {} = {}): Promise { // tslint:disable-line:no-any @@ -42,8 +44,13 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu //@ts-ignore return await this.helpers.request(options); } else { + + const oAuth2Options: IOAuth2Options = { + tokenType: 'Bearer', + property: 'authed_user.access_token', + }; //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token'); + return await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, oAuth2Options); } } catch (error) { if (error.statusCode === 401) { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 6cae4ae7eb..15e3fd8461 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -38,6 +38,7 @@ "dist/credentials/BannerbearApi.credentials.js", "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/BitlyApi.credentials.js", + "dist/credentials/BoxOAuth2Api.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", @@ -177,6 +178,8 @@ "dist/nodes/Bannerbear/Bannerbear.node.js", "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Bitly/Bitly.node.js", + "dist/nodes/Box/Box.node.js", + "dist/nodes/Box/BoxTrigger.node.js", "dist/nodes/Calendly/CalendlyTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 0b6a7c79c3..52155e0090 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -12,6 +12,11 @@ export interface IBinaryData { fileExtension?: string; } +export interface IOAuth2Options { + includeCredentialsOnRefreshOnBody?: boolean; + property?: string; + tokenType?: string; +} export interface IConnection { // The node the connection is to @@ -180,7 +185,6 @@ export interface IExecuteData { node: INode; } - export type IContextObject = { [key: string]: any; // tslint:disable-line:no-any };