From e1dbb72929dc2289785643edbf36004020da5646 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 11 Mar 2021 03:16:05 -0500 Subject: [PATCH] :zap: Add file:search operation to Dropbox Node (#1494) * :zap: Add search resource to Dropbox Node * :books: Add breaking change instructions * :zap: Add missing credentials * :zap: Add "simple" parameter to the operation search:query * :zap: Update breaking change message * :zap: Improvement Co-authored-by: Jan Oberhauser --- packages/cli/BREAKING-CHANGES.md | 12 + .../DropboxOAuth2Api.credentials.ts | 3 +- .../nodes-base/nodes/Dropbox/Dropbox.node.ts | 401 +++++++++++++++++- .../nodes/Dropbox/GenericFunctions.ts | 50 ++- packages/nodes-base/nodes/Dropbox/dropbox.png | Bin 1169 -> 0 bytes packages/nodes-base/nodes/Dropbox/dropbox.svg | 1 + 6 files changed, 451 insertions(+), 16 deletions(-) delete mode 100644 packages/nodes-base/nodes/Dropbox/dropbox.png create mode 100644 packages/nodes-base/nodes/Dropbox/dropbox.svg diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 44eddb69a4..f3eca82a12 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,18 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.111.0 + +### What changed? +In the Dropbox node, now all operations are performed relative to the user's root directory. + +### When is action necessary? +If you are using the `folder:list` operation with the parameter `Folder Path` empty (root path) and have a Team Space in your Dropbox account. + +### How to upgrade: +Open the Dropbox node, go to the `folder:list` operation, and make sure your logic is taking into account the team folders in the response. + + ## 0.105.0 ### What changed? diff --git a/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts b/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts index c3c9f248db..81c84d92aa 100644 --- a/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/DropboxOAuth2Api.credentials.ts @@ -7,6 +7,7 @@ const scopes = [ 'files.content.write', 'files.content.read', 'sharing.read', + 'account_info.read', ]; export class DropboxOAuth2Api implements ICredentialType { @@ -41,7 +42,7 @@ export class DropboxOAuth2Api implements ICredentialType { displayName: 'Auth URI Query Parameters', name: 'authQueryParameters', type: 'hidden' as NodePropertyTypes, - default: 'token_access_type=offline', + default: 'token_access_type=offline&force_reapprove=true', }, { displayName: 'Authentication', diff --git a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts index 790b181261..5f4e219652 100644 --- a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts +++ b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts @@ -11,21 +11,24 @@ import { } from 'n8n-workflow'; import { - dropboxApiRequest + dropboxApiRequest, + dropboxpiRequestAllItems, + getRootDirectory, + simplify, } from './GenericFunctions'; export class Dropbox implements INodeType { description: INodeTypeDescription = { displayName: 'Dropbox', name: 'dropbox', - icon: 'file:dropbox.png', + icon: 'file:dropbox.svg', group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Access data on Dropbox', defaults: { name: 'Dropbox', - color: '#0062ff', + color: '#007ee5', }, inputs: ['main'], outputs: ['main'], @@ -84,6 +87,10 @@ export class Dropbox implements INodeType { name: 'Folder', value: 'folder', }, + { + name: 'Search', + value: 'search', + }, ], default: 'file', description: 'The resource to operate on.', @@ -176,6 +183,27 @@ export class Dropbox implements INodeType { description: 'The operation to perform.', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'search', + ], + }, + }, + options: [ + { + name: 'Query', + value: 'query', + }, + ], + default: 'query', + description: 'The operation to perform.', + }, + // ---------------------------------- // file // ---------------------------------- @@ -419,7 +447,189 @@ export class Dropbox implements INodeType { description: 'Name of the binary property which contains
the data for the file to be uploaded.', }, - + // ---------------------------------- + // search:query + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + description: ' The string to search for. May match across multiple fields based on the request arguments.', + }, + { + displayName: 'File Status', + name: 'fileStatus', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Deleted', + value: 'deleted', + }, + ], + default: 'active', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + description: ' The string to search for. May match across multiple fields based on the request arguments.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + returnAll: [ + false, + ], + }, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'query', + ], + resource: [ + 'search', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'search', + ], + operation: [ + 'query', + ], + }, + }, + options: [ + { + displayName: 'File Categories', + name: 'file_categories', + type: 'multiOptions', + options: [ + { + name: 'Audio (mp3, wav, mid, etc.)', + value: 'audio', + }, + { + name: 'Document (doc, docx, txt, etc.)', + value: 'document', + }, + { + name: 'Folder', + value: 'folder', + }, + { + name: 'Image (jpg, png, gif, etc.)', + value: 'image', + }, + { + name: 'Other', + value: 'other', + }, + { + name: 'Dropbox Paper', + value: 'paper', + }, + { + name: 'PDF', + value: 'pdf', + }, + { + name: 'Presentation (ppt, pptx, key, etc.)', + value: 'presentation', + }, + { + name: 'Spreadsheet (xlsx, xls, csv, etc.)', + value: 'spreadsheet', + }, + { + name: 'Video (avi, wmv, mp4, etc.)', + value: 'video', + }, + ], + default: [], + }, + { + displayName: 'File Extensions', + name: 'file_extensions', + type: 'string', + default: '', + description: 'Multiple can be set separated by comma. Example: jpg,pdf', + }, + { + displayName: 'Folder', + name: 'path', + type: 'string', + default: '', + description: 'If this field is not specified, this module searches the entire Dropbox', + }, + ], + }, // ---------------------------------- // folder @@ -469,7 +679,97 @@ export class Dropbox implements INodeType { placeholder: '/invoices/2019/', description: 'The path of which to list the content.', }, - + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'list', + ], + 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: { + resource: [ + 'folder', + ], + operation: [ + 'list', + ], + returnAll: [ + false, + ], + }, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'folder', + ], + operation: [ + 'list', + ], + }, + }, + options: [ + { + displayName: 'Include Deleted', + name: 'include_deleted', + type: 'boolean', + default: false, + description: 'If true, the results will include entries for files and folders that used to exist but were deleted. The default for this field is False.', + }, + { + displayName: 'Include Shared Members ', + name: 'include_has_explicit_shared_members', + type: 'boolean', + default: false, + description: 'If true, the results will include a flag for each file indicating whether or not that file has any explicit members. The default for this field is False.', + }, + { + displayName: 'Include Mounted Folders ', + name: 'include_mounted_folders', + type: 'boolean', + default: true, + description: 'If true, the results will include entries under mounted folders which includes app folder, shared folder and team folder. The default for this field is True.', + }, + { + displayName: 'Include Non Downloadable Files ', + name: 'include_non_downloadable_files', + type: 'boolean', + default: true, + description: 'If true, include files that are not downloadable, i.e. Google Docs. The default for this field is True.', + }, + { + displayName: 'Recursive', + name: 'recursive', + type: 'boolean', + default: false, + description: 'If true, the list folder operation will be applied recursively to all subfolders and the response will contain contents of all subfolders. The default for this field is False.', + }, + ], + }, ], }; @@ -484,11 +784,24 @@ export class Dropbox implements INodeType { let endpoint = ''; let requestMethod = ''; + let returnAll = false; + let property = ''; let body: IDataObject | Buffer; let options; const query: IDataObject = {}; - const headers: IDataObject = {}; + let headers: IDataObject = {}; + let simple = false; + + // get the root directory to set it as the default search folder + const { root_info: { root_namespace_id } } = await getRootDirectory.call(this); + + headers = { + 'dropbox-api-path-root': JSON.stringify({ + '.tag': 'root', + 'root': root_namespace_id, + }), + }; for (let i = 0; i < items.length; i++) { body = {}; @@ -545,7 +858,6 @@ export class Dropbox implements INodeType { body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); } } - } else if (resource === 'folder') { if (operation === 'create') { // ---------------------------------- @@ -564,20 +876,65 @@ export class Dropbox implements INodeType { // list // ---------------------------------- + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + property = 'entries'; + requestMethod = 'POST'; body = { path: this.getNodeParameter('path', i) as string, - limit: 2000, + limit: 1000, }; - // TODO: If more files than the max-amount exist it has to be possible to - // also request them. + if (returnAll === false) { + const limit = this.getNodeParameter('limit', 0) as number; + body.limit = limit; + } + + Object.assign(body, filters); endpoint = 'https://api.dropboxapi.com/2/files/list_folder'; } + } else if (resource === 'search') { + if (operation === 'query') { + // ---------------------------------- + // query + // ---------------------------------- + + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + simple = this.getNodeParameter('simple', 0) as boolean; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + property = 'matches'; + + requestMethod = 'POST'; + body = { + query: this.getNodeParameter('query', i) as string, + options: { + filename_only: true, + }, + }; + + if (filters.file_extensions) { + filters.file_extensions = (filters.file_extensions as string).split(','); + } + + Object.assign(body.options, filters); + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + Object.assign(body.options, { max_results: limit }); + } + + endpoint = 'https://api.dropboxapi.com/2/files/search_v2'; + } } - if (['file', 'folder'].includes(resource)) { + if (['file', 'folder', 'search'].includes(resource)) { if (operation === 'copy') { // ---------------------------------- // copy @@ -625,7 +982,13 @@ export class Dropbox implements INodeType { options = { encoding: null }; } - let responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options); + let responseData; + + if (returnAll === true) { + responseData = await dropboxpiRequestAllItems.call(this, property, requestMethod, endpoint, body, query, headers); + } else { + responseData = await dropboxApiRequest.call(this, requestMethod, endpoint, body, query, headers, options); + } if (resource === 'file' && operation === 'upload') { responseData = JSON.parse(responseData); @@ -665,7 +1028,11 @@ export class Dropbox implements INodeType { 'content_hash': 'contentHash', }; - for (const item of responseData.entries) { + if (returnAll === false) { + responseData = responseData.entries; + } + + for (const item of responseData) { const newItem: IDataObject = {}; // Get the props and save them under a proper name @@ -677,8 +1044,14 @@ export class Dropbox implements INodeType { returnData.push(newItem as IDataObject); } + } else if (resource === 'search' && operation === 'query') { + if (returnAll === true) { + returnData.push.apply(returnData, (simple === true) ? simplify(responseData) : responseData); + } else { + returnData.push.apply(returnData, (simple === true) ? simplify(responseData[property]) : responseData[property]); + } } else { - returnData.push(responseData as IDataObject); + returnData.push(responseData); } } diff --git a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts index 880ec6bc8c..66533c4c01 100644 --- a/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Dropbox/GenericFunctions.ts @@ -20,7 +20,7 @@ import { * @param {object} body * @returns {Promise} */ -export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers?: object, option: IDataObject = {}): Promise {// tslint:disable-line:no-any +export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers: object = {}, option: IDataObject = {}): Promise {// tslint:disable-line:no-any const options: OptionsWithUri = { headers, @@ -67,3 +67,51 @@ export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions throw error; } } + +export async function dropboxpiRequestAllItems(this: IExecuteFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + + const returnData: IDataObject[] = []; + + const paginationEndpoint: IDataObject = { + 'folder': 'https://api.dropboxapi.com/2/files/list_folder/continue', + 'search': 'https://api.dropboxapi.com/2/files/search/continue_v2', + }; + + let responseData; + do { + responseData = await dropboxApiRequest.call(this, method, endpoint, body, query, headers); + const cursor = responseData.cursor; + if (cursor !== undefined) { + endpoint = paginationEndpoint[resource] as string; + body = { cursor }; + } + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.has_more !== false + ); + + return returnData; +} + +export function getRootDirectory(this: IHookFunctions | IExecuteFunctions) { + return dropboxApiRequest.call(this, 'POST', 'https://api.dropboxapi.com/2/users/get_current_account', {}); +} + +export function simplify(data: IDataObject[]) { + const results = []; + for (const element of data) { + const { '.tag': key } = element?.metadata as IDataObject; + const metadata = (element?.metadata as IDataObject)[key as string] as IDataObject; + delete element.metadata; + Object.assign(element, metadata); + if ((element?.match_type as IDataObject)['.tag']) { + element.match_type = (element?.match_type as IDataObject)['.tag'] as string; + } + results.push(element); + } + return results; +} + + diff --git a/packages/nodes-base/nodes/Dropbox/dropbox.png b/packages/nodes-base/nodes/Dropbox/dropbox.png deleted file mode 100644 index 6fe6d86751328843341bb0a3b9940a9cc66f3cde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1169 zcmX9;c~BE~6n-J292JvVwTf1h){a;p0&1-dDv$&afp94WIXa4Au#x~K5Mqvs98nGh zskUMfib6}uA+|VRKq_doq6qB>o)9*>n@F%25p7ZX+c>j3``-7y_r34EKUU2T4RCaD zasU9wpg>Lp`m(9lj!q#iu3G?TiTrI*T$I8A91dWRMS$D@3In9$ptJ@kmSdp6Kx_n= zo+_bO{*H!2mI0)=6|~ac^&-9urRW(01!7{5ZG;>HNQ{s{fD#88YQZTS4Ml=N(yD+{ z9JvwXIP51O#YlmOAVF1RzyM+rWTT)QquR5cfD{9%o|=Nl$XKcg_Lx9KQhWqb9Kx!A zK_&_5Bq)&$0uGEpmJw701tveCS_CHRf_jh|;2_e(Ap=b$AdLhuvPysuN#T%2P|7Le zkU~bSpTm4}YY(q%e=Lv zcnD*WApTI~JS=P%H@8oBj2v2`1#IXn`1a|fqhZuPL4b!t}N zA&vS{dx0vm`=mpUd*J9^E^DH&OPbu?)1wMDV$Ojk&HAtCbIQzozky$(7P$_@-|arB z4ULht-7g;cvEH6GJLIQzsdtu}HH86v%RVn*=oz&KuQqBs#tBi_i%%ch_D4&9b1VOP z*TQP2&Br9eDXViUinWQ`?K?Jj=$dy7-CPu_X*f6dv_2Y#0pARlTpnm+lS637`#n;R zI)yLcd+HkkSrejZnm}Cc7EEj`?Rxn@P1bikyqGs^5?8QuE h(`V|ON^v&4v8kF \ No newline at end of file