Support object queries, not just arrays. #20

Open
gaiety wants to merge 2 commits from collections into master
4 changed files with 271 additions and 109 deletions

View file

@ -2,34 +2,38 @@
[![Build Status](https://travis-ci.org/sharpshark28/json-query-chain.svg?branch=master)](https://travis-ci.org/sharpshark28/json-query-chain) [![npm version](https://badge.fury.io/js/json-query-chain.svg)](https://badge.fury.io/js/json-query-chain) ![Code Coverage](coverage.svg) [![Maintainability](https://api.codeclimate.com/v1/badges/4dc20d8b5e6a7334044d/maintainability)](https://codeclimate.com/github/sharpshark28/json-query-chain/maintainability) [![Build Status](https://travis-ci.org/sharpshark28/json-query-chain.svg?branch=master)](https://travis-ci.org/sharpshark28/json-query-chain) [![npm version](https://badge.fury.io/js/json-query-chain.svg)](https://badge.fury.io/js/json-query-chain) ![Code Coverage](coverage.svg) [![Maintainability](https://api.codeclimate.com/v1/badges/4dc20d8b5e6a7334044d/maintainability)](https://codeclimate.com/github/sharpshark28/json-query-chain/maintainability)
Chain queries onto POJOs to return precise results. Chain queries onto arrays and arrays of objects to return precise results. See [example usages in the tests](./index.test.js) running on [this test data](./testdata.json).
## Usage ## Usage
```javascript ```javascript
import Query from 'json-query-chain'; import Query from 'json-query-chain';
let myQ = new Query(someJsonData) let query = new Query(someJsonData)
.search('isActiveUser', true) .search('isActiveUser', true)
.results; .results;
console.log('Results: ', query);
``` ```
### Chainable Methods ### Chainable Methods
#### Search #### Search
Currently supports booleans and strings. (See [#1](https://github.com/sharpshark28/json-query-chain/issues/1) for Integer Support)
##### By Boolean ##### By Boolean
Acts as a smart filter returning only elements who's key matches the expected result.
```javascript ```javascript
.search('isActiveUser', true) .search(true, 'isActiveUser')
``` ```
##### By String ##### By String
Sorts and filters out any elements in the array not matching the requested value while attempting to raise the best results to the top (most frequent number of occurrences).
```javascript ```javascript
.search('name', 'steele') .search('steele', 'name')
``` ```
#### Filter #### Filter
@ -42,28 +46,42 @@ Simpler version of search using a custom function in the chain.
##### By Key ##### By Key
Like `.filter()`, but narrowed down by key.
```javascript ```javascript
.filterBy('age', x => x >= 21) .filterBy('age', x => x >= 21)
``` ```
#### Sort #### Sort
A chainable version of javascript's built in array sort.
```
.sort((a, b) => a > b)
```
##### By Boolean ##### By Boolean
Abstracted sort by matching a key to a boolean.
```javascript ```javascript
.sort('isActiveUser', true) .sortBy('isActiveUser')
``` ```
##### By String ##### By String
Sorts alphabetically based on key.
```javascript ```javascript
.sort('name') .sortBy('name')
``` ```
##### By Number ##### By Number
Sorts by ascending numerical order based on key.
```javascript ```javascript
.sort('netWorth') .sortBy('netWorth')
``` ```
#### Pagination #### Pagination
@ -74,7 +92,7 @@ Page 1 with 5 results per page.
.paginate(1, 5) .paginate(1, 5)
``` ```
Page 2 wtih default of 10 results per page. Page 2 with default of 10 results per page.
```javascript ```javascript
.paginate(2) .paginate(2)

View file

@ -1,9 +1,6 @@
module.exports = class Query { module.exports = class Query {
constructor (data) { constructor (data) {
this.data = data.map(item => { this.data = data
item.sortScore = 0
return item
})
} }
get results () { get results () {
@ -16,33 +13,47 @@ module.exports = class Query {
} }
filterBy (key, func) { filterBy (key, func) {
this.data = this.data.filter(item => func(item[key])) if (this.data.length && typeof this.data[0] === 'object') {
this.data = this.data.filter(item => func(item[key]))
} else console.warn('Attempted to use filterBy with a flat array')
return this return this
} }
search (key, term, score = 0) { search (term, key) {
switch (typeof term) { switch (typeof term) {
case 'boolean': case 'boolean':
this.data = this.data.filter(item => item[key] === term) if (this.data.length && typeof this.data[0] === 'object') {
this.data = this.data.filter(item => item[key] === term)
} else console.warn('Attempted to use search by boolean with a flat array')
break break
case 'string': case 'string':
this.data = this.data.filter(item => { let regFind = new RegExp(term, 'gi')
let regFind = new RegExp(term, 'gi') let getMatches = item => {
let termMatches = (item[key].match(regFind) || []).length if (typeof item === 'string') {
item.sortScore += termMatches return (item.match(regFind) || []).length
return termMatches } else {
}) return (item[key].match(regFind) || []).length
}
}
this.data = [...this.data].sort(getMatches).filter(getMatches)
break break
} }
return this return this
} }
sort (key = 'sortScore') { sort (func) {
this.data = this.data.sort((a, b) => { this.data = [...this.data].sort(func)
if (a[key] < b[key]) return -1 return this
if (a[key] > b[key]) return 1 }
return 0
}) sortBy (key) {
if (this.data.length && typeof this.data[0] === 'object') {
this.data = [...this.data].sort((a, b) => {
if (a[key] < b[key]) return -1
if (a[key] > b[key]) return 1
return 0
})
} else console.warn('Attempted to use sortBy with a flat array. Try .sort(func) instead.')
return this return this
} }

View file

@ -1,6 +1,8 @@
const Query = require('./index') const Query = require('./index')
const TestData = require('./testdata.json') const TestData = require('./testdata.json')
const arrayOf100Things = [...Array(100).keys()]
test('should not modify passed data without chain alterations', () => { test('should not modify passed data without chain alterations', () => {
let query = new Query(TestData) let query = new Query(TestData)
.results .results
@ -8,102 +10,233 @@ test('should not modify passed data without chain alterations', () => {
expect(query).toMatchObject(TestData) expect(query).toMatchObject(TestData)
}) })
test('should paginate with default params', () => { describe('.paginate()', () => {
let query = new Query(TestData) test('with a flat array', () => {
.paginate() let query = new Query(arrayOf100Things)
.results .paginate()
.results
expect(query.length).toBe(9) expect(query.length).toBe(10)
})
test('using default params', () => {
let query = new Query(TestData)
.paginate()
.results
expect(query.length).toBe(9)
})
test('with custom page length, first page', () => {
let query = new Query(TestData)
.paginate(1, 3)
.results
expect(query.length).toBe(3)
expect(query[0].name).toBe('Haynes Meadows')
})
test('with custom page length, second page', () => {
let query = new Query(TestData)
.paginate(2, 3)
.results
expect(query.length).toBe(3)
expect(query[0].name).toBe('Howard Buckley')
})
}) })
test('should paginate with custom page length', () => { describe('.search()', () => {
let query = new Query(TestData) test('partial string in flat array of strings', () => {
.paginate(1, 3) let query = new Query(['bar', 'foo', 'foobar', 'foofoobar'])
.results .search('foo')
.results
expect(query.length).toBe(3) expect(query[0]).toBe('foofoobar')
expect(query[0].name).toBe('Haynes Meadows') expect(query).toContain('foo')
expect(query).not.toContain('bar')
})
test('by name with value/key', () => {
let query = new Query(TestData)
.search('steele', 'name')
.results
expect(query.length).toBe(2)
})
test('by boolean isActive', () => {
let query = new Query(TestData)
.search(true, 'isActive')
.results
expect(query.length).toBe(4)
})
test('by boolean isActive, false', () => {
let query = new Query(TestData)
.search(false, 'isActive')
.results
expect(query.length).toBe(5)
})
test('warn when searching a flat array of booleans', () => {
let consoleWarnSpy = jest.spyOn(global.console, 'warn')
consoleWarnSpy.mockImplementation(() => {})
let query = new Query(arrayOf100Things)
.search(true, 'N/A')
.results
expect(query).toEqual(arrayOf100Things)
expect(consoleWarnSpy).toHaveBeenCalled()
consoleWarnSpy.mockReset()
consoleWarnSpy.mockRestore()
})
}) })
test('should paginate to second page with custom page length', () => { describe('.sort()', () => {
let query = new Query(TestData) test('with flat array', () => {
.paginate(2, 3) let alphabetical = (a, b) => a > b
.results
expect(query.length).toBe(3) let query = new Query(['foo', 'bar', 'foobar'])
expect(query[0].name).toBe('Howard Buckley') .sort(alphabetical)
.results
expect(query[0]).toBe('bar')
expect(query[1]).toBe('foo')
expect(query[2]).toBe('foobar')
})
test('with an array of objects', () => {
let alphabetical = (a, b) => a.name > b.name
let query = new Query(TestData)
.sort(alphabetical)
.results
expect(query[0].name).toBe('Dudley Conner')
expect(query[query.length - 1].name).toBe('Wade Steele')
})
}) })
test('should search by boolean isActive', () => { describe('.sortBy()', () => {
let query = new Query(TestData) test('by boolean', () => {
.search('isActive', true) let query = new Query(TestData)
.results .sortBy('isActive')
.results
expect(query.length).toBe(4) expect(query[0].name).toBe('Katelyn Steele')
})
test('by number', () => {
let query = new Query(TestData)
.sortBy('netWorth')
.results
expect(query[0].name).toBe('Howard Buckley') // Negative
expect(query[1].name).toBe('Natalia Petty') // 0
expect(query[query.length - 1].name).toBe('Newman Mays') // Richest
})
test('by string', () => {
let query = new Query(TestData)
.sortBy('name')
.results
expect(query[0].name).toBe('Dudley Conner')
})
test('warn when using sortBy with a flat array', () => {
let consoleWarnSpy = jest.spyOn(global.console, 'warn')
consoleWarnSpy.mockImplementation(() => {})
let query = new Query(arrayOf100Things)
.sortBy('N/A')
.results
expect(query).toEqual(arrayOf100Things)
expect(consoleWarnSpy).toHaveBeenCalled()
consoleWarnSpy.mockReset()
consoleWarnSpy.mockRestore()
})
}) })
test('should search by name', () => { describe('.filter()', () => {
let query = new Query(TestData) test('with a flat array', () => {
.search('name', 'steele') let isEven = a => !(a % 2)
.results
expect(query.length).toBe(2) let query = new Query(arrayOf100Things)
.filter(isEven)
.results
expect(query).toContain(2)
expect(query).not.toContain(1)
})
test('with an array of objects', () => {
let isAgeOver33 = a => a.age > 33
let query = new Query(TestData)
.filter(isAgeOver33)
.results
expect(query[0].name).toBe('Howard Buckley')
})
}) })
test('should sort by boolean isActive', () => { describe('.filterBy()', () => {
let query = new Query(TestData) test('by key', () => {
.sort('isActive') let isNumGT33 = num => num > 33
.results
expect(query[0].name).toBe('Katelyn Steele') let query = new Query(TestData)
.filterBy('age', isNumGT33)
.results
expect(query[0].name).toBe('Howard Buckley')
})
test('warn when using filterBy with a flat array', () => {
let consoleWarnSpy = jest.spyOn(global.console, 'warn')
consoleWarnSpy.mockImplementation(() => {})
let query = new Query(arrayOf100Things)
.filterBy('N/A', () => {})
.results
expect(query).toEqual(arrayOf100Things)
expect(consoleWarnSpy).toHaveBeenCalled()
consoleWarnSpy.mockReset()
consoleWarnSpy.mockRestore()
})
}) })
test('should sort by number netWorth', () => { describe('chaining everything together', () => {
let query = new Query(TestData) test('with a flat array', () => {
.sort('netWorth') let query = new Query(['bar', 'foo', 'foobar', 'foofoobar'])
.results .search('foo')
.sort()
.paginate(1, 2)
.results
expect(query[0].name).toBe('Howard Buckley') // Negative expect(query.length).toBe(2)
expect(query[1].name).toBe('Natalia Petty') // 0 expect(query[0]).toBe('foo')
expect(query[query.length - 1].name).toBe('Newman Mays') // Richest expect(query[1]).toBe('foobar')
}) })
test('should sort by string name', () => { test('with an array of objects', () => {
let query = new Query(TestData) let query = new Query(TestData)
.sort('name') .search(true, 'isActive')
.results .sortBy('name')
.paginate(1, 2)
expect(query[0].name).toBe('Dudley Conner') .results
})
expect(query.length).toBe(2)
test('should filter', () => { expect(query[0].name).toBe('Dudley Conner')
let isAgeOver33 = a => a.age > 33 expect(query[query.length - 1].name).toBe('Haynes Meadows')
})
let query = new Query(TestData)
.filter(isAgeOver33)
.results
expect(query[0].name).toBe('Howard Buckley')
})
test('should filter by key', () => {
let isNumGT33 = num => num > 33
let query = new Query(TestData)
.filterBy('age', isNumGT33)
.results
expect(query[0].name).toBe('Howard Buckley')
})
test('should chain everything together', () => {
let query = new Query(TestData)
.search('isActive', true)
.sort('name')
.paginate(1, 2)
.results
expect(query.length).toBe(2)
expect(query[0].name).toBe('Dudley Conner')
expect(query[query.length - 1].name).toBe('Haynes Meadows')
}) })

View file

@ -1,6 +1,6 @@
{ {
"name": "json-query-chain", "name": "json-query-chain",
"version": "1.1.2", "version": "1.1.3",
"description": "Chain queries onto POJOs to return precise results.", "description": "Chain queries onto POJOs to return precise results.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {