Skip to content

Commit

Permalink
✨ Add Box Folder service
Browse files Browse the repository at this point in the history
  • Loading branch information
naelob committed Jun 24, 2024
1 parent cf68179 commit edfd049
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 4 deletions.
7 changes: 7 additions & 0 deletions packages/api/src/filestorage/@lib/@utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@@core/prisma/prisma.service';

@Injectable()
export class Utils {
constructor(private readonly prisma: PrismaService) {}
}
13 changes: 13 additions & 0 deletions packages/api/src/filestorage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
### File Storage

Panora supports integration with the following objects across multiple platforms:
| File Storage | Drives | Files | Folders | Groups | Users | Permissions | Shared links |
|-----------------------------------------------|:--------:|:-----:|:-----:|:-----------:|:-----:|:-----:|:---------:|
| [Google Drive]() | | | | | | | |
| [Box]() | | | | | | | |
| [Dropbox]() | | | | | | | |
| [OneDrive]() | | | | | | | |

Your favourite software is missing? [Ask the community to build a connector!](https://github.com/panoratech/Panora/issues/new)

Thanks to our contributors: [mit-27](https://github.com/mit-27)
5 changes: 2 additions & 3 deletions packages/api/src/filestorage/folder/folder.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { FolderService } from './services/folder.service';
import { ServiceRegistry } from './services/registry.service';
import { EncryptionService } from '@@core/encryption/encryption.service';
import { FieldMappingService } from '@@core/field-mapping/field-mapping.service';
import { PrismaService } from '@@core/prisma/prisma.service';
import { WebhookService } from '@@core/webhook/webhook.service';
import { BullModule } from '@nestjs/bull';
import { ConnectionUtils } from '@@core/connections/@utils';
import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard';
import { BoxService } from './services/box';

@Module({
imports: [
Expand All @@ -21,7 +20,6 @@ import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard';
controllers: [FolderController],
providers: [
FolderService,

LoggerService,
SyncService,
WebhookService,
Expand All @@ -30,6 +28,7 @@ import { ApiKeyAuthGuard } from '@@core/auth/guards/api-key.guard';
ServiceRegistry,
ConnectionUtils,
/* PROVIDERS SERVICES */
BoxService,
],
exports: [SyncService],
})
Expand Down
136 changes: 136 additions & 0 deletions packages/api/src/filestorage/folder/services/box/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Injectable } from '@nestjs/common';
import { IFolderService } from '@filestorage/folder/types';
import { FileStorageObject } from '@filestorage/@lib/@types';
import axios from 'axios';
import { PrismaService } from '@@core/prisma/prisma.service';
import { LoggerService } from '@@core/logger/logger.service';
import { ActionType, handle3rdPartyServiceError } from '@@core/utils/errors';
import { EncryptionService } from '@@core/encryption/encryption.service';
import { ApiResponse } from '@@core/utils/types';
import { ServiceRegistry } from '../registry.service';
import { BoxFolderInput, BoxFolderOutput } from './types';

@Injectable()
export class BoxService implements IFolderService {
constructor(
private prisma: PrismaService,
private logger: LoggerService,
private cryptoService: EncryptionService,
private registry: ServiceRegistry,
) {
this.logger.setContext(
FileStorageObject.folder.toUpperCase() + ':' + BoxService.name,
);
this.registry.registerService('box', this);
}

async addFolder(
folderData: BoxFolderInput,
linkedUserId: string,
): Promise<ApiResponse<BoxFolderOutput>> {
try {
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'box',
vertical: 'filestorage',
},
});
const resp = await axios.post(
`${connection.account_url}/folders`,
JSON.stringify({
name: folderData.name,
parent: {
id: folderData.parent.id,
},
}),
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.cryptoService.decrypt(
connection.access_token,
)}`,
},
},
);

return {
data: resp?.data,
message: 'Box folder created',
statusCode: 201,
};
} catch (error) {
handle3rdPartyServiceError(
error,
this.logger,
'Box',
FileStorageObject.folder,
ActionType.POST,
);
}
}

async recursiveGetBoxFolders(
remote_folder_id: string,
linkedUserId: string,
): Promise<BoxFolderOutput[]> {
try {
const connection = await this.prisma.connections.findFirst({
where: {
id_linked_user: linkedUserId,
provider_slug: 'box',
vertical: 'filestorage',
},
});
const resp = await axios.get(
`${connection.account_url}/folders/${remote_folder_id}/items`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.cryptoService.decrypt(
connection.access_token,
)}`,
},
},
);
const folders = resp.data.filter((elem) => elem.type == 'folder');
let results: BoxFolderOutput[] = folders;
for (const folder of folders) {
// Recursively get subfolders
const subFolders = await this.recursiveGetBoxFolders(
folder.id,
linkedUserId,
);
results = results.concat(subFolders);
}
return results;
} catch (error) {
throw error;
}
}

