🤝 Shared with me (#71)

* 🚧 #46 Shared with me
  - added converter of DriveFile to dto with user information
  - updated version of jest to the latest for better compatibility with typescript and mocking class instances
  - added run configuration for unit tests

Co-authored-by: Romaric Mourgues <rmourgues@linagora.com>
This commit is contained in:
Anton Shepilov
2023-06-09 14:37:15 +02:00
committed by GitHub
parent f1bf507865
commit dfd25a234a
32 changed files with 3215 additions and 4560 deletions
@@ -89,4 +89,5 @@ type AuthEntity = {
type: "user" | "channel" | "company" | "folder";
id: string | "parent";
level: publicAccessLevel | DriveFileAccessLevel;
grantor: string;
};
@@ -3,7 +3,7 @@ import _ from "lodash";
import { logger } from "../../../core/platform/framework";
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
import globalResolver from "../../global-resolver";
import { DriveFile } from "../entities/drive-file";
import { AccessInformation, DriveFile } from "../entities/drive-file";
import { CompanyExecutionContext, DriveFileAccessLevel } from "../types";
/**
@@ -325,3 +325,22 @@ export const makeStandaloneAccessLevel = async (
return accessInfo;
};
export const getSharedByUser = (
accessInfo: AccessInformation,
context: CompanyExecutionContext,
): string => {
// Try to find entity that matches user the most
// at first user, and then company
// we don't consider right now the access level by folder or channel or workspace
for (const idx in accessInfo?.entities) {
const entity = accessInfo.entities[idx];
if (entity.type === "user" && entity.id === context.user?.id) {
return entity.grantor;
}
if (entity.type === "company" && entity.id === context.company.id) {
return entity.grantor;
}
}
return null;
};
@@ -370,7 +370,14 @@ export class DocumentsService {
oldParent = item.parent_id;
}
if (key === "name") {
if (key === "access_info") {
item.access_info = content.access_info;
item.access_info.entities.forEach(info => {
if (!info.grantor) {
info.grantor = context.user.id;
}
});
} else if (key === "name") {
item.name = await getItemName(
content.parent_id || item.parent_id,
item.id,
@@ -885,6 +892,7 @@ export class DocumentsService {
type: "channel",
id: channelId,
level: level === "write" ? "write" : "read",
grantor: context.user.id,
},
],
},
@@ -47,6 +47,8 @@ export type SearchDocumentsOptions = {
last_modified_gt?: number;
last_modified_lt?: number;
sort?: SortType;
view?: string;
fields?: string[];
};
export type SearchDocumentsBody = {
@@ -59,6 +61,8 @@ export type SearchDocumentsBody = {
last_modified_gt?: number;
last_modified_lt?: number;
sort?: SortType;
view?: string;
fields?: string[];
};
export type DocumentsMessageQueueRequest = {
@@ -61,16 +61,19 @@ export const getDefaultDriveItem = (
id: "parent",
type: "folder",
level: "manage",
grantor: null,
},
{
id: item.company_id,
type: "company",
level: "none",
grantor: null,
},
{
id: context.user?.id,
type: "user",
level: "manage",
grantor: null,
},
],
public: {
@@ -476,7 +479,7 @@ export const getItemName = async (
* @param {string} source - the to be moved item id.
* @param {string} target - the to be moved to item id.
* @param {string} repository - the Drive item repository.
* @param {CompanyExecutionContex} context - the execution context.
* @param {CompanyExecutionContext} context - the execution context.
* @returns {Promise<boolean>} - whether the move is possible or not.
*/
export const canMoveItem = async (
@@ -531,7 +534,11 @@ export const canMoveItem = async (
return true;
};
export function isFileType(fileMime: string, fileName: string, requiredExtensions: string[]): any {
export function isFileType(
fileMime: string,
fileName: string,
requiredExtensions: string[],
): boolean {
const extension = fileName.split(".").pop();
const secondaryExtensions = Object.keys(mimes).filter(k => mimes[k] === fileMime);
const fileExtensions = [extension, ...secondaryExtensions];
@@ -18,8 +18,12 @@ import {
SearchDocumentsBody,
SearchDocumentsOptions,
} from "../../types";
import { DriveFileDTO } from "../dto/drive-file-dto";
import { DriveFileDTOBuilder } from "../dto/drive-file-dto-builder";
export class DocumentsController {
private driveFileDTOBuilder = new DriveFileDTOBuilder();
/**
* Creates a DriveFile item
*
@@ -340,7 +344,7 @@ export class DocumentsController {
Body: SearchDocumentsBody;
Querystring: { public_token?: string };
}>,
): Promise<ListResult<DriveFile>> => {
): Promise<ListResult<DriveFileDTO>> => {
try {
const context = getDriveExecutionContext(request);
@@ -353,7 +357,9 @@ export class DocumentsController {
this.throw500Search();
}
return await globalResolver.services.documents.documents.search(options, context);
const fileList = await globalResolver.services.documents.documents.search(options, context);
return this.driveFileDTOBuilder.build(fileList, context, options.fields, options.view);
} catch (error) {
logger.error("error while searching for document", error);
this.throw500Search();
@@ -0,0 +1,118 @@
import { DriveFile } from "../../entities/drive-file";
import { DriveFileDTO } from "./drive-file-dto";
import {
ListResult,
Paginable,
Pagination,
} from "../../../../core/platform/framework/api/crud-service";
import _ from "lodash";
import { CompanyExecutionContext } from "../../types";
import globalResolver from "../../../../services/global-resolver";
import User from "../../../../services/user/entities/user";
import { getSharedByUser } from "../../services/access-check";
export class DriveFileDTOBuilder {
private views: Map<string, string[]> = new Map([
["default", Object.getOwnPropertyNames(new DriveFile())],
[
"shared_with_me",
[
"id",
"name",
"parent_id",
"shared_by",
"created_by",
"is_directory",
"extension",
"added",
"last_modified",
"size",
"last_version_cache",
],
],
]);
usersService = globalResolver.services?.users;
public async build(
files: ListResult<DriveFile>,
context: CompanyExecutionContext,
fields?: string[],
view?: string,
): Promise<ListResult<DriveFileDTO>> {
if (view) {
fields = this.views.get(view);
}
if (!fields) {
fields = this.views.get("default");
}
//if we need to fetch users, find all the user identifiers ask theirs data at once
const fileUsers = new Map<string, FileUsers>();
// gather all user identifiers who share files with the current user
if (fields.some(f => f == "shared_by")) {
files.getEntities().forEach(f => {
fileUsers.set(f.id, { grantor: getSharedByUser(f.access_info, context) });
});
}
// gather all users who created the files
if (fields.some(f => f == "created_by")) {
files.getEntities().forEach(f => {
if (!fileUsers.has(f.id)) fileUsers.set(f.id, { creator: f.creator });
else fileUsers.get(f.id).creator = f.creator;
});
}
if (fileUsers.size > 0) {
await this.initUserCash(fileUsers);
}
const nextPage: Paginable = new Pagination(files.page_token, files.nextPage?.limitStr);
return new ListResult(
"drive_files_dto",
files.getEntities().map(f => this._buildDto(f, fields, fileUsers)),
nextPage,
);
}
private async initUserCash(fileUsers: Map<string, FileUsers>): Promise<void> {
const ids = [] as string[];
fileUsers.forEach(f => {
if (f.creator) ids.push(f.creator);
if (f.grantor) ids.push(f.grantor);
});
const users = await this.fetchUsers(ids);
const userById = new Map(users.map(u => [u.id, u]));
fileUsers.forEach(f => {
f.creatorObj = userById.get(f.creator);
f.grantorObj = userById.get(f.grantor);
});
}
private async fetchUsers(ids: string[]) {
return (
await this.usersService.list(
{
limitStr: ids.length.toString(),
} as Pagination,
{ userIds: _.uniq(ids) },
)
).getEntities();
}
private _buildDto(file: DriveFile, fields: string[], userCache: Map<string, FileUsers>) {
const dto = _.pick(file, fields) as DriveFileDTO;
const users = userCache.get(file.id);
if (users) {
dto.created_by = users?.creatorObj;
dto.shared_by = users?.grantorObj;
}
return dto;
}
}
type FileUsers = {
creator?: string;
grantor?: string;
creatorObj?: User;
grantorObj?: User;
};
@@ -0,0 +1,7 @@
import { DriveFile } from "../../entities/drive-file";
import User from "../../../user/entities/user";
export class DriveFileDTO extends DriveFile {
created_by: User;
shared_by: User;
}