Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add integration with: Dropbox #695

Merged
merged 8 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ BOX_FILESTORAGE_CLOUD_CLIENT_SECRET=
# Onedrive
ONEDRIVE_FILESTORAGE_CLOUD_CLIENT_ID=
ONEDRIVE_FILESTORAGE_CLOUD_CLIENT_SECRET=
# dropbox
DROPBOX_FILESTORAGE_CLOUD_CLIENT_ID=
DROPBOX_FILESTORAGE_CLOUD_CLIENT_SECRET=


# ================================================
Expand Down
1 change: 1 addition & 0 deletions packages/api/scripts/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ CREATE TABLE connector_sets
ats_ashby boolean NULL,
ecom_webflow boolean NULL,
crm_microsoftdynamicssales boolean NULL,
fs_dropbox boolean NULL,
CONSTRAINT PK_project_connector PRIMARY KEY ( id_connector_set )
);

Expand Down
8 changes: 4 additions & 4 deletions packages/api/scripts/seed.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
INSERT INTO users (id_user, identification_strategy, email, password_hash, first_name, last_name) VALUES
('0ce39030-2901-4c56-8db0-5e326182ec6b', 'b2c','local@panora.dev', '$2b$10$Y7Q8TWGyGuc5ecdIASbBsuXMo3q/Rs3/cnY.mLZP4tUgfGUOCUBlG', 'local', 'Panora');

INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, crm_microsoftdynamicssales, ecom_webflow, tcg_linear, ecom_shopify, ecom_woocommerce, ecom_amazon, ecom_squarespace, hris_gusto) VALUES
('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE);
INSERT INTO connector_sets (id_connector_set, crm_hubspot, crm_zoho, crm_pipedrive, crm_attio, crm_zendesk, crm_close, tcg_zendesk, tcg_gorgias, tcg_front, tcg_jira, tcg_gitlab, fs_box, tcg_github, hris_deel, hris_sage, ats_ashby, crm_microsoftdynamicssales, ecom_webflow, tcg_linear, ecom_shopify, ecom_woocommerce, ecom_amazon, ecom_squarespace, hris_gusto, fs_dropbox) VALUES
('1709da40-17f7-4d3a-93a0-96dc5da6ddd7', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('852dfff8-ab63-4530-ae49-e4b2924407f8', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE),
('aed0f856-f802-4a79-8640-66d441581a99', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE);

INSERT INTO projects (id_project, name, sync_mode, id_user, id_connector_set) VALUES
('1e468c15-aa57-4448-aa2b-7fed640d1e3d', 'Project 1', 'pull', '0ce39030-2901-4c56-8db0-5e326182ec6b', '1709da40-17f7-4d3a-93a0-96dc5da6ddd7'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { DropboxGroupInput, DropboxGroupOutput } from '@filestorage/group/services/dropbox/types';

import { DropboxUserInput, DropboxUserOutput } from '@filestorage/user/services/dropbox/types';

import { DropboxFileInput, DropboxFileOutput } from '@filestorage/file/services/dropbox/types';

import { DropboxFolderInput, DropboxFolderOutput } from '@filestorage/folder/services/dropbox/types';

import {
BoxSharedLinkInput,
BoxSharedLinkOutput,
Expand Down Expand Up @@ -57,10 +65,10 @@ import {
} from '@filestorage/user/services/box/types';

/* file */
export type OriginalFileInput = BoxFileInput | OnedriveFileInput;
export type OriginalFileInput = BoxFileInput | OnedriveFileInput | DropboxFileInput;

/* folder */
export type OriginalFolderInput = BoxFolderInput | OnedriveFolderInput;
export type OriginalFolderInput = BoxFolderInput | OnedriveFolderInput | DropboxFolderInput;

/* permission */
export type OriginalPermissionInput = any | OnedrivePermissionInput;
Expand All @@ -72,10 +80,10 @@ export type OriginalSharedLinkInput = any;
export type OriginalDriveInput = any | OnedriveDriveInput;

/* group */
export type OriginalGroupInput = BoxGroupInput | OnedriveGroupInput;
export type OriginalGroupInput = BoxGroupInput | OnedriveGroupInput | DropboxGroupInput;

/* user */
export type OriginalUserInput = BoxUserInput | OnedriveUserInput;
export type OriginalUserInput = BoxUserInput | OnedriveUserInput | DropboxUserInput;

export type FileStorageObjectInput =
| OriginalFileInput
Expand All @@ -89,10 +97,10 @@ export type FileStorageObjectInput =
/* OUTPUT */

/* file */
export type OriginalFileOutput = BoxFileOutput | OnedriveFileOutput;
export type OriginalFileOutput = BoxFileOutput | OnedriveFileOutput | DropboxFileOutput;

/* folder */
export type OriginalFolderOutput = BoxFolderOutput | OnedriveFolderOutput;
export type OriginalFolderOutput = BoxFolderOutput | OnedriveFolderOutput | DropboxFolderOutput;

/* permission */
export type OriginalPermissionOutput = any | OnedrivePermissionOutput;
Expand All @@ -104,10 +112,10 @@ export type OriginalSharedLinkOutput = any;
export type OriginalDriveOutput = any | OnedriveDriveOutput;

/* group */
export type OriginalGroupOutput = BoxGroupOutput | OnedriveGroupOutput;
export type OriginalGroupOutput = BoxGroupOutput | OnedriveGroupOutput | DropboxGroupOutput;

/* user */
export type OriginalUserOutput = BoxUserOutput | OnedriveUserOutput;
export type OriginalUserOutput = BoxUserOutput | OnedriveUserOutput | DropboxUserOutput;

export type FileStorageObjectOutput =
| OriginalFileOutput
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/filestorage/file/file.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DropboxFileMapper } from './services/dropbox/mappers';
import { DropboxService } from './services/dropbox';
import { OnedriveFileMapper } from './services/onedrive/mappers';
import { OnedriveService } from './services/onedrive';
import { BullQueueModule } from '@@core/@core-services/queues/queue.module';
Expand Down Expand Up @@ -28,6 +30,8 @@ import { Utils } from '@filestorage/@lib/@utils';
BoxService,
OnedriveService,
OnedriveFileMapper,
DropboxService,
DropboxFileMapper,
],
exports: [SyncService],
})
Expand Down
113 changes: 113 additions & 0 deletions packages/api/src/filestorage/file/services/dropbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { EncryptionService } from '@@core/@core-services/encryption/encryption.service';
import { LoggerService } from '@@core/@core-services/logger/logger.service';
import { PrismaService } from '@@core/@core-services/prisma/prisma.service';
import { ApiResponse } from '@@core/utils/types';
import { SyncParam } from '@@core/utils/types/interface';
import { FileStorageObject } from '@filestorage/@lib/@types';
import { IFileService } from '@filestorage/file/types';
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { ServiceRegistry } from '../registry.service';
import { DropboxFileOutput } from './types';
import { UnifiedFilestorageFolderOutput } from '@filestorage/folder/types/model.unified';

@Injectable()
export class DropboxService implements IFileService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
) {
this.logger.setContext(
FileStorageObject.file.toUpperCase() + ':' + DropboxService.name,
);
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a template literal for string concatenation.

As suggested by static analysis, using a template literal is preferred over string concatenation. It makes the code more readable and maintainable.

Apply this diff to use a template literal:

- this.logger.setContext(
-   FileStorageObject.file.toUpperCase() + ':' + DropboxService.name,
- );
+ this.logger.setContext(`${FileStorageObject.file.toUpperCase()}:${DropboxService.name}`);
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.logger.setContext(
FileStorageObject.file.toUpperCase() + ':' + DropboxService.name,
);
this.logger.setContext(`${FileStorageObject.file.toUpperCase()}:${DropboxService.name}`);
Tools
Biome

[error] 23-23: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)

this.registry.registerService('dropbox', this);
}

async getAllFilesInFolder(
folderPath: string,
connection: any,
): Promise<DropboxFileOutput[]> {
// ref: https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
const files: DropboxFileOutput[] = [];
let cursor: string | null = null;
let hasMore = true;

while (hasMore) {
const url = cursor
? `${connection.account_url}/files/list_folder/continue`
: `${connection.account_url}/files/list_folder`;

const data = cursor ? { cursor } : { path: folderPath, recursive: false };

try {
const response = await axios.post(url, data, {
headers: {
Authorization: `Bearer ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
});

const { entries, has_more, cursor: newCursor } = response.data;

// Collect all file entries
files.push(...entries.filter((entry: any) => entry['.tag'] === 'file'));

hasMore = has_more;
cursor = newCursor;
} catch (error) {
console.error('Error listing files in folder:', error);
throw new Error('Failed to list all files in the folder.');
}
}

return files;
}
Comment on lines +28 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhance error handling.

The method implementation looks good overall. It correctly retrieves all files in a specified Dropbox folder, handles pagination, and filters the entries to only include files.

However, the error handling could be improved:

  • Provide more specific error messages based on the type of error encountered.
  • Log the error using the injected LoggerService for easier debugging.

Apply this diff to enhance the error handling:

  } catch (error) {
-   console.error('Error listing files in folder:', error);
+   this.logger.error('Error listing files in folder:', error);
-   throw new Error('Failed to list all files in the folder.');
+   throw new Error(`Failed to list all files in the folder: ${error.message}`);
  }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async getAllFilesInFolder(
folderPath: string,
connection: any,
): Promise<DropboxFileOutput[]> {
// ref: https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
const files: DropboxFileOutput[] = [];
let cursor: string | null = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? `${connection.account_url}/files/list_folder/continue`
: `${connection.account_url}/files/list_folder`;
const data = cursor ? { cursor } : { path: folderPath, recursive: false };
try {
const response = await axios.post(url, data, {
headers: {
Authorization: `Bearer ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
});
const { entries, has_more, cursor: newCursor } = response.data;
// Collect all file entries
files.push(...entries.filter((entry: any) => entry['.tag'] === 'file'));
hasMore = has_more;
cursor = newCursor;
} catch (error) {
console.error('Error listing files in folder:', error);
throw new Error('Failed to list all files in the folder.');
}
}
return files;
}
async getAllFilesInFolder(
folderPath: string,
connection: any,
): Promise<DropboxFileOutput[]> {
// ref: https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder
const files: DropboxFileOutput[] = [];
let cursor: string | null = null;
let hasMore = true;
while (hasMore) {
const url = cursor
? `${connection.account_url}/files/list_folder/continue`
: `${connection.account_url}/files/list_folder`;
const data = cursor ? { cursor } : { path: folderPath, recursive: false };
try {
const response = await axios.post(url, data, {
headers: {
Authorization: `Bearer ${this.cryptoService.decrypt(
connection.access_token,
)}`,
'Content-Type': 'application/json',
},
});
const { entries, has_more, cursor: newCursor } = response.data;
// Collect all file entries
files.push(...entries.filter((entry: any) => entry['.tag'] === 'file'));
hasMore = has_more;
cursor = newCursor;
} catch (error) {
this.logger.error('Error listing files in folder:', error);
throw new Error(`Failed to list all files in the folder: ${error.message}`);
}
}
return files;
}


async sync(data: SyncParam): Promise<ApiResponse<DropboxFileOutput[]>> {
try {
const { linkedUserId, id_folder } = data;
if (!id_folder) return;

const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'dropbox',
vertical: 'filestorage',
},
});

