🚧 #46 Shared with me (#67)

* 🚧 #46 Shared with me
This commit is contained in:
Anton Shepilov
2023-06-07 10:48:44 +02:00
committed by GitHub
parent 49ec20d60e
commit f1bf507865
42 changed files with 1122 additions and 867 deletions
@@ -149,6 +149,10 @@ export class CrudException extends Error {
static badGateway(details: string): CrudException {
return new CrudException(details, 502);
}
static throwMe(cause: Error, newOne: CrudException): void {
throw cause instanceof CrudException ? cause : newOne;
}
}
export interface Paginable {
@@ -202,7 +206,7 @@ export interface CRUDService<Entity, PrimaryKey, Context extends ExecutionContex
* Save a resource.
* If the resource exists, it is updated, if it does not exists, it is created.
*
* @param itemOrItems
* @param item
* @param options
* @param context
*/
@@ -223,6 +227,8 @@ export interface CRUDService<Entity, PrimaryKey, Context extends ExecutionContex
/**
* List a resource
*
* @param pagination
* @param options
* @param context
*/
list<ListOptions>(
@@ -49,7 +49,7 @@ export function buildComparison(where: any, options: FindOptions = {}): string[]
if (operator === "$gt" || operator === "$gte" || operator === "$lt" || operator === "$lte") {
(options[operator] || []).forEach(element => {
if (!where[element[0]]) where[element[0]] = {};
where[element[0]][operator] = element[1];
where[element[0]][operator] = parseInt(element[1]);
});
}
});
@@ -16,9 +16,9 @@ export type FindFilter = { [key: string]: any };
export type RemoveFilter = { [key: string]: any };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type comparisonType = [string, any];
export type comparisonType = [string, any];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type inType = [string, Array<any>];
export type inType = [string, Array<any>];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type likeType = [string, any];
@@ -42,6 +42,9 @@ export default class ElasticSearch extends SearchAdapter implements SearchAdapte
try {
this.client = new Client({
node: this.configuration.endpoint,
ssl: {
rejectUnauthorized: false,
},
});
} catch (e) {
logger.error(`Unable to connect to ElasticSearch at ${this.configuration.endpoint}`);
@@ -3,6 +3,7 @@ import { TransportRequestOptions } from "@elastic/elasticsearch/lib/Transport";
import { logger } from "../../../../../../core/platform/framework/logger";
import { EntityTarget, FindFilter, FindOptions, getEntityDefinition } from "../../api";
import { asciiFold } from "../utils";
import { comparisonType } from "src/core/platform/services/database/services/orm/repository/repository";
export function buildSearchQuery<Entity>(
entityType: EntityTarget<Entity>,
@@ -45,7 +46,44 @@ export function buildSearchQuery<Entity>(
}
}
//TODO implement $gte, $lt, etc
function buildRangeQuery(
lteOperations: comparisonType[],
key: "gte" | "lte" | "lt" | "gt",
): void {
for (const lteOperation of lteOperations) {
if (lteOperation?.length == 2 && lteOperation[0]) {
const field_name = lteOperation[0];
esBody.query.bool.must.push({
range: {
[field_name]: {
[key]: lteOperation[1] ? parseInt(lteOperation[1]) : 0,
},
},
});
} else {
logger.warn(`Not enough data to include to the query: ${lteOperation}`);
}
}
}
if (options.$lte?.length) {
esBody.query.bool.must = esBody.query.bool.must || [];
buildRangeQuery(options.$lte, "lte");
}
if (options.$gte?.length) {
esBody.query.bool.must = esBody.query.bool.must || [];
buildRangeQuery(options.$gte, "gte");
}
if (options.$lt?.length) {
esBody.query.bool.must = esBody.query.bool.must || [];
buildRangeQuery(options.$lt, "lt");
}
if (options.$gt?.length) {
esBody.query.bool.must = esBody.query.bool.must || [];
buildRangeQuery(options.$gt, "gt");
}
if (options.$text) {
esBody.query.bool.minimum_should_match = 1;
@@ -84,7 +84,9 @@ export default class MongoSearch extends SearchAdapter implements SearchAdapterI
//Create one index for each type of indexes ["text"]
Object.keys(indexedFields).forEach(k => {
collection.createIndex(indexedFields[k], { default_language: "none" });
collection.createIndex(indexedFields[k], {
default_language: "none",
});
});
}
@@ -58,6 +58,10 @@ export function buildSearchQuery<Entity>(
});
}
if (options.$sort) {
sort = options.$sort;
}
return {
project,
query,
@@ -26,7 +26,7 @@ type TextType = {
$diacriticSensitive?: boolean; //Default false
};
type SortType = {
export type SortType = {
[key: string]: "asc" | "desc";
};
@@ -8,43 +8,36 @@ export default {
creator: entity.creator,
added: entity.added,
name: entity.name,
//especially for ES because it doesn't allow to sort by not a keyword fields
name_keyword: entity.name,
company_id: entity.company_id,
access_users: ["user_1234", "user_454"],
access_users_x_initiator: ["user_1234_x_user_4343", "user_1234_x_user_4343"],
access_entities: [
{
type: "user",
level: "read",
target: "user_1234",
source: "user_5678",
},
{
type: "user",
level: "read",
target: "user_abcd",
source: "user_5678",
},
],
access_entities: entity.access_info?.entities?.filter(e => e.level != "none").map(e => e.id),
last_modified: entity.last_modified,
mime_type: entity.last_version_cache?.file_metadata?.mime,
}),
mongoMapping: {
text: {
content_keywords: "text",
tags: "text",
creator: "text",
added: "text",
name: "text",
name_keyword: "text",
company_id: "text",
mime_type: "text",
},
},
esMapping: {
properties: {
name: { type: "text", index_prefixes: { min_chars: 1 } },
name_keyword: { type: "keyword" },
content_keywords: { type: "text", index_prefixes: { min_chars: 1 } },
tags: { type: "keyword" },
creator: { type: "keyword" },
added: { type: "keyword" },
added: { type: "unsigned_long" },
company_id: { type: "keyword" },
access_entities: { type: "keyword" },
mime_type: { type: "keyword" },
last_modified: { type: "unsigned_long" },
},
},
};
@@ -48,13 +48,13 @@ export class DriveFile {
@Column("tags", "encoded_json")
tags: string[];
@Type(() => String)
@Column("added", "string")
added: string;
@Type(() => Number)
@Column("added", "number")
added: number;
@Type(() => String)
@Column("last_modified", "string")
last_modified: string;
@Type(() => Number)
@Column("last_modified", "number")
last_modified: number;
@Column("access_info", "encoded_json")
access_info: AccessInformation;
@@ -1,7 +1,10 @@
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 from "../../../core/platform/services/database/services/orm/repository/repository";
import Repository, {
inType,
comparisonType,
} from "../../../core/platform/services/database/services/orm/repository/repository";
import { PublicFile } from "../../../services/files/entities/file";
import globalResolver from "../../../services/global-resolver";
import { hasCompanyAdminLevel } from "../../../utils/company";
@@ -291,9 +294,9 @@ export class DocumentsService {
await this.repository.save(driveItem);
await updateItemSize(driveItem.parent_id, this.repository, context);
this.notifyWebsocket(driveItem.parent_id, context);
await this.notifyWebsocket(driveItem.parent_id, context);
globalResolver.platformServices.messageQueue.publish<DocumentsMessageQueueRequest>(
await globalResolver.platformServices.messageQueue.publish<DocumentsMessageQueueRequest>(
"services:documents:process",
{
data: {
@@ -307,7 +310,7 @@ export class DocumentsService {
return driveItem;
} catch (error) {
this.logger.error("Failed to create drive item", error);
throw new CrudException("Failed to create item", 500);
CrudException.throwMe(error, new CrudException("Failed to create item", 500));
}
};
@@ -768,12 +771,39 @@ export class DocumentsService {
pagination: {
limitStr: "100",
},
...(options.company_id ? { $in: [["company_id", [options.company_id]]] } : {}),
...(options.creator ? { $in: [["creator", [options.creator]]] } : {}),
...(options.added ? { $in: [["added", [options.added]]] } : {}),
$text: {
$search: options.search,
},
$in: [
["access_entities", [context.user.id, context.company.id]],
...(options.company_id ? [["company_id", [options.company_id]] as inType] : []),
...(options.creator ? [["creator", [options.creator]] as inType] : []),
...(options.mime_type
? [
[
"mime_type",
Array.isArray(options.mime_type) ? options.mime_type : [options.mime_type],
] as inType,
]
: []),
],
$lte: [
...(options.last_modified_lt
? [["last_modified", options.last_modified_lt] as comparisonType]
: []),
...(options.added_lt ? [["added", options.added_lt] as comparisonType] : []),
],
$gte: [
...(options.last_modified_gt
? [["last_modified", options.last_modified_gt] as comparisonType]
: []),
...(options.added_gt ? [["added", options.added_gt] as comparisonType] : []),
],
...(options.search
? {
$text: {
$search: options.search,
},
}
: {}),
...(options.sort ? { $sort: options.sort } : {}),
},
context,
);
@@ -1,6 +1,7 @@
import { ExecutionContext } from "../../core/platform/framework/api/crud-service";
import { DriveFile } from "./entities/drive-file";
import { FileVersion } from "./entities/file-version";
import { SortType } from "src/core/platform/services/search/api";
export interface CompanyExecutionContext extends ExecutionContext {
company: { id: string };
@@ -40,14 +41,24 @@ export type SearchDocumentsOptions = {
search?: string;
company_id?: string;
creator?: string;
added?: string;
added_gt?: number;
added_lt?: number;
mime_type?: Array<string>;
last_modified_gt?: number;
last_modified_lt?: number;
sort?: SortType;
};
export type SearchDocumentsBody = {
search?: string;
company_id?: string;
creator?: string;
added?: string;
added_gt?: number;
added_lt?: number;
mime_type?: [string];
last_modified_gt?: number;
last_modified_lt?: number;
sort?: SortType;
};
export type DocumentsMessageQueueRequest = {
@@ -47,11 +47,11 @@ export const getDefaultDriveItem = (
): DriveFile => {
const defaultDriveItem = merge<DriveFile, Partial<DriveFile>>(new DriveFile(), {
company_id: context.company.id,
added: item.added || new Date().getTime().toString(),
added: item.added || new Date().getTime(),
creator: item.creator || context.user?.id,
is_directory: item.is_directory || false,
is_in_trash: item.is_in_trash || false,
last_modified: new Date().getTime().toString(),
last_modified: new Date().getTime(),
parent_id: item.parent_id || "root",
content_keywords: item.content_keywords || "",
description: item.description || "",
@@ -422,7 +422,9 @@ export const getFileMetadata = async (
* Finds a suitable name for an item based on items inside the same folder.
*
* @param {string} parent_id - the parent id.
* @param id
* @param {string} name - the item name.
* @param is_directory
* @param {Repository<DriveFile>} repository - the drive repository.
* @param {CompanyExecutionContext} context - the execution context.
* @returns {Promise<string>} - the drive item name.
@@ -65,7 +65,7 @@ export class DocumentsController {
);
} catch (error) {
logger.error("Failed to create Drive item", error);
throw new CrudException("Failed to create Drive item", 500);
CrudException.throwMe(error, new CrudException("Failed to create Drive item", 500));
}
};
@@ -73,6 +73,7 @@ export class DocumentsController {
* Deletes a DriveFile item or empty the trash or delete root folder contents
*
* @param {FastifyRequest} request
* @param {FastifyReply} reply
* @returns {Promise<void>}
*/
delete = async (
@@ -227,7 +228,7 @@ export class DocumentsController {
* If the item is a folder, a zip will be automatically generated.
*
* @param {FastifyRequest} request
* @param {FastifyReply} reply
* @param {FastifyReply} response
*/
download = async (
request: FastifyRequest<{
@@ -342,26 +343,27 @@ export class DocumentsController {
): Promise<ListResult<DriveFile>> => {
try {
const context = getDriveExecutionContext(request);
const { search = "", added = "", company_id = "", creator = "" } = request.body;
const options: SearchDocumentsOptions = {
company_id: company_id || context.company.id,
...(search ? { search } : {}),
...(added ? { added } : {}),
...(creator ? { creator } : {}),
...request.body,
company_id: request.body.company_id || context.company.id,
};
if (!Object.keys(options).length) {
throw Error("Search options are empty");
this.throw500Search();
}
return await globalResolver.services.documents.documents.search(options, context);
} catch (error) {
logger.error("error while searching for document", error);
throw new CrudException("Failed to search for documents", 500);
this.throw500Search();
}
};
private throw500Search() {
throw new CrudException("Failed to search for documents", 500);
}
getTab = async (
request: FastifyRequest<{
Params: { tab_id: string; company_id: string };
@@ -187,6 +187,9 @@ export class FileServiceImpl {
},
): Promise<File> {
let entity = await this.repository.findOne(pk, {}, context);
if (entity == null) {
throw new CrudException(`File not found ${pk.company_id}|${pk.id}`, 404);
}
if (options?.waitForThumbnail) {
for (let i = 1; i < 100; i++) {
+2
View File
@@ -103,7 +103,9 @@ export interface ResourceEventsPayload {
}
export interface PaginationQueryParameters {
// It is offset for MongoDB and pointer to the next page for ScyllaDB
page_token?: string;
// Page size
limit?: string;
websockets?: boolean;
}