backend/lib/seed/services/database/DBRequestService.ts
2025-05-14 21:45:16 +02:00

562 lines
23 KiB
TypeScript

import { GetArgs } from '@lib/seed/graphql/Request';
import { parsePaginationOptions } from '@lib/seed/helpers/Request';
import { ApolloContext } from '@lib/seed/interfaces/context';
import { EngineModel } from '@seed/engine/EngineModel';
import { BaseGraphModel } from '@seed/graphql/BaseGraphModel';
import { newError } from '@seed/helpers/Error';
import { escapeRegex } from '@seed/helpers/StringHelper';
import { DateTabType, RangeType } from '@seed/interfaces/components';
import { GeolocSearchComponent } from '@seed/interfaces/components.geo';
import { getGeoLocFromAddress } from '@seed/services/geolocation/services/GooglePlaceService';
import { ApolloError } from 'apollo-server-lambda';
import moment from 'moment';
import { AggregationCursor, Collection } from 'mongodb';
type searchType = 'dateTab' | 'dateRange' | 'hourRange' | 'beetween' | 'bool' | 'regex' | 'nameRegex';
interface SearchEngineValue<T> {
operation:
| 'dateTab'
| 'dateRange'
| 'hourRange'
| 'between'
| '$eq'
| '$gt'
| '$gte'
| '$in'
| '$lt'
| '$lte'
| '$ne'
| '$nin'
| '$not'
| '$exists'
| 'bool'
| 'regex'
| 'nameRegex'
| 'freeInput'
| 'searchText';
dateTabConfig?: {
dbFromField: string;
dbToField: string;
};
dateRangeConfig?: {
inputFromField: keyof T;
inputToField: keyof T;
dbFromField: string;
dbToField: string;
dateRangeType?: RangeType;
};
hourRangeConfig?: {
inputFromField: keyof T;
inputToField: keyof T;
dbMainField: string;
dbSubfieldFrom: string;
dbSubfieldTo: string;
dateRangeType?: RangeType;
};
beetweenConfig?: {
inputFromField: keyof T;
inputToField: keyof T;
};
freeInputConfig?: {
searchOptions: string[];
};
dbFName?: string;
forcedValue?: any;
}
export type CustomSearchEngine<T> = {
[P in keyof T]: SearchEngineValue<T>;
} &
{
[P in searchType]?: SearchEngineValue<T>;
} & {
[key: string]: SearchEngineValue<T>;
};
export const createSearchRequest = <T extends BaseGraphModel>(model: T, args: { search?: string }): any[] | undefined => {
if (!args.search) return;
const search = args.search;
const searchValue = escapeRegex(search);
if (model.searchOptions) {
const searchOptions = model.searchOptions();
if (searchOptions.length > 0) {
const $or: any[] = [];
for (let index = 0; index < searchOptions.length; index++) {
const element = searchOptions[index];
$or.push({ [`${element}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } });
}
return $or;
}
}
return;
};
export const addSearchRequest = <T extends BaseGraphModel>(model: T, args: { search?: string }, filters: { $and: any[] }): any | undefined => {
const $or = createSearchRequest(model, args);
if ($or) filters.$and.push({ $or });
};
export const createDateRangeRequest = (fromField: string, toField: string, args: any, type: RangeType = RangeType.intersect): any | undefined => {
try {
if ((args[fromField] && !args[toField]) || (args[toField] && !args[fromField])) throw newError(3005, '403');
if (args[fromField] && args[toField]) {
if (type == RangeType.strict)
return { [`${fromField}`]: { $gte: moment(args[fromField]).toDate() }, [`${toField}`]: { $lte: moment(args[toField]).toDate() } };
else if (type == RangeType.intersect)
return {
$or: [
{ [`${fromField}`]: { $gte: moment(args[fromField]).toDate(), $lte: moment(args[toField]).toDate() } },
{ [`${toField}`]: { $gte: moment(args[fromField]).toDate(), $lte: moment(args[toField]).toDate() } },
],
};
else
return {
$or: [
{ [`${fromField}`]: { $gte: moment(args[fromField]).toDate() } },
{ [`${toField}`]: { $lte: moment(args[toField]).toDate() } },
],
};
}
return;
} catch (error) {
throw error;
}
};
export const addDateRangeRequest = (
fromField: string,
toField: string,
args: any,
filters: { $and: any[] },
type: RangeType = RangeType.intersect,
): any | undefined => {
try {
const dateQuery = createDateRangeRequest(fromField, toField, args, type);
if (dateQuery) filters.$and.push(dateQuery);
} catch (error) {
throw error;
}
};
export const createTabRangeRequest = (fromField: string, toField: string, type: DateTabType = DateTabType.today): any | undefined => {
let from;
let to;
switch (type) {
case DateTabType.today:
from = moment().startOf('day');
to = moment().endOf('day');
break;
case DateTabType.tomorrow:
from = moment()
.add(1, 'day')
.startOf('day');
to = moment()
.add(1, 'day')
.endOf('day');
break;
case DateTabType.upcomming:
from = moment()
.add(2, 'day')
.startOf('day');
break;
case DateTabType.past:
to = moment().startOf('day');
break;
default:
break;
}
if (from && to) {
return {
$or: [{ [`${fromField}`]: { $gte: from.toDate(), $lte: to.toDate() } }, { [`${toField}`]: { $gte: from.toDate(), $lte: to.toDate() } }],
};
} else if (from) {
return {
$or: [{ [`${fromField}`]: { $gte: from.toDate() } }, { [`${toField}`]: { $gte: from.toDate() } }],
};
} else if (to) {
return {
$or: [{ [`${fromField}`]: { $lte: to.toDate() } }, { [`${toField}`]: { $lte: to.toDate() } }],
};
} else return;
};
export const addDateTabRequest = (
fromField: string,
toField: string,
filters: { $and: any[] },
type: DateTabType = DateTabType.today,
): any | undefined => {
try {
const dateQuery = createTabRangeRequest(fromField, toField, type);
if (dateQuery) filters.$and.push(dateQuery);
} catch (error) {
throw error;
}
};
export const addGeoAggregatePipeline = (longitude: number, latitude: number, radiusInMeter: number, query?: any): any | undefined => {
if (query)
return {
$geoNear: {
near: { type: 'Point', coordinates: [longitude, latitude] },
distanceField: 'dist.calculated',
maxDistance: radiusInMeter,
query: query,
includeLocs: 'dist.location',
spherical: true,
},
};
return {
$geoNear: {
near: { type: 'Point', coordinates: [longitude, latitude] },
distanceField: 'dist.calculated',
maxDistance: radiusInMeter,
includeLocs: 'dist.location',
spherical: true,
},
};
};
export const createGeoAggreatePipeline = (input: {
collection: Collection;
longitude: number;
latitude: number;
radiusInMeter: number;
count?: boolean;
query?: any;
pagination?: { limit: number; skip: number };
}): AggregationCursor => {
// eslint-disable-next-line prefer-const
let { collection, longitude, latitude, radiusInMeter, count, query, pagination } = input;
if (!pagination) pagination = { limit: 100, skip: 0 };
const pipe = addGeoAggregatePipeline(longitude, latitude, radiusInMeter, query);
if (count) {
return collection.aggregate([{ $geoNear: pipe.$geoNear }, { $count: 'count' }]);
} else {
return collection.aggregate([{ $geoNear: pipe.$geoNear }, { $skip: pagination.skip }, { $limit: pagination.limit }]);
}
};
export const addFilterRequest = <T extends BaseGraphModel>(model: T, args: any, filters: { $and: any[] }, check = true): any | undefined => {
if (model.searchEngine) {
const searchEngine = model.searchEngine();
for (const key in searchEngine) {
const el = searchEngine[key] as SearchEngineValue<T>;
const fieldName = el.dbFName || key;
if (el.operation == 'between') {
if (!el.beetweenConfig) throw 'Bad search engine config';
if (args[el.beetweenConfig.inputFromField] && args[el.beetweenConfig.inputToField])
filters.$and.push({
[`${fieldName}`]: { $gte: args[el.beetweenConfig.inputFromField], $lte: args[el.beetweenConfig.inputToField] },
});
else if (args[el.beetweenConfig.inputFromField])
filters.$and.push({ [`${fieldName}`]: { $gte: args[el.beetweenConfig.inputFromField] } });
else if (args[el.beetweenConfig.inputToField])
filters.$and.push({ [`${fieldName}`]: { $lte: args[el.beetweenConfig.inputToField] } });
} else if (el.operation == 'dateRange') {
if (!el.dateRangeConfig) throw 'Bad search engine config';
let { inputFromField, inputToField, dbFromField, dbToField, dateRangeType } = el.dateRangeConfig;
// Check who has the args
const argsToUse = args[key] || args;
if (argsToUse['rangeType']) dateRangeType = argsToUse['rangeType'];
if (argsToUse[inputFromField] && argsToUse[inputToField]) {
if (dateRangeType == RangeType.strict)
if (dbFromField == dbToField)
filters.$and.push({
[`${dbFromField}`]: {
$gte: moment(argsToUse[inputFromField]).toDate(),
$lte: moment(argsToUse[inputToField]).toDate(),
},
});
else
filters.$and.push({
[`${dbFromField}`]: { $gte: moment(argsToUse[inputFromField]).toDate() },
[`${dbToField}`]: { $lte: moment(argsToUse[inputToField]).toDate() },
});
else if (dateRangeType == RangeType.intersect)
filters.$and.push({
$or: [
{
[`${dbFromField}`]: {
$gte: moment(argsToUse[inputFromField]).toDate(),
$lte: moment(argsToUse[inputToField]).toDate(),
},
},
{
[`${dbToField}`]: {
$gte: moment(argsToUse[inputFromField]).toDate(),
$lte: moment(argsToUse[inputToField]).toDate(),
},
},
],
});
else if (dateRangeType == RangeType.included) {
if (dbFromField == dbToField)
filters.$and.push({
[`${dbFromField}`]: {
$lte: moment(argsToUse[inputToField]).toDate(),
$gte: moment(argsToUse[inputFromField]).toDate(),
},
});
else
filters.$and.push({
[`${dbFromField}`]: { $lte: moment(argsToUse[inputFromField]).toDate() },
[`${dbToField}`]: { $gte: moment(argsToUse[inputToField]).toDate() },
});
} else
filters.$and.push({
$or: [
{ [`${dbFromField}`]: { $gte: moment(argsToUse[inputFromField]).toDate() } },
{ [`${dbToField}`]: { $lte: moment(argsToUse[inputToField]).toDate() } },
],
});
}
} else if (el.operation == 'hourRange') {
if (!el.hourRangeConfig) throw 'Bad search engine config';
const { inputFromField, inputToField, dbMainField, dbSubfieldFrom, dbSubfieldTo, dateRangeType } = el.hourRangeConfig;
// Get the subfi
if (args[inputFromField] && args[inputToField]) {
if (dateRangeType == RangeType.strict)
filters.$and.push({
[`${dbMainField}`]: {
$elemMatch: {
[`${dbSubfieldFrom}`]: { $gte: args[inputFromField] },
[`${dbSubfieldTo}`]: { $lte: args[inputToField] },
},
},
});
else if (dateRangeType == RangeType.intersect)
filters.$and.push({
[`${dbMainField}`]: {
$elemMatch: {
$or: [
{
[`${dbSubfieldFrom}`]: {
$gte: args[inputFromField],
$lte: args[inputToField],
},
},
{ [`${dbSubfieldTo}`]: { $gte: args[inputFromField], $lte: args[inputToField] } },
],
},
},
});
else if (dateRangeType == RangeType.included)
filters.$and.push({
[`${dbMainField}`]: {
$elemMatch: {
[`${dbSubfieldFrom}`]: { $lte: args[inputFromField] },
[`${dbSubfieldTo}`]: { $gte: args[inputToField] },
},
},
});
else
filters.$and.push({
$elemMatch: {
$or: [
{ [`${dbSubfieldFrom}`]: { $gte: args[inputFromField] } },
{ [`${dbSubfieldTo}`]: { $lte: args[inputToField] } },
],
},
});
}
} else if (el.operation == 'dateTab') {
if (!el.dateTabConfig) throw 'Bad search engine config';
const { dbFromField, dbToField } = el.dateTabConfig;
if (args.dateTabType) addDateTabRequest(dbFromField, dbToField, filters, args.dateTabType);
} else if (el.operation == 'bool') {
if (args[key] === false || args[key] === true) {
filters.$and.push({ [`${fieldName}`]: args[key] });
}
} else if (el.operation == 'nameRegex') {
if (args[key]) {
const searchValue = escapeRegex(args[key]);
filters.$and.push({
$expr: {
$regexMatch: {
input: { $concat: ['$firstName', ' ', '$lastName'] },
regex: `.*${searchValue}.*`,
options: 'i',
},
},
});
}
} else if (el.operation == 'regex') {
if (args[key]) {
const searchValue = args[key].replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
filters.$and.push({ [`${fieldName}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } });
}
} else if (el.operation == 'freeInput') {
if (!el.freeInputConfig) throw 'Bad search engine config';
const freeInputValue = args[key];
if (freeInputValue) {
const searchValue = escapeRegex(freeInputValue);
const searchOptions = el.freeInputConfig.searchOptions;
if (searchOptions.length > 0) {
const $or: any[] = [];
for (let index = 0; index < searchOptions.length; index++) {
const element = searchOptions[index];
$or.push({ [`${element}`]: { $regex: `.*${searchValue}.*`, $options: 'i' } });
}
filters.$and.push({ $or });
}
}
} else if (el.operation == 'searchText') {
if (args[key]) {
const searchTextValue = escapeRegex(args[key]);
// if (searchTextValue) filters.$and.push({ $text: { $search: searchTextValue } });
if (searchTextValue) filters.$and.push({ searchT: { $regex: `.*${searchTextValue}.*`, $options: 'i' } });
}
} else {
if (el.forcedValue != undefined) filters.$and.push({ [`${fieldName}`]: { [`${el.operation}`]: el.forcedValue } });
else if (args[key] != undefined) filters.$and.push({ [`${fieldName}`]: { [`${el.operation}`]: args[key] } });
}
}
} else {
const filterOptions = model.filterOptions ? model.filterOptions() : null;
delete args.search;
for (const key in args) {
const element = args[key];
if (check) {
if (filterOptions && filterOptions.includes(key)) {
if (key == '_ids') {
filters.$and.push({ _id: { $in: (element as string).replace(' ', '').split(',') } });
} else filters.$and.push({ [`${key}`]: element });
}
} else {
if (key == '_ids') {
filters.$and.push({ _id: { $in: (element as string).replace(' ', '').split(',') } });
} else filters.$and.push({ [`${key}`]: element });
}
}
}
return;
};
export const baseSearchFunction = async <T extends BaseGraphModel>(input: {
model: T;
query?: any;
count?: boolean;
pagination?: GetArgs;
all?: boolean;
engine?: boolean;
ctx: ApolloContext | null;
}): Promise<any> => {
const { model, query, pagination, all, ctx } = input;
let finalSearch: any = { $and: [] };
let gSearch: GeolocSearchComponent | undefined;
let count = false;
if (query) {
const { search, geoSearch, geoSearchWithAddress, /*geoPlaceSearch,*/ afterCreatedAt, afterUpdatedAt, _ids, n_ids, ...filterArgument } = query;
if (input.count) count = true;
if (search) {
const $or = createSearchRequest(model, query);
if ($or) finalSearch.$and.push({ $or });
}
if (geoSearch) gSearch = geoSearch;
else if (geoSearchWithAddress) {
const geoloc = await getGeoLocFromAddress(geoSearchWithAddress.formattedAddress);
gSearch = {
longitude: geoloc.loc.coordinates[0],
latitude: geoloc.loc.coordinates[1],
radius: geoSearchWithAddress.radius,
};
}
if (filterArgument) {
addFilterRequest(model, { search, ...filterArgument }, finalSearch, false);
}
if (_ids) finalSearch.$and.push({ _id: { $in: _ids } });
// eslint-disable-next-line @typescript-eslint/camelcase
if (n_ids) finalSearch.$and.push({ _id: { $nin: n_ids } });
if (afterCreatedAt) finalSearch.$and.push({ createdAt: { $gte: new Date(afterCreatedAt) } });
if (afterUpdatedAt) finalSearch.$and.push({ updatedAt: { $gte: new Date(afterUpdatedAt) } });
}
// if (query.search) finalSearch.$and.push({ search: { $regex: `.*${query.search}.*`, $options: 'i' } });
// Check if exist in DB
if (finalSearch.$and.length == 0) finalSearch = {};
try {
if (gSearch) {
let pipelineCursor;
const finalQ = model.getQuery(finalSearch, ctx);
if (count) {
pipelineCursor = createGeoAggreatePipeline({
...gSearch,
collection: await model.db(),
query: finalQ,
radiusInMeter: gSearch.radius,
count: true,
});
} else {
const paginationOption = parsePaginationOptions(pagination);
pipelineCursor = createGeoAggreatePipeline({
...gSearch,
collection: await model.db(),
query: finalQ,
radiusInMeter: gSearch.radius,
pagination: paginationOption,
});
}
if (input.engine) {
const result = await pipelineCursor.toArray();
if (count) return result[0] || 0;
else return ((model as unknown) as EngineModel<any, any, any>).plainToClass(result);
} else {
const result = await pipelineCursor.toArray();
if (count) return result[0] || 0;
else return result;
}
} else {
if (input.engine) {
if (count) {
console.log('counting');
return await ((model as unknown) as EngineModel<any, any, any>).getCount({ query: finalSearch, ctx });
} else {
if (all) return await ((model as unknown) as EngineModel<any, any, any>).getAll({ query: finalSearch, ctx });
return await ((model as unknown) as EngineModel<any, any, any>).getMany({ query: finalSearch, pagination, ctx });
}
} else {
if (count) {
console.log('counting');
return await model.getCount(finalSearch, ctx);
} else {
if (all) return await model.getAll(finalSearch, ctx);
return await model.getMany(finalSearch, pagination, ctx);
}
}
}
} catch (error) {
throw error;
}
};