const folder = await this.prisma.fs_folders.findUnique({
where: {
id_fs_folder: id_folder as string,
},
});

const remote_data = await this.prisma.remote_data.findFirst({
where: {
ressource_owner_id: folder.id_fs_folder,
},
});

const folder_remote_data = JSON.parse(remote_data.data);

const files = await this.getAllFilesInFolder(
folder_remote_data.path_display,
connection,
);

this.logger.log(`Synced dropbox files !`);

return {
data: files,
message: 'Dropbox files retrieved',
statusCode: 200,
};
} catch (error) {
throw error;
}
}
Comment on lines +70 to +112
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the redundant catch clause.

The method implementation looks good. It correctly synchronizes the files by retrieving the necessary data from the database and calling the getAllFilesInFolder method. Returning an ApiResponse with the retrieved files is appropriate.

However, as flagged by static analysis, the catch clause that only rethrows the original error is redundant and can be confusing. It is recommended to remove it.

Apply this diff to remove the redundant catch clause:

- } catch (error) {
-   throw error;
- }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async sync(data: SyncParam): Promise<ApiResponse<DropboxFileOutput[]>> {
try {
const { linkedUserId, id_folder } = data;
if (!id_folder) return;
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'dropbox',
vertical: 'filestorage',
},
});
const folder = await this.prisma.fs_folders.findUnique({
where: {
id_fs_folder: id_folder as string,
},
});
const remote_data = await this.prisma.remote_data.findFirst({
where: {
ressource_owner_id: folder.id_fs_folder,
},
});
const folder_remote_data = JSON.parse(remote_data.data);
const files = await this.getAllFilesInFolder(
folder_remote_data.path_display,
connection,
);
this.logger.log(`Synced dropbox files !`);
return {
data: files,
message: 'Dropbox files retrieved',
statusCode: 200,
};
} catch (error) {
throw error;
}
}
async sync(data: SyncParam): Promise<ApiResponse<DropboxFileOutput[]>> {
try {
const { linkedUserId, id_folder } = data;
if (!id_folder) return;
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'dropbox',
vertical: 'filestorage',
},
});
const folder = await this.prisma.fs_folders.findUnique({
where: {
id_fs_folder: id_folder as string,
},
});
const remote_data = await this.prisma.remote_data.findFirst({
where: {
ressource_owner_id: folder.id_fs_folder,
},
});
const folder_remote_data = JSON.parse(remote_data.data);
const files = await this.getAllFilesInFolder(
folder_remote_data.path_display,
connection,
);
this.logger.log(`Synced dropbox files !`);
return {
data: files,
message: 'Dropbox files retrieved',
statusCode: 200,
};
}
}
Tools
Biome

