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 0000000000..a85a7fb9f7 Binary files /dev/null and b/packages/nodes-base/nodes/Box/box.png differ 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 };