Share with me endpoint (#77)

* 🛠️ remove babel from dependencies
* 🛠️ removes some of the redundant dependencies + versions update
*  separate endpoint for "shared with me"
This commit is contained in:
Anton Shepilov
2023-06-13 17:16:13 +02:00
committed by GitHub
parent d17972b2f1
commit d7fa56cb13
19 changed files with 7163 additions and 8157 deletions
+1 -1
View File
@@ -1 +1 @@
12
18
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-parameter-decorator",
],
};
+6499 -5853
View File
File diff suppressed because it is too large Load Diff
+10 -32
View File
@@ -32,6 +32,7 @@
"kill": "kill $(lsof -t -i:3000) | exit 0"
},
"jest": {
"preset": "ts-jest",
"collectCoverage": true,
"coverageReporters": [
"json",
@@ -57,47 +58,29 @@
"tdrive-cli": "bin/tdrive-cli"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.18.10",
"@babel/preset-env": "^7.19.4",
"@babel/preset-typescript": "^7.18.6",
"@types/amqp-connection-manager": "^2.0.10",
"@types/analytics-node": "^3.1.5",
"@types/archiver": "^5.3.1",
"@types/bcrypt": "^5.0.0",
"@types/busboy": "^0.2.3",
"@types/chai": "^4.2.12",
"@types/cli-table": "^0.3.0",
"@types/config": "0.0.36",
"@types/eslint": "^7.2.3",
"@types/fastify-multipart": "^0.7.0",
"@types/fastify-static": "^2.2.1",
"@types/fluent-ffmpeg": "^2.1.20",
"@types/html-to-text": "^8.1.1",
"@types/jest": "^29.1.0",
"@types/lodash": "^4.14.165",
"@types/minio": "^7.0.7",
"@types/mongodb": "^4.0.7",
"@types/node": "^18.11",
"@types/node-cron": "^3.0.0",
"@types/node-fetch": "^2.5.12",
"@types/node-uuid": "^0.0.28",
"@types/pdf-image": "^2.0.1",
"@types/pino": "^6.3.2",
"@types/probe-image-size": "^7.0.1",
"@types/pump": "^1.1.1",
"@types/random-useragent": "^0.3.1",
"@types/socket.io-client": "^1.4.34",
"@types/supertest": "2.0.4",
"@types/socket.io-client": "^1.4.36",
"@types/uuid": "^8.3.0",
"@types/ws": "^7.2.7",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"babel-jest": "^29.1.0",
"babel-plugin-parameter-decorator": "^1.0.16",
"chai": "^4.2.0",
"cpy-cli": "^4.2.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.8.0",
@@ -106,17 +89,16 @@
"form-auto-content": "^2.2.0",
"jest": "^29.1.0",
"jest-mock-extended": "^3.0.4",
"nodemon": "2.0.4",
"nodemon": "^2.0.22",
"pino-pretty": "^10.0.0",
"prettier": "^2.1.2",
"rimraf": "^3.0.2",
"supertest": "4.0.2",
"ts-jest": "^29.1.0",
"ts-node": "^9.0.0",
"ts-node-dev": "^1.1.8",
"tsc-watch": "^4.2.9",
"typescript": "^4.9.5",
"tsconfig-paths": "^3.14.1"
"tsconfig-paths": "^3.14.1",
"typescript": "^4.9.5"
},
"dependencies": {
"@elastic/elasticsearch": "7",
@@ -126,7 +108,7 @@
"@ffprobe-installer/ffprobe": "^1.4.1",
"@sentry/node": "^6.19.7",
"@sentry/tracing": "^6.19.7",
"@socket.io/redis-adapter": "^7.1.0",
"@socket.io/redis-adapter": "^7.2.0",
"@types/http-proxy": "^1.17.11",
"@types/pdf-parse": "^1.1.1",
"@types/redis": "^4.0.11",
@@ -144,7 +126,7 @@
"config": "^3.3.2",
"deep-object-diff": "^1.1.0",
"emoji-name-map": "^1.2.9",
"eta": "^1.12.3",
"eta": "^2.2.0",
"fastify": "^3.29.4",
"fastify-cors": "^4.1.0",
"fastify-formbody": "^5.0.0",
@@ -156,7 +138,6 @@
"fastify-static": "^4.7.0",
"fastify-swagger": "^4.12.6",
"fastify-websocket": "^2.0.11",
"find-my-way": "^5.2.0",
"fluent-ffmpeg": "^2.1.2",
"fold-to-ascii": "^5.0.0",
"free-email-domains": "1.0.26",
@@ -165,12 +146,10 @@
"html-metadata-parser": "^2.0.4",
"html-to-text": "^8.2.1",
"idtoken-verifier": "^2.2.3",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"jwks-rsa": "^3.0.1",
"keyv": "^4.5.0",
"lodash": "^4.17.21",
"match-all": "^1.2.6",
"minio": "^7.0.18",
"minio": "7.0.18",
"moment": "^2.29.4",
"mongodb": "^5.6.0",
"multistream": "^4.1.0",
@@ -179,7 +158,6 @@
"node-cache": "^5.1.2",
"node-cron": "^3.0.0",
"node-fetch": "^2.6.7",
"node-uuid": "^1.4.8",
"openid-client": "^5.4.0",
"ora": "^5.4.0",
"pdf-parse": "^1.1.1",
@@ -193,7 +171,7 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^6.6.3",
"sharp": "^0.30.5",
"socket.io": "4",
"socket.io": "^4.6.2",
"socket.io-client": "^3.0.0",
"unoconv-promise": "^1.0.8",
"uuid": "^8.3.2",
@@ -1,5 +1,5 @@
import * as cron from "node-cron";
import uuid from "node-uuid";
import uuid from "uuid";
import { TdriveService } from "../../framework";
import { CronAPI, CronJob, CronExpression, CronTask } from "./api";
@@ -168,12 +168,12 @@ export const getAccessLevel = async (
const prevalidatedPublicTokenDocumentId = context?.user?.public_token_document_id;
try {
item =
item ||
(await repository.findOne({
if (!item) {
item = await repository.findOne({
id,
company_id: context.company.id,
}));
});
}
if (!item) {
throw Error("Drive item doesn't exist");
@@ -2,8 +2,8 @@ import SearchRepository from "../../../core/platform/services/search/repository"
import { getLogger, logger, TdriveLogger } from "../../../core/platform/framework";
import { CrudException, ListResult } from "../../../core/platform/framework/api/crud-service";
import Repository, {
inType,
comparisonType,
inType,
} from "../../../core/platform/services/database/services/orm/repository/repository";
import { PublicFile } from "../../../services/files/entities/file";
import globalResolver from "../../../services/global-resolver";
@@ -16,15 +16,15 @@ import {
TYPE as DriveTdriveTabRepoType,
} from "../entities/drive-tdrive-tab";
import {
DriveExecutionContext,
CompanyExecutionContext,
DocumentsMessageQueueRequest,
DriveExecutionContext,
DriveFileAccessLevel,
DriveItemDetails,
DriveTdriveTab,
RootType,
SearchDocumentsOptions,
TrashType,
CompanyExecutionContext,
DriveTdriveTab,
DriveFileAccessLevel,
} from "../types";
import {
addDriveItemToArchive,
@@ -783,7 +783,9 @@ export class DocumentsService {
limitStr: "100",
},
$in: [
["access_entities", [context.user.id, context.company.id]],
...(options.onlyDirectlyShared
? [["access_entities", [context.user.id, context.company.id]] as inType]
: []),
...(options.company_id ? [["company_id", [options.company_id]] as inType] : []),
...(options.creator ? [["creator", [options.creator]] as inType] : []),
...(options.mime_type
@@ -819,24 +821,31 @@ export class DocumentsService {
context,
);
// Use Promise.all to check access on each item in parallel
const filteredResult = await Promise.all(
result.getEntities().filter(async item => {
// if this flag is on, the access permissions were checked inside the database
if (!options.onlyDirectlyShared) {
const filteredResult = await this.filter(result.getEntities(), async item => {
try {
// Check access for each item
const hasAccess = await checkAccess(item.id, null, "read", this.repository, context);
// Return true if the user has access
return hasAccess;
return await checkAccess(item.id, null, "read", this.repository, context);
} catch (error) {
this.logger.warn("failed to check item access", error);
return false;
}
}),
);
});
return new ListResult(result.type, filteredResult, result.nextPage);
return new ListResult(result.type, filteredResult, result.nextPage);
} else {
return result;
}
};
private async filter(arr, callback) {
const fail = Symbol();
return (
await Promise.all(arr.map(async item => ((await callback(item)) ? item : fail)))
).filter(i => i !== fail);
}
getTab = async (tabId: string, context: CompanyExecutionContext): Promise<DriveTdriveTab> => {
const tab = await this.driveTdriveTabRepository.findOne(
{ company_id: context.company.id, tab_id: tabId },
@@ -47,6 +47,7 @@ export type SearchDocumentsOptions = {
sort?: SortType;
view?: string;
fields?: string[];
onlyDirectlyShared?: boolean;
};
export type SearchDocumentsBody = {
@@ -55,6 +55,7 @@ export class DocumentsController {
filename: q.resumableFilename || q.filename || file?.filename || undefined,
type: q.resumableType || q.type || file?.mimetype || undefined,
waitForThumbnail: !!q.thumbnail_sync,
ignoreThumbnails: false,
};
createdFile = await globalResolver.services.files.save(null, file, options, context);
@@ -140,6 +141,35 @@ export class DocumentsController {
};
};
sharedWithMe = async (
request: FastifyRequest<{
Params: RequestParams;
Body: SearchDocumentsBody;
Querystring: { public_token?: string };
}>,
): Promise<ListResult<DriveFileDTO>> => {
try {
const context = getDriveExecutionContext(request);
const options: SearchDocumentsOptions = {
...request.body,
company_id: request.body.company_id || context.company.id,
onlyDirectlyShared: true,
};
if (!Object.keys(options).length) {
this.throw500Search();
}
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: `${error}` }, "error while searching for document");
this.throw500Search();
}
};
/**
* Return access level of a given user on a given item
*/
@@ -352,6 +382,7 @@ export class DocumentsController {
const options: SearchDocumentsOptions = {
...request.body,
company_id: request.body.company_id || context.company.id,
onlyDirectlyShared: false,
};
if (!Object.keys(options).length) {
@@ -13,7 +13,27 @@ import { getSharedByUser } from "../../services/access-check";
export class DriveFileDTOBuilder {
private views: Map<string, string[]> = new Map([
["default", Object.getOwnPropertyNames(new DriveFile())],
[
"default",
[
"company_id",
"id",
"parent_id",
"is_in_trash",
"is_directory",
"name",
"extension",
"description",
"added",
"last_modified",
"size",
"last_version_cache",
"tags",
"access_info",
"content_keywords",
"creator",
],
],
[
"shared_with_me",
[
@@ -39,6 +59,8 @@ export class DriveFileDTOBuilder {
fields?: string[],
view?: string,
): Promise<ListResult<DriveFileDTO>> {
const file = new DriveFile();
file.id = "1";
if (view) {
fields = this.views.get(view);
}
@@ -87,6 +87,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
handler: documentsController.search.bind(documentsController),
});
fastify.route({
method: "POST",
url: `${baseUrl}/shared-with-me`,
preValidation: [fastify.authenticate],
handler: documentsController.sharedWithMe.bind(documentsController),
});
fastify.route({
method: "GET",
url: `${baseUrl}/tabs/:tab_id`,
@@ -1,4 +1,4 @@
import { logger, TwakeContext } from "../../../../../core/platform/framework";
import { logger, TdriveContext } from "../../../../../core/platform/framework";
import { PreviewClearMessageQueueRequest, PreviewMessageQueueCallback } from "../../../types";
import gr from "../../../../global-resolver";
import { MessageQueueHandler } from "../../../../../core/platform/services/message-queue/api";
@@ -22,7 +22,7 @@ export class ClearProcessor
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
init?(context?: TwakeContext): Promise<this> {
init?(context?: TdriveContext): Promise<this> {
throw new Error("Method not implemented.");
}
@@ -10,10 +10,10 @@ import {
} from "../../../../../utils/mime";
import { PreviewMessageQueueRequest, ThumbnailResult } from "../../../types";
import { generateVideoPreview } from "./video";
import { Initializable, TwakeServiceProvider } from "../../../../../core/platform/framework";
import { Initializable, TdriveServiceProvider } from "../../../../../core/platform/framework";
import { cleanFiles } from "../../../../../utils/files";
export class PreviewProcessService implements TwakeServiceProvider, Initializable {
export class PreviewProcessService implements TdriveServiceProvider, Initializable {
name: "PreviewProcessService";
version: "1";
@@ -125,10 +125,10 @@ async function getVideoDimensions(videoPath: string): Promise<{ width: number; h
ffmpeg.ffprobe(videoPath, (err, metadata) => {
if (err) {
reject(err);
} else {
const { width, height } = metadata.streams[0];
resolve({ width, height });
}
const { width, height } = metadata.streams[0];
resolve({ width, height });
});
});
}
@@ -73,11 +73,11 @@ export default class TestHelpers {
const filesUploadRaw = await this.platform.app.inject({
method: "POST",
url: `${url}/companies/${this.platform.workspace.company_id}/files?thumbnail_sync=1`,
url: `${url}/companies/${this.platform.workspace.company_id}/files?thumbnail_sync=0`,
...form,
});
const filesUpload: ResourceUpdateResponse<File> = deserialize(
const filesUpload: ResourceUpdateResponse<File> = deserialize<ResourceUpdateResponse<File>>(
ResourceUpdateResponse,
filesUploadRaw.body,
);
@@ -188,5 +188,22 @@ export default class TestHelpers {
response.body)
};
async sharedWithMeDocuments (
payload: Record<string, any>
){
const response = await this.platform.app.inject({
method: "POST",
url: `${TestHelpers.DOC_URL}/companies/${this.platform.workspace.company_id}/shared-with-me`,
headers: {
authorization: `Bearer ${this.jwt}`,
},
payload,
});
return deserialize<SearchResultMockClass>(
SearchResultMockClass,
response.body)
};
}
@@ -50,7 +50,7 @@ describe("the Drive feature", () => {
});
currentUser = await TestHelpers.getInstance(platform);
dbService = await TestDbService.getInstance(platform, true);
});
}, 300000000);
afterAll(async () => {
await platform?.tearDown();
@@ -294,7 +294,6 @@ describe("the Drive feature", () => {
});
it("did search a file shared by another user", async () => {
jest.setTimeout(30000);
//given:
const oneUser = await TestHelpers.getInstance(platform, true);
const anotherUser = await TestHelpers.getInstance(platform, true);
@@ -304,7 +303,7 @@ describe("the Drive feature", () => {
await new Promise(r => setTimeout(r, 5000));
//then:: files are not searchable for user without permissions
expect((await anotherUser.searchDocument({})).entities).toHaveLength(0);
expect((await anotherUser.sharedWithMeDocuments({})).entities).toHaveLength(0);
//and searchable for user that have
expect((await oneUser.searchDocument({})).entities).toHaveLength(TestHelpers.ALL_FILES.length);
@@ -320,8 +319,8 @@ describe("the Drive feature", () => {
await new Promise(r => setTimeout(r, 3000));
//then file become searchable
expect((await anotherUser.searchDocument({})).entities).toHaveLength(1);
});
expect((await anotherUser.sharedWithMeDocuments({})).entities).toHaveLength(1);
}, 30000000);
it("did search a file by file owner", async () => {
jest.setTimeout(30000);
@@ -387,7 +386,6 @@ describe("the Drive feature", () => {
});
it("did search order by name", async () => {
jest.setTimeout(10000);
const user = await TestHelpers.getInstance(platform, true);
// given:: all the sample files uploaded and documents for them created
await user.uploadAllFilesAndCreateDocuments();
@@ -404,7 +402,7 @@ describe("the Drive feature", () => {
//then all the files are sorted properly by name
expect(documents.entities.map(e => e.name)).toEqual(TestHelpers.ALL_FILES.sort());
});
}, 30000);
it("did search order by name desc", async () => {
jest.setTimeout(10000);
@@ -56,7 +56,7 @@ describe("The Tag feature", () => {
},
});
const tagResult: ResourceCreateResponse<Tag> = deserialize(
const tagResult: ResourceCreateResponse<Tag> = deserialize<ResourceCreateResponse<Tag>>(
ResourceCreateResponse,
createTag.body,
);
@@ -98,7 +98,7 @@ describe("The Tag feature", () => {
});
expect(createTag.statusCode).toBe(401);
const tagResult: ResourceCreateResponse<Tag> = deserialize(
const tagResult: ResourceCreateResponse<Tag> = deserialize<ResourceCreateResponse<Tag>>(
ResourceCreateResponse,
createTag.body,
);
@@ -121,7 +121,7 @@ describe("The Tag feature", () => {
});
expect(getTag.statusCode).toBe(200);
const getResult: ResourceGetResponse<Tag> = deserialize(ResourceGetResponse, getTag.body);
const getResult: ResourceGetResponse<Tag> = deserialize<ResourceGetResponse<Tag>>(ResourceGetResponse, getTag.body);
expect(getResult.resource).toBeDefined();
expect(getResult.resource.name).toBe("test0");
expect(getResult.resource.colour).toBe("#000000");
@@ -144,7 +144,7 @@ describe("The Tag feature", () => {
});
expect(getTag.statusCode).toBe(200);
const getResult: ResourceGetResponse<Tag> = deserialize(ResourceGetResponse, getTag.body);
const getResult: ResourceGetResponse<Tag> = deserialize<ResourceGetResponse<Tag>>(ResourceGetResponse, getTag.body);
expect(getResult.resource).toBe(null);
@@ -170,7 +170,7 @@ describe("The Tag feature", () => {
});
expect(updateTag.statusCode).toBe(201);
const tagUpdatedResult: ResourceCreateResponse<Tag> = deserialize(
const tagUpdatedResult: ResourceCreateResponse<Tag> = deserialize<ResourceCreateResponse<Tag>>(
ResourceCreateResponse,
updateTag.body,
);
@@ -210,7 +210,7 @@ describe("The Tag feature", () => {
});
expect(createTag.statusCode).toBe(401);
const tagResult: ResourceCreateResponse<Tag> = deserialize(
const tagResult: ResourceCreateResponse<any> = deserialize(
ResourceCreateResponse,
createTag.body,
);
@@ -236,7 +236,7 @@ describe("The Tag feature", () => {
});
expect(listTag.statusCode).toBe(200);
const tagResult: ResourceListResponse<Tag> = deserialize(ResourceListResponse, listTag.body);
const tagResult: ResourceListResponse<any> = deserialize(ResourceListResponse, listTag.body);
expect(tagResult.resources).toBeDefined();
for (const tag of tagResult.resources) {
expect(tag.name).toBeDefined();
+1 -1
View File
@@ -6,7 +6,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"noImplicitAny": true,
"noImplicitAny": false,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
File diff suppressed because it is too large Load Diff