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.
## 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?

View file

@ -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',

View file

@ -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<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
@ -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);
}
}

View file

@ -20,7 +20,7 @@ import {
* @param {object} body
* @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 = {
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<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