@@ -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>(
|
||||
|
||||
+1
-1
@@ -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]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+2
-2
@@ -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}`);
|
||||
|
||||
+39
-1
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user