[error] 110-110: The catch clause that only rethrows the original error is redundant.

These unnecessary catch clauses can be confusing. It is recommended to remove them.

(lint/complexity/noUselessCatch)

}
97 changes: 97 additions & 0 deletions packages/api/src/filestorage/file/services/dropbox/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { MappersRegistry } from '@@core/@core-services/registries/mappers.registry';
import { CoreUnification } from '@@core/@core-services/unification/core-unification.service';
import { OriginalSharedLinkOutput } from '@@core/utils/types/original/original.file-storage';
import { FileStorageObject } from '@filestorage/@lib/@types';
import { Utils } from '@filestorage/@lib/@utils';
import { IFileMapper } from '@filestorage/file/types';
import {
UnifiedFilestorageFileInput,
UnifiedFilestorageFileOutput,
} from '@filestorage/file/types/model.unified';
import { UnifiedFilestorageSharedlinkOutput } from '@filestorage/sharedlink/types/model.unified';
import { Injectable } from '@nestjs/common';
import { DropboxFileInput, DropboxFileOutput } from './types';

@Injectable()
export class DropboxFileMapper implements IFileMapper {
constructor(
private mappersRegistry: MappersRegistry,
private utils: Utils,
private coreUnificationService: CoreUnification,
) {
this.mappersRegistry.registerService(
'filestorage',
'file',
'dropbox',
this,
);
}

async desunify(
source: UnifiedFilestorageFileInput,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<DropboxFileInput> {
// todo: do something with customFieldMappings
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder: Address the TODO comment.

The TODO comment indicates that the customFieldMappings are not being used currently. Please ensure that this is addressed before merging the PR.

Do you want me to open a GitHub issue to track this task?

return {
path: `/${source.name}`,
mode: 'add',
autorename: true,
};
}

async unify(
source: DropboxFileOutput | DropboxFileOutput[],
connectionId: string,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<UnifiedFilestorageFileOutput | UnifiedFilestorageFileOutput[]> {
if (!Array.isArray(source)) {
return await this.mapSingleFileToUnified(
source,
connectionId,
customFieldMappings,
);
}
// Handling array of DropboxFileOutput
return Promise.all(
source.map((file) =>
this.mapSingleFileToUnified(file, connectionId, customFieldMappings),
),
);
}

private async mapSingleFileToUnified(
file: DropboxFileOutput,
connectionId: string,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<UnifiedFilestorageFileOutput> {
const result: UnifiedFilestorageFileOutput = {
remote_id: file.id,
remote_data: file,
name: file.name,
file_url: null,
mime_type: null,
size: file.size.toString(),
folder_id: null,
permission: null,
shared_link: null,
field_mappings: {},
};

if (customFieldMappings) {
for (const mapping of customFieldMappings) {
result.field_mappings[mapping.slug] = file[mapping.remote_id];
}
}

return result;
}
}
Loading
Loading