๐ Welcome to EasyFilterParser! ๐
EasyFilterParser is a lightweight โ๏ธ, zero dependencies ๐ข, minimal setup ๐ฎ, intuitive ๐ and powerful ๐ช parser used in the EasyFilter trilogy packages.
It's as easy as this:
const parser = EasyFilterParser()
const { options, searchTree } = parser.search('your query')
npm install @noriller/easy-filter-parser
yarn add @noriller/easy-filter-parser
import EasyFilterParser from '@noriller/easy-filter-parser'
const EasyFilterParser = require('@noriller/easy-filter-parser')
const parser = EasyFilterParser()
const { options, searchTree } = parser.search('your query')
That's it! ๐งโโ๏ธ
Check out the section EasyFilterParser Operators to see all that you can pass to the filter, the real โจmagicโจ is there!
โจ Magic like turning this:
`search for something "this between quotes" and then here:"you search for this"`
โจ
Into something that works for single values, quoted values and even values nested inside keys. AND MORE!
โจ
Ok. If you need more options, here's the full setup you can do using all options available:
const parser = EasyFilterParser({
filterOptions: {
dateFormat: 'DD-MM-YYYY',
normalize: true,
indexing: true,
limit: 10,
},
tagAliases: {
tag: ['tag1', 'tag2', 'tag3'],
}
})
const { options, searchTree } = parser.search('your query')
It's still that simple. ๐จโ๐ป
All the options will be explained in EasyFilterParser Options.
And most of them you can pass in the search
๐ string when you need.
In corporate scenarios, sometimes we have too much information ๐ต. We make pages with endless columns and if we need to filter that data, we either use something generic like Object.keys(object).join(' ').includes('string')
or we have to make a custom search... for. each. table. ๐ซ
Meanwhile I saw awesome (and probably custom solutions) in things we use everyday.
Check out the ones I was aiming for ๐:
- Github
- Stackoverflow
- Gmail/Google Search
In the latter, users can use their UI to create their queries while powerusers can just type that and much more.
I too needed to provide a way to users to filter the data and ended up settling at a simpler version of this project. Mostly because of all the solutions I was able to find were neither user or developer friendly. ๐ข
(Also a little rant: search
, filter
, matcher
and the like are a nightmare to search for... too many hits and too little relevant results ๐)
This is what I'm trying to offer here: a powerful engine to make your queries. ๐๐
Then it's up to you to offer a UI for what makes sense for your data. And it's still intuitive for common users and powerful for powerusers.
Most of this should be intuitive for most users... that's what I was aiming for after all. ๐ง
Any word or operators are, primarily and lastly, treated as OR
queries.
parser.search('word1 word2 tag:value "quoted value"')
// Returns:
{
options: {},
searchTree: [
{
payload: 'quoted value',
mode: 'QUOTE',
childs:
[
{ payload: 'quoted', mode: 'OR', childs: undefined },
{ payload: 'value', mode: 'OR', childs: undefined }
]
},
{
payload: 'value',
tag: 'tag',
mode: 'TAG',
childs: [{ payload: 'value', mode: 'OR', childs: undefined }],
aliases: {}
},
{ payload: 'word1', mode: 'OR', childs: undefined },
{ payload: 'word2', mode: 'OR', childs: undefined }
]
}
word1
, word2
, tag:value
and "quoted value"
each become separated entities.
Anything inside quotes (either double "
or single '
) will be treated as one entity.
parser.search('"quoted value tag:value"')
//Returns:
{
options: {},
searchTree:
[{
payload: 'quoted value tag:value',
mode: 'QUOTE',
childs:
[
{
payload: 'value',
tag: 'tag',
mode: 'TAG',
childs: [{ payload: "value", mode: "OR" }],
aliases: {}
},
{ payload: 'quoted', mode: 'OR', childs: undefined },
{ payload: 'value', mode: 'OR', childs: undefined }
]
}]
}
quoted
, value
and tag:value
became an AND
query.
And in this case, both quoted
and value
become OR
queries and tag:value
becomes TAG
query.
An AND
query can contain: OR
, TAG
and even nested AND
queries.
In case of nested TAG
and AND
queries, the nested quote must not match the parent quote.
TAG here is equivalent to any key
of a Javascript object.
A TAG
query can contain: OR
, AND
and then NULL
and RANGE
/DATE_RANGE
queries.
TAG
doesn't support nested TAG
queries.
parser.search('tag:value')
//Returns:
{
options: {},
searchTree:
[{
payload: 'value',
tag: 'tag',
mode: 'TAG',
childs: [{ payload: 'value', mode: 'OR', childs: undefined }],
aliases: {}
}]
}
Just the TAG
followed by a colon and the value
.
value
in this example will become an OR
query.
parser.search('tag:(value1 value2 value3)')
//Returns:
{
options: {},
searchTree:
[{
payload: 'value1 value2 value3',
tag: 'tag',
mode: 'TAG',
childs:
[
{ payload: 'value1', mode: 'OR', childs: undefined },
{ payload: 'value2', mode: 'OR', childs: undefined },
{ payload: 'value3', mode: 'OR', childs: undefined }
],
aliases: {}
}]
}
By using brackets, you can have an OR
query with multiple values at once.
parser.search('tag:"value1 value2 value3"')
//Returns:
{
options: {},
searchTree:
[{
payload: '"value1 value2 value3"',
tag: 'tag',
mode: 'TAG',
childs:
[{
payload: 'value1 value2 value3',
mode: 'QUOTE',
childs: [
{ payload: "value1", mode: "OR" },
{ payload: "value2", mode: "OR" },
{ payload: "value3", mode: "OR" }
]
}],
aliases: {}
}]
}
By using quotes (single/double), you can have an AND
query.
parser.search('tag:null tag:nil tag:none tag:nothing')
//Returns:
{
options: {},
searchTree:
[
{
payload: 'null',
tag: 'tag',
mode: 'TAG_NULL',
childs: [{ payload: 'null', mode: 'OR', childs: undefined }],
aliases: {}
},
{
payload: 'nil',
tag: 'tag',
mode: 'TAG_NULL',
childs: [{ payload: 'nil', mode: 'OR', childs: undefined }],
aliases: {}
},
{
payload: 'none',
tag: 'tag',
mode: 'TAG_NULL',
childs: [{ payload: 'none', mode: 'OR', childs: undefined }],
aliases: {}
},
{
payload: 'nothing',
tag: 'tag',
mode: 'TAG_NULL',
childs: [{ payload: 'nothing', mode: 'OR', childs: undefined }],
aliases: {}
}
]
}
By passing, alone, any of the words: NULL
, NIL
, NONE
or NOTHING
as the value of the TAG
, it will create a TAG_NULL
that can be used to search nullish values.
tag:(nothing)
, in contrast, will create a normal TAG
query.
parser.search('tag.subTag.thirdTag:value')
//Returns:
{
options: {},
searchTree:
[{
payload: 'value',
tag: 'tag.subTag.thirdTag',
mode: 'TAG',
childs: [{ payload: 'value', mode: 'OR', childs: undefined }],
aliases: {}
}]
}
You can chain tags together using a .
(full stop/period).
This would be equivalent to nested TAGs
. (Nested tags aren't supported.)
parser.search('tag.0:value tag2.*.subTag:value')
//Returns:
{
options: {},
searchTree:
[
{
payload: 'value',
tag: 'tag.0',
mode: 'TAG',
childs: [{ payload: 'value', mode: 'OR', childs: undefined }],
aliases: {}
},
{
payload: 'value',
tag: 'tag2.*.subTag',
mode: 'TAG',
childs: [{ payload: 'value', mode: 'OR', childs: undefined }],
aliases: {}
}
]
}
As a use case in EasyFilter, arrays are supported.
The main difference is that the key
they use are numerical and ordered.
tag.0:value
and tag2.*.subTag:value
have the same syntax as a normal chaining and the difference will happen in the filter implementation.
parser.search('tag:range(0,5)')
//Returns:
{
options: {},
searchTree:
[{
payload: 'range(0,5)',
tag: 'tag',
mode: 'TAG',
childs:
[{
payload: null,
range: [0, 5],
mode: 'RANGE',
childs: undefined
}],
aliases: {}
}]
}
By passing, alone, the operator RANGE()
you can pass one or two arguments that will filter based on the numbers.
RANGE
can only be used inside TAG
and with number
values.
The first argument is the lower bound (-Infinity
as default) and the second argument is the upper bound (Infinity
as default).
Passing only one argument sets only the lower bound. To set only the upper bound, pass it empty: RANGE(,5)
.
parser.search('tag:dateRange(2020-05-01, 2021-09-05)')
//Returns:
{
options: {},
searchTree: [
{
aliases: {},
childs: [
{
childs: undefined,
mode: 'DATE_RANGE',
payload: null,
// the range return is actually: "2020-05-01T00:00:00.000Z","2021-09-05T00:00:00.000Z"
// but it usually shows as a locale date string
range: [new Date('2020-05-01'), new Date('2021-09-05')],
},
],
mode: 'TAG',
payload: 'dateRange(2020-05-01, 2021-09-05)',
tag: 'dates',
},
],
}
By passing, alone, the operator DATERANGE()
you can pass one or two arguments that will filter based on the dates.
DATERANGE
can only be used inside TAG
and with date
values.
The first argument is the lower bound (0000-01-01
as default) and the second argument is the upper bound (9999-01-01
as default).
Passing only one argument sets only the lower bound. To set only the upper bound, pass it empty: DATERANGE(,2021-09-05)
.
More on accepted Date Formats
in Date Format (Query), but you can use all the common formats like DD/MM/YYYY
, MM/DD/YYYY
and YYYY/MM/DD
as long as you pass it as an OPTION
. If no Date Format
is provided, the Javascript default implementation of new Date('your date string')
will be used.
By nesting any and multiple queries inside the syntax NOT()
you can invert those and it will NOT return anything that matches.
A NOT
query can contain: OR
, AND
and TAG
queries.
All NOT
are parsed at the same level, nesting it inside other queries will just remove them from the query.
parser.search('not("quoted value tag:value")')
//Returns:
{
options: {},
searcTree: [{
payload: "\"quoted value tag:value\"",
mode: "NOT",
childs: [
{
payload: "quoted value tag:value",
mode: "QUOTE",
childs: [
{
payload: "value",
tag: "tag",
mode: "TAG",
childs: [{ payload: "value", mode: "OR" }],
aliases: {}
},
{ payload: "quoted", mode: "OR" },
{ payload: "value", mode: "OR" }
]
}
]
}]
}
There's three types of options:
- Those that can be passed any time:
- Those that can only be passed in the setup:
- Those that can only be passed with the query:
Using the syntax OPTION()
or OPTIONS()
you can pass the following options inside your search string.
The OPTION
keyword is parsed first, it will be just removed if nested in other queries and anything else inside will be either parsed as an option or ignored.
When passed as an OPTION
, DateFormat
will be used to parse the dates used in DATE_RANGE
.
This way your users can use their locale date format in their query.
When using DATE_RANGE
, if no DateFormat
is passed as an option the Javascript default implementation of new Date('your date string')
will be used.
The formats can be: YYYY-MM-DD
, DD-MM-YYYY
and MM-DD-YYYY
while the separators can be: -
, .
, ,
and /
.
parser.search('tag:dateRange(30-12-2020,30-12-2022) option(dateFormat:DD.MM.YYYY)')
//Returns:
{
options: { dateFormatSearch: 'DD.MM.YYYY' },
searchTree:
[{
payload: 'dateRange(30-12-2020,30-12-2022)',
tag: 'tag',
mode: 'TAG',
childs:
[{
payload: null,
range:
[new Date('2020-12-30'), new Date('2022-12-30')],
mode: 'DATE_RANGE',
childs: undefined
}],
aliases: {}
}]
}
When the NORMALIZE
option is used, EasyFilterParser
will discard/ignore every and all diacritics. It's FALSE by default.
This means that with NORMALIZE
: Crรจme brรปlรฉe
is equal to Creme brulee
.
EasyFilterParser
uses the string.normalize('NFD')
javascript API to decompose the strings and then remove all Combining Diacritical Marks.
NORMALIZE
uses a boolean
flag, and when used in OPTIONS
alone like option(normalize)
it will assume the TRUE value, but you can explicitly use: normalize:true
.
You can also use normalize:false
to disable a setup default normalization for a specific query.
When the INDEXING
option is used, the option can be used on the filter implementation to have a "relevance score" that you can use to sort the results. It's FALSE by default.
INDEXING
uses a boolean
flag, and when used in OPTIONS
alone like option(index)
or option(indexing)
it will assume the TRUE value, but you can explicitly use: index:true
.
You can also use normalize:false
to disable a setup default indexing for a specific query.
When the LIMIT
option is used, the option can be used on the filter implementation to return only the LIMIT
number of results. It's Zero/FALSE by default.
LIMIT
needs a number
value, when used in OPTIONS
you need to also pass a number
value: option(limit:1)
.
You can also use limit:0
to disable a setup default limit for a specific query.
In the setup you may pass:
The following options works the same way as if passing in the query:
By passing it in the setup, they will be used in every search
.
When passed in the Setup
, DateFormat
will be used to parse the dates in your source
if your implementation uses it.
If no DateFormat
is passed in the setup
, the default implementation of dates will be used.
If that default implementation wouldn't work with your source
, then provide a DateFormat
.
The formats can be: YYYY-MM-DD
, DD-MM-YYYY
and MM-DD-YYYY
while the separators can be: -
, .
, ,
and /
(you can use the provided typing).
Pass TAG Aliases
in the setup to expose to users more friendly (or broader) terms that they can call your data using TAG
.
Tag Aliases
should be a dictionary with key
/value
pairs where the key
is what your users can use and the value
is a array of strings that will refer to your actual data.
Our data sources
might not always be the most user friendly, or something important might be nested where users couldn't possibly know. This is where you use Tag Aliases
.
const parse = EasyFilterParser({
tagAliases: {
// if you want more friendly aliases
data: ['DT_0001X420'],
name: ['nm_first', 'nm_last'],
// if the important data is nested
age: ['person.info.age'],
// if your users expect to find everything related to a word
address: ['address', 'city', 'country', 'province', 'zip_code'],
// and you have no idea which words they will search for
// just create multiple aliases with the same tags
city: ['address', 'city', 'country', 'province', 'zip_code'],
country: ['address', 'city', 'country', 'province', 'zip_code'],
province: ['address', 'city', 'country', 'province', 'zip_code'],
zip: ['address', 'city', 'country', 'province', 'zip_code'],
location: ['address', 'city', 'country', 'province', 'zip_code'],
where: ['address', 'city', 'country', 'province', 'zip_code'],
position: ['address', 'city', 'country', 'province', 'zip_code'],
}
})
parse.search('data.address.name:something')
//Returns:
{
options: {},
searchTree:
[{
payload: 'something',
tag: 'data.address.name',
mode: 'TAG',
childs: [{ payload: 'something', mode: 'OR', childs: undefined }],
aliases:
{
data: ['DT_0001X420'],
name: ['nm_first', 'nm_last'],
address: ['address', 'city', 'country', 'province', 'zip_code']
}
}]
}
Import with:
import { removeDiacritics, cleanString, parseDate } from '@noriller/easy-filter-parser/utils'
OR
const { removeDiacritics, cleanString, parseDate } = require('@noriller/easy-filter-parser/utils')
Inside there's also utils that will be used on the other packages (and that you can use too):
Use to remove diacritics from strings:
removeDiacritics("Crรจme brรปlรฉe")
// returns: "Creme brulee"
Takes a string to be cleaned, trims it and removes double spaces.
Then, if a removeString is provided, it also removes it from the stringToClean.
cleanString(' string dirty to be cleaned ', ' dirty to be ')
// returns: "string cleaned"
Expect a string that should be a date and a DateFormat
to return a Date.UTC date.
If no DateFormat
is specified, it returns the date as passed.
parseDate('05-11-2020', "DD-MM-YYYY"); // returns the equivalent date as: '2020-11-05'
parseDate('05-11-2020', "MM-DD-YYYY"); // returns the equivalent date as: '2020-05-11'
parseDate('2020-11-05'); // just returns: '2020-11-05'
Types you use to instantiate EasyFilterParser
are avaiable alongside the main import.
Import with:
import EasyFilterParser, {
DateFormat,
OptionalParameters,
SetupOptions,
FilterOptions,
TagAliases,
} from '@noriller/easy-filter-parser/types/shapes'
OR
const EasyFilterParser, {
DateFormat,
OptionalParameters,
SetupOptions,
FilterOptions,
TagAliases,
} = require('@noriller/easy-filter-parser/types/shapes')
Returns of the search
method are avaiable by importing from:
import {
ParsedPart,
ParsedRange,
ParsedTag,
} from '@noriller/easy-filter-parser/types'
OR
const {
ParsedPart,
ParsedRange,
ParsedTag,
} = require('@noriller/easy-filter-parser/types')
Here's something you can expect in the future:
EasyFilterParser
will be the base of aEasyFilter
trilogy:- EasyFilter that filters Javascript objects.
EasyFilterParser-SQL
- That will create SQL queries. (I'm working on this now!)EasyFilterParser-Mongo
- That will create Mongo queries. (TBD)
Either if you're encountered a problem: ๐ข or if you're have an idea to make it better: ๐คฉ
Feel free to contribute, to open issues, bug reports or just to say hello! ๐ค๐ค
In case of bugs or errors, if possible, send an example of the query you're using and what you've expected.
Since it supports any kind of queries... who knows what can happen?
https://www.linkedin.com/in/noriller/
- $5 Nice job! Keep it up.
- $10 I really liked that, thank you!
- $42 This is exactly what I was looking for.
- $1K WOW. Did not know javascript could do that!
- $5K I need something done ASAP! Can you do it for yesterday?
- $10K Please consider this: quit your job and work with me!
- $??? Shut up and take my money!