Add file:search operation to Dropbox Node (#1494)

*  Add search resource to Dropbox Node

* 📚 Add breaking change instructions

*  Add missing credentials

*  Add "simple" parameter to the operation search:query

*  Update breaking change message

*  Improvement

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-03-11 03:16:05 -05:00 committed by GitHub
parent 263813a8f9
commit e1dbb72929
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 451 additions and 16 deletions

View file

@ -2,6 +2,18 @@
This list shows all the versions which include breaking changes and how to upgrade. 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 ## 0.105.0
### What changed? ### What changed?

View file

@ -7,6 +7,7 @@ const scopes = [
'files.content.write', 'files.content.write',
'files.content.read', 'files.content.read',
'sharing.read', 'sharing.read',
'account_info.read',
]; ];
export class DropboxOAuth2Api implements ICredentialType { export class DropboxOAuth2Api implements ICredentialType {
@ -41,7 +42,7 @@ export class DropboxOAuth2Api implements ICredentialType {
displayName: 'Auth URI Query Parameters', displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters', name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes, type: 'hidden' as NodePropertyTypes,
default: 'token_access_type=offline', default: 'token_access_type=offline&force_reapprove=true',
}, },
{ {
displayName: 'Authentication', displayName: 'Authentication',

View file

@ -11,21 +11,24 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
dropboxApiRequest dropboxApiRequest,
dropboxpiRequestAllItems,
getRootDirectory,
simplify,
} from './GenericFunctions'; } from './GenericFunctions';
export class Dropbox implements INodeType { export class Dropbox implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Dropbox', displayName: 'Dropbox',
name: 'dropbox', name: 'dropbox',
icon: 'file:dropbox.png', icon: 'file:dropbox.svg',
group: ['input'], group: ['input'],
version: 1, version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Access data on Dropbox', description: 'Access data on Dropbox',
defaults: { defaults: {
name: 'Dropbox', name: 'Dropbox',
color: '#0062ff', color: '#007ee5',
}, },
inputs: ['main'], inputs: ['main'],
outputs: ['main'], outputs: ['main'],
@ -84,6 +87,10 @@ export class Dropbox implements INodeType {
name: 'Folder', name: 'Folder',
value: 'folder', value: 'folder',
}, },
{
name: 'Search',
value: 'search',
},
], ],
default: 'file', default: 'file',
description: 'The resource to operate on.', description: 'The resource to operate on.',
@ -176,6 +183,27 @@ export class Dropbox implements INodeType {
description: 'The operation to perform.', 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 // file
// ---------------------------------- // ----------------------------------
@ -419,7 +447,189 @@ export class Dropbox implements INodeType {
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.', description: 'Name of the binary property which contains<br />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 // folder
@ -469,7 +679,97 @@ export class Dropbox implements INodeType {
placeholder: '/invoices/2019/', placeholder: '/invoices/2019/',
description: 'The path of which to list the content.', 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 endpoint = '';
let requestMethod = ''; let requestMethod = '';
let returnAll = false;
let property = '';
let body: IDataObject | Buffer; let body: IDataObject | Buffer;
let options; let options;
const query: IDataObject = {}; 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++) { for (let i = 0; i < items.length; i++) {
body = {}; body = {};
@ -545,7 +858,6 @@ export class Dropbox implements INodeType {
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8');
} }
} }
} else if (resource === 'folder') { } else if (resource === 'folder') {
if (operation === 'create') { if (operation === 'create') {
// ---------------------------------- // ----------------------------------
@ -564,20 +876,65 @@ export class Dropbox implements INodeType {
// list // list
// ---------------------------------- // ----------------------------------
returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const filters = this.getNodeParameter('filters', i) as IDataObject;
property = 'entries';
requestMethod = 'POST'; requestMethod = 'POST';
body = { body = {
path: this.getNodeParameter('path', i) as string, 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 if (returnAll === false) {
// also request them. const limit = this.getNodeParameter('limit', 0) as number;
body.limit = limit;
}
Object.assign(body, filters);
endpoint = 'https://api.dropboxapi.com/2/files/list_folder'; 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') { if (operation === 'copy') {
// ---------------------------------- // ----------------------------------
// copy // copy
@ -625,7 +982,13 @@ export class Dropbox implements INodeType {
options = { encoding: null }; 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') { if (resource === 'file' && operation === 'upload') {
responseData = JSON.parse(responseData); responseData = JSON.parse(responseData);
@ -665,7 +1028,11 @@ export class Dropbox implements INodeType {
'content_hash': 'contentHash', 'content_hash': 'contentHash',
}; };
for (const item of responseData.entries) { if (returnAll === false) {
responseData = responseData.entries;
}
for (const item of responseData) {
const newItem: IDataObject = {}; const newItem: IDataObject = {};
// Get the props and save them under a proper name // Get the props and save them under a proper name
@ -677,8 +1044,14 @@ export class Dropbox implements INodeType {
returnData.push(newItem as IDataObject); 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 { } else {
returnData.push(responseData as IDataObject); returnData.push(responseData);
} }
} }

View file

@ -20,7 +20,7 @@ import {
* @param {object} body * @param {object} body
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query: IDataObject = {}, headers?: object, option: IDataObject = {}): Promise<any> {// 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<any> {// tslint:disable-line:no-any
const options: OptionsWithUri = { const options: OptionsWithUri = {
headers, headers,
@ -67,3 +67,51 @@ export async function dropboxApiRequest(this: IHookFunctions | IExecuteFunctions
throw error; throw error;
} }
} }
export async function dropboxpiRequestAllItems(this: IExecuteFunctions | IHookFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise<any> { // 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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 67 62" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="1" y="1"/><symbol id="A" overflow="visible"><path d="M18.874.02L0 12.18l13.066 10.526L32 11.032m-32 22l18.874 12.4L32 34.422 13.066 22.686M32 34.422l13.188 11.01L64 33.152 50.994 22.686M64 12.28L45.188 0 32 11.01l18.994 11.674M32.06 36.778L18.872 47.726l-5.686-3.69v4.174L32.06 59.522 50.934 48.21v-4.174l-5.686 3.69" stroke="none" fill="#007ee5" fill-rule="nonzero"/></symbol></svg>

After

Width:  |  Height:  |  Size: 603 B