async syncFolders(
linkedUserId: string,
custom_properties?: string[],
): Promise<ApiResponse<BoxFolderOutput[]>> {
try {
// to sync all folders we start from root folder ("0") and recurse through it
const results = await this.recursiveGetBoxFolders('0', linkedUserId);
this.logger.log(`Synced box folders !`);

return {
data: results,
message: 'Box folders retrieved',
statusCode: 200,
};
} catch (error) {
handle3rdPartyServiceError(
error,
this.logger,
'Box',
FileStorageObject.folder,
ActionType.GET,
);
}
}
}
71 changes: 71 additions & 0 deletions packages/api/src/filestorage/folder/services/box/mappers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { BoxFolderInput, BoxFolderOutput } from './types';
import {
UnifiedFolderInput,
UnifiedFolderOutput,
} from '@filestorage/folder/types/model.unified';
import { IFolderMapper } from '@filestorage/folder/types';
import { Utils } from '@filestorage/@lib/@utils';
import { MappersRegistry } from '@@core/utils/registry/mappings.registry';
import { Injectable } from '@nestjs/common';

@Injectable()
export class BoxFolderMapper implements IFolderMapper {
constructor(private mappersRegistry: MappersRegistry, private utils: Utils) {
this.mappersRegistry.registerService('filestorage', 'folder', 'box', this);
}

async desunify(
source: UnifiedFolderInput,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<BoxFolderInput> {
if (customFieldMappings && source.field_mappings) {
for (const [k, v] of Object.entries(source.field_mappings)) {
const mapping = customFieldMappings.find(
(mapping) => mapping.slug === k,
);
if (mapping) {
result[mapping.remote_id] = v;
}
}
}
return;
}

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

private async mapSingleFolderToUnified(
folder: BoxFolderOutput,
customFieldMappings?: {
slug: string;
remote_id: string;
}[],
): Promise<UnifiedFolderOutput> {
const field_mappings: { [key: string]: any } = {};
if (customFieldMappings) {
for (const mapping of customFieldMappings) {
field_mappings[mapping.slug] = folder[mapping.remote_id];
}
}

return;
}
}
121 changes: 121 additions & 0 deletions packages/api/src/filestorage/folder/services/box/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
export interface BoxFolderInput {
id: string;
type: 'folder';
allowed_invitee_roles: string[];
allowed_shared_link_access_levels: string[];
can_non_owners_invite: boolean;
can_non_owners_view_collaborators: boolean;
classification: BoxClassification;
content_created_at: string;
content_modified_at: string;
created_at: string;
created_by: BoxUser;
description: string;
etag: string;
folder_upload_email: BoxFolderUploadEmail;
has_collaborations: boolean;
is_accessible_via_shared_link: boolean;
is_collaboration_restricted_to_enterprise: boolean;
is_externally_owned: boolean;
item_collection: BoxItemCollection;
item_status: string;
metadata: Record<string, any>;
modified_at: string;
modified_by: BoxUser;
name: string;
owned_by: BoxUser;
parent: {
id: string;
type: 'folder';
etag: string;
name: string;
sequence_id: string;
};
path_collection: {
entries: {
id: string;
etag: string;
type: 'folder';
sequence_id: string;
name: string;
}[];
total_count: number;
};
permissions: {
can_delete: boolean;
can_download: boolean;
can_invite_collaborator: boolean;
can_rename: boolean;
can_set_share_access: boolean;
can_share: boolean;
can_upload: boolean;
};
purged_at: string | null;
sequence_id: string;
shared_link: BoxSharedLink;
size: number;
sync_state: string;
tags: string[];
trashed_at: string | null;
watermark_info: {
is_watermarked: boolean;
};
}

export type BoxFolderOutput = Partial<BoxFolderInput>;

type BoxUser = {
id: string;
type: 'user';
login: string;
name: string;
};

type BoxSharedLink = {
url: string;
download_url: string;
vanity_url?: string;
vanity_name?: string;
access: string;
effective_access: string;
effective_permission: string;
unshared_at: string;
is_password_enabled: boolean;
permissions: {
can_download: boolean;
can_preview: boolean;
can_edit: boolean;
};
download_count: number;
preview_count: number;
};

type BoxFolderUploadEmail = {
access: string;
email: string;
};

type BoxClassification = {
color: string;
definition: string;
name: string;
};

type BoxItemCollection = {
entries: BoxItem[];
limit: number;
next_marker?: string;
offset: number;
order: { by: string; direction: string }[];
prev_marker?: string;
total_count: number;
};

type BoxItem = {
id: string;
type: 'file' | 'folder';
etag: string;
sequence_id: string;
name: string;
// ... other common properties for both files and folders
};
2 changes: 1 addition & 1 deletion packages/shared/src/connectors/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2713,7 +2713,7 @@ export const CONNECTORS_METADATA: ProvidersConfig = {
scopes: '',
urls: {
docsUrl: 'https://developer.box.com/reference/',
apiUrl: 'https://api.box.com',
apiUrl: 'https://api.box.com/2.0',
authBaseUrl: 'https://account.box.com/api/oauth2/authorize'
},
logoPath: 'https://gdm-catalog-fmapi-prod.imgix.net/ProductLogo/95b201e8-845a-4064-a9b2-a8eb49d19ca3.png?w=128&h=128&fit=max&dpr=3&auto=format&q=50',
Expand Down

0 comments on commit edfd049

Please sign in to comment.