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 { 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 = { [P in keyof T]: SearchEngineValue; } & { [P in searchType]?: SearchEngineValue; } & { [key: string]: SearchEngineValue; }; export const createSearchRequest = (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 = (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 = (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; 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 (input: { model: T; query?: any; count?: boolean; pagination?: GetArgs; all?: boolean; engine?: boolean; ctx: ApolloContext | null; }): Promise => { 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).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).getCount({ query: finalSearch, ctx }); } else { if (all) return await ((model as unknown) as EngineModel).getAll({ query: finalSearch, ctx }); return await ((model as unknown) as EngineModel).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; } };