diff --git a/interfaces/index.ts b/interfaces/index.ts index 5eaaa18..77e3ee8 100644 --- a/interfaces/index.ts +++ b/interfaces/index.ts @@ -1,4 +1,4 @@ -export type IRuleType = 'string' | 'number' | 'date' +export type IRuleType = 'string' | 'number' | 'date' | 'custom' export enum RuleStringOptions { contains = 'contains', @@ -32,11 +32,15 @@ export enum RuleOperator { } export type IRoles = RuleStringOptions | RuleNumberOptions | RuleDateOptions + +export type IRuleFilter = (datum: T) => boolean + export interface IRule { - field: string - term?: any - role: IRoles type: IRuleType operator: RuleOperator + field?: string + term?: any + role?: IRoles caseSensitive?: boolean + filter?: IRuleFilter } diff --git a/src/index.ts b/src/index.ts index 62ad513..2777c09 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { IRule, RuleOperator } from '../interfaces' import { NumberProcessor } from './utils/number' import { hashCode } from './utils/hash' import { DateProcessor } from './utils/dates' +import { omit } from './utils/helpers/objects' class SearchEngine { private shouldHave: any[] = [] private mustHave: any[] = [] @@ -17,7 +18,7 @@ class SearchEngine { this.mustHave = collectionToBeStored } - search(queries: IRule[]) { + public search(queries: IRule[]) { const query = { '@or': queries.filter((item) => item.operator === RuleOperator.OR), '@and': queries.filter((item) => item.operator === RuleOperator.AND), @@ -51,20 +52,28 @@ class SearchEngine { } private someDataIsValid(queryCurrent: IRule, data: Record) { + const field = queryCurrent.field || '' switch (queryCurrent.type) { case 'string': return new StringProcessor(queryCurrent?.term || null, queryCurrent.role).compareWith( - data[queryCurrent.field], + data[field], queryCurrent.caseSensitive || false, ) case 'number': return new NumberProcessor(queryCurrent?.term || null, queryCurrent.role).compareWith( - data[queryCurrent.field], + data[field], ) case 'date': return new DateProcessor(queryCurrent?.term || null, queryCurrent.role).compareWith( - data[queryCurrent.field], + data[field], ) + case 'custom': + if (queryCurrent.filter && typeof queryCurrent.filter === 'function') { + const datum = omit(data, ['fs_uuid']) + + return queryCurrent.filter(datum) + } + throw new Error('[flexysearch]: Custom filter not valid') default: throw new Error('[flexysearch]: Processor not found') } diff --git a/src/tests/custom.test.ts b/src/tests/custom.test.ts new file mode 100644 index 0000000..65cac71 --- /dev/null +++ b/src/tests/custom.test.ts @@ -0,0 +1,47 @@ +import SearchEngine from '..' +import { RuleOperator } from '../../interfaces' +import collection from '../../__mocks__/movies.json' + +interface IMovie { + id: number + title: string + year: number +} + +describe('Should match strings', () => { + it('[String]: Should cause exception', () => { + const results = () => + new SearchEngine(collection).search([ + { + type: 'custom', + operator: RuleOperator.AND, + }, + ]) + + expect(results).toThrow('[flexysearch]: Custom filter not valid') + }) + it('[String]: Should cause exception', () => { + const results = new SearchEngine(collection).search([ + { + type: 'custom', + operator: RuleOperator.AND, + filter: (datum: IMovie) => { + return datum.year === 2009 + }, + }, + ]) + + expect(results).toStrictEqual([ + { + id: 1, + title: 'Film 1', + year: 2009, + }, + { + id: 7, + title: 'Film 7', + year: 2009, + }, + ]) + }) +}) diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 515c879..a37b412 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,12 +1,12 @@ import { RuleDateOptions } from '../../interfaces/index' import { IRoles } from '../../interfaces' -import { VALIDATE_DATE_REGEXP } from './regexp' +import { VALIDATE_DATE_REGEXP } from './helpers/regexp' export class DateProcessor { private term: string private role: IRoles - constructor(value: string, role: IRoles) { + constructor(value: string, role?: IRoles) { this.term = value this.role = role as RuleDateOptions } diff --git a/src/utils/helpers/objects.ts b/src/utils/helpers/objects.ts new file mode 100644 index 0000000..8affca6 --- /dev/null +++ b/src/utils/helpers/objects.ts @@ -0,0 +1,15 @@ +export const omit = (object: Record, keys: Array): Record => { + if (!object) return {} + try { + const payload = Object.entries(object).filter(([key]) => !keys.includes(key)) + const data: { [key: string]: any } = {} + + payload.forEach((item) => { + const [key, value] = item + data[key] = value + }) + return data + } catch { + return {} + } +} diff --git a/src/utils/regexp/index.ts b/src/utils/helpers/regexp.ts similarity index 100% rename from src/utils/regexp/index.ts rename to src/utils/helpers/regexp.ts diff --git a/src/utils/number.ts b/src/utils/number.ts index a935184..1f677d3 100644 --- a/src/utils/number.ts +++ b/src/utils/number.ts @@ -5,7 +5,7 @@ export class NumberProcessor { private term: number private role: IRoles - constructor(value: number, role: IRoles) { + constructor(value: number, role?: IRoles) { this.term = Number(value) this.role = role as RuleNumberOptions } diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 73cbb1b..cd703db 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -5,7 +5,7 @@ export class StringProcessor { private caseSensitive = false private role: IRoles - constructor(value: string, role: IRoles) { + constructor(value: string, role?: IRoles) { this.term = value this.role = role as RuleStringOptions }