-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
7,433 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
root = true | ||
|
||
[*] | ||
indent_size = 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
dist | ||
.vscode | ||
.cache | ||
examples |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,111 @@ | ||
# filtersjs | ||
|
||
Simple faceted search solution for small JSON datasets. | ||
|
||
## Installation | ||
|
||
`yarn` or `npm install` | ||
|
||
## Usage | ||
|
||
```javascript | ||
// import or require filtersjs | ||
import { FiltersJS, Operation } from "filtersjs"; | ||
|
||
// data to filter | ||
const items = [ | ||
{ | ||
name: "Scot", | ||
gender: "Male", | ||
age: 18, | ||
card: ["jcb", "visa", "mastercard"] | ||
}, | ||
{ | ||
name: "Seana", | ||
gender: "Female", | ||
age: 21, | ||
card: ["jcb", "visa"] | ||
}, | ||
{ | ||
name: "Ken", | ||
gender: "Male", | ||
age: 45, | ||
card: ["jcb", "visa"] | ||
}, | ||
{ | ||
name: "Boony", | ||
gender: "Male", | ||
age: 47, | ||
card: ["visa"] | ||
}, | ||
{ | ||
name: "Mike", | ||
gender: "Male", | ||
age: 67, | ||
card: ["jcb", "visa"] | ||
} | ||
]; | ||
|
||
/** | ||
* Options object with filters definitions. By default values are converted to lower case strings before match, for custom | ||
* comparator provide a isMatching(val) function. | ||
*/ | ||
const options = { | ||
filters: [ | ||
{ key: "card", value: "jcb" }, | ||
{ key: "card", value: "mastercard" }, | ||
{ key: "card", value: "visa" }, | ||
{ key: "gender", value: "male" }, | ||
{ key: "gender", value: "female" }, | ||
{ | ||
key: "age", | ||
value: "18-25", | ||
isMatching: v => 18 <= v && v <= 25 | ||
}, | ||
{ | ||
key: "age", | ||
value: "25-50", | ||
isMatching: v => 25 <= v && v <= 50 | ||
}, | ||
{ key: "age", value: ">50", isMatching: v => 50 < v } | ||
] | ||
}; | ||
|
||
// create a FiltersJS instance | ||
const filtersJS = new FiltersJS(items, options); | ||
|
||
// call search by providing current filters values | ||
const { results, filters } = filtersJS.search({ | ||
card: { values: ["jcb", "visa"], operation: Operation.And }, | ||
gender: "male", | ||
age: ["18-25", "25-50"] | ||
}); | ||
|
||
// filtered results | ||
console.log(results); | ||
// Prints: | ||
// { | ||
// name: "Scot", | ||
// gender: "Male", | ||
// age: 18, | ||
// card: ["jcb", "visa", "mastercard"] | ||
// }, | ||
// { | ||
// name: "Ken", | ||
// gender: "Male", | ||
// age: 45, | ||
// card: ["jcb", "visa"] | ||
// } | ||
|
||
/** | ||
* Active filters with number of results for each filter (if it will be applied). | ||
* Can be used for basic facating and hiding filters that won't produce any results. | ||
*/ | ||
console.log(filters); | ||
// Prints: | ||
// { | ||
// card: { mastercard: 3 }, | ||
// gender: { female: 3 }, | ||
// age: { ">50": 3 } | ||
// } | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"name": "filtersjs", | ||
"version": "1.0.0", | ||
"description": "Simple faceted search solution for small JSON datasets.", | ||
"main": "src/index.ts", | ||
"repository": "[email protected]:ognus/filtersjs.git", | ||
"author": "Tomek Kolasa <[email protected]>", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"@types/jest": "^24.0.15", | ||
"jest": "^24.8.0", | ||
"parcel-bundler": "^1.12.3", | ||
"ts-jest": "^24.0.2", | ||
"typescript": "^3.5.3" | ||
}, | ||
"scripts": { | ||
"dev": "parcel src/index.ts", | ||
"test": "jest" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
type Item = object; | ||
|
||
type WithId = [number, Item]; | ||
|
||
export type FilterCallback = ( | ||
item: WithId, | ||
idx: number, | ||
array: IDataProvider | ||
) => boolean; | ||
|
||
export type MapCallback = ( | ||
item: WithId, | ||
idx: number, | ||
array: IDataProvider | ||
) => any; | ||
|
||
export interface IDataProvider { | ||
length: number; | ||
get: (id: string | number) => Item; | ||
filter: (cb: FilterCallback) => IDataProvider; | ||
map: (cb: MapCallback) => any[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Finder, EOperation } from "./Finder"; | ||
import { IndexStore } from "./IndexStore"; | ||
import { MutableItemsStore } from "./ItemsStore"; | ||
import { IDataProvider } from "./DataProvider"; | ||
import { ISearchProvider, TFilter } from "./SearchProvider"; | ||
|
||
export class Factory { | ||
private dataStore: IDataProvider; | ||
|
||
constructor(items: object[]) { | ||
this.dataStore = new MutableItemsStore(items); | ||
} | ||
|
||
getSearchProvider(filters: TFilter[]): ISearchProvider { | ||
const index = new IndexStore(this.dataStore); | ||
filters.forEach(filter => index.add(filter)); | ||
return index; | ||
} | ||
|
||
getFinder(index: ISearchProvider, operations: { [key: string]: EOperation }) { | ||
return new Finder(index, this.dataStore, operations); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { ISearchProvider, TTerm } from "./SearchProvider"; | ||
import { IDataProvider } from "./DataProvider"; | ||
|
||
export enum EOperation { | ||
And = "AND", | ||
Or = "OR" | ||
} | ||
|
||
export class Finder { | ||
constructor( | ||
private search: ISearchProvider, | ||
private data: IDataProvider, | ||
private operations: { [key: string]: EOperation } | ||
) {} | ||
|
||
private isMatching( | ||
itemId: number, | ||
propertyKey: string, | ||
values: (string | number)[] | ||
) { | ||
const operation = this.operations[propertyKey]; | ||
|
||
if (operation === EOperation.And) { | ||
return values.every(value => | ||
this.search.isMatching(itemId, propertyKey, value) | ||
); | ||
} | ||
|
||
return values.some(value => | ||
this.search.isMatching(itemId, propertyKey, value) | ||
); | ||
} | ||
|
||
private getGroupedTerms(terms: TTerm[]) { | ||
const termsByKey: { [key: string]: (string | number)[] } = terms.reduce( | ||
(byKey, { key, value }) => { | ||
byKey[key] = byKey[key] || []; | ||
byKey[key].push(value); | ||
return byKey; | ||
}, | ||
{} | ||
); | ||
|
||
return Object.entries(termsByKey); | ||
} | ||
|
||
find(terms: TTerm[]): IDataProvider { | ||
const grouped = this.getGroupedTerms(terms); | ||
|
||
return this.data.filter(([itemId]) => { | ||
return grouped.every(([key, values]) => | ||
this.isMatching(itemId, key, values) | ||
); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { IDataProvider } from "./DataProvider"; | ||
import { | ||
ISearchProvider, | ||
TTerm, | ||
TMatchingFunction, | ||
TFilter | ||
} from "./SearchProvider"; | ||
|
||
function defaultMatchingFunction(value: string | number) { | ||
return ( | ||
new String(this.value.toLowerCase()).toLowerCase() === | ||
new String(value).toLowerCase() | ||
); | ||
} | ||
|
||
class FilterOption implements TTerm { | ||
private itemKey: string; | ||
private optionValue: string | number; | ||
private isMatchingFn: TMatchingFunction; | ||
|
||
constructor( | ||
itemKey: string, | ||
optionValue: string | number, | ||
isMatchingFn?: TMatchingFunction | ||
) { | ||
this.itemKey = itemKey; | ||
this.optionValue = optionValue; | ||
this.isMatchingFn = isMatchingFn || defaultMatchingFunction.bind(this); | ||
} | ||
|
||
private getValues(item = {}) { | ||
const value = item[this.itemKey]; | ||
return Array.isArray(value) ? value : [value]; | ||
} | ||
|
||
isMatching(item: object) { | ||
return this.getValues(item).some(value => this.isMatchingFn(value)); | ||
} | ||
|
||
get value() { | ||
return this.optionValue; | ||
} | ||
|
||
get key() { | ||
return this.itemKey; | ||
} | ||
} | ||
|
||
export class IndexStore implements ISearchProvider { | ||
private index: { | ||
[itemKey: string]: { [optionValue: string]: IDataProvider }; | ||
} = {}; | ||
private options: FilterOption[] = []; | ||
|
||
constructor(private items: IDataProvider) {} | ||
|
||
add({ key, value, isMatching }: TFilter) { | ||
const option = new FilterOption(key, value, isMatching); | ||
this.options.push(option); | ||
this.index[key] = this.index[key] || {}; | ||
this.index[key][value] = this.items.filter(([, item]) => | ||
option.isMatching(item) | ||
); | ||
} | ||
|
||
isMatching( | ||
itemId: string | number, | ||
propertyKey: string, | ||
optionValue: string | number | ||
) { | ||
return !!this.index[propertyKey][optionValue].get(itemId); | ||
} | ||
|
||
getTerms(): TTerm[] { | ||
return this.options; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { IDataProvider, FilterCallback, MapCallback } from "./DataProvider"; | ||
|
||
export class ItemsStore implements IDataProvider { | ||
protected itemsById: { [id: number]: object } = {}; | ||
protected items: [number, object][] = []; | ||
|
||
constructor(items: [number, object][]) { | ||
this.items = items; | ||
this.itemsById = Object.fromEntries(items); | ||
} | ||
|
||
get(id: string | number) { | ||
return this.itemsById[id]; | ||
} | ||
|
||
filter(precicate: FilterCallback) { | ||
return new ItemsStore( | ||
this.items.filter((itemWithId, idx) => precicate(itemWithId, idx, this)) | ||
); | ||
} | ||
|
||
map(iterator: MapCallback) { | ||
return this.items.map((itemWithId, idx) => iterator(itemWithId, idx, this)); | ||
} | ||
|
||
get length() { | ||
return this.items.length; | ||
} | ||
} | ||
|
||
export class MutableItemsStore extends ItemsStore { | ||
private lastItemId = 0; | ||
|
||
constructor(items: object[]) { | ||
super([]); | ||
items.forEach(item => this.add(item)); | ||
} | ||
|
||
add(item: object) { | ||
if (item) { | ||
const id = ++this.lastItemId; | ||
this.itemsById[id] = item; | ||
this.items.push([id, item]); | ||
return id; | ||
} | ||
|
||
return -1; | ||
} | ||
} |
Oops, something went wrong.