562 lines
23 KiB
TypeScript
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;
|
|
}
|
|
};
|