diff --git a/README.md b/README.md index 82c5da3..d837168 100644 --- a/README.md +++ b/README.md @@ -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) -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 ```javascript import Query from 'json-query-chain'; -let myQ = new Query(someJsonData) +let query = new Query(someJsonData) .search('isActiveUser', true) .results; + +console.log('Results: ', query); ``` ### Chainable Methods #### Search -Currently supports booleans and strings. (See [#1](https://github.com/sharpshark28/json-query-chain/issues/1) for Integer Support) - ##### By Boolean +Acts as a smart filter returning only elements who's key matches the expected result. + ```javascript -.search('isActiveUser', true) +.search(true, 'isActiveUser') ``` ##### 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 -.search('name', 'steele') +.search('steele', 'name') ``` #### Filter @@ -42,28 +46,42 @@ Simpler version of search using a custom function in the chain. ##### By Key +Like `.filter()`, but narrowed down by key. + ```javascript .filterBy('age', x => x >= 21) ``` #### Sort +A chainable version of javascript's built in array sort. + +``` +.sort((a, b) => a > b) +``` + ##### By Boolean +Abstracted sort by matching a key to a boolean. + ```javascript -.sort('isActiveUser', true) +.sortBy('isActiveUser') ``` ##### By String +Sorts alphabetically based on key. + ```javascript -.sort('name') +.sortBy('name') ``` ##### By Number +Sorts by ascending numerical order based on key. + ```javascript -.sort('netWorth') +.sortBy('netWorth') ``` #### Pagination @@ -74,7 +92,7 @@ Page 1 with 5 results per page. .paginate(1, 5) ``` -Page 2 wtih default of 10 results per page. +Page 2 with default of 10 results per page. ```javascript .paginate(2) diff --git a/index.js b/index.js index a7c32d9..ef6fad7 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,6 @@ module.exports = class Query { constructor (data) { - this.data = data.map(item => { - item.sortScore = 0 - return item - }) + this.data = data } get results () { @@ -16,33 +13,47 @@ module.exports = class Query { } 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 } - search (key, term, score = 0) { + search (term, key) { switch (typeof term) { 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 case 'string': - this.data = this.data.filter(item => { - let regFind = new RegExp(term, 'gi') - let termMatches = (item[key].match(regFind) || []).length - item.sortScore += termMatches - return termMatches - }) + let regFind = new RegExp(term, 'gi') + let getMatches = item => { + if (typeof item === 'string') { + return (item.match(regFind) || []).length + } else { + return (item[key].match(regFind) || []).length + } + } + this.data = [...this.data].sort(getMatches).filter(getMatches) break } return this } - sort (key = 'sortScore') { - this.data = this.data.sort((a, b) => { - if (a[key] < b[key]) return -1 - if (a[key] > b[key]) return 1 - return 0 - }) + sort (func) { + this.data = [...this.data].sort(func) + return this + } + + 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 } diff --git a/index.test.js b/index.test.js index 38f4bd9..422092e 100644 --- a/index.test.js +++ b/index.test.js @@ -1,6 +1,8 @@ const Query = require('./index') const TestData = require('./testdata.json') +const arrayOf100Things = [...Array(100).keys()] + test('should not modify passed data without chain alterations', () => { let query = new Query(TestData) .results @@ -8,102 +10,233 @@ test('should not modify passed data without chain alterations', () => { expect(query).toMatchObject(TestData) }) -test('should paginate with default params', () => { - let query = new Query(TestData) - .paginate() - .results +describe('.paginate()', () => { + test('with a flat array', () => { + let query = new Query(arrayOf100Things) + .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', () => { - let query = new Query(TestData) - .paginate(1, 3) - .results +describe('.search()', () => { + test('partial string in flat array of strings', () => { + let query = new Query(['bar', 'foo', 'foobar', 'foofoobar']) + .search('foo') + .results - expect(query.length).toBe(3) - expect(query[0].name).toBe('Haynes Meadows') + expect(query[0]).toBe('foofoobar') + 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', () => { - let query = new Query(TestData) - .paginate(2, 3) - .results +describe('.sort()', () => { + test('with flat array', () => { + let alphabetical = (a, b) => a > b - expect(query.length).toBe(3) - expect(query[0].name).toBe('Howard Buckley') + let query = new Query(['foo', 'bar', 'foobar']) + .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', () => { - let query = new Query(TestData) - .search('isActive', true) - .results +describe('.sortBy()', () => { + test('by boolean', () => { + let query = new Query(TestData) + .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', () => { - let query = new Query(TestData) - .search('name', 'steele') - .results +describe('.filter()', () => { + test('with a flat array', () => { + let isEven = a => !(a % 2) - 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', () => { - let query = new Query(TestData) - .sort('isActive') - .results +describe('.filterBy()', () => { + test('by key', () => { + let isNumGT33 = num => num > 33 - 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', () => { - let query = new Query(TestData) - .sort('netWorth') - .results +describe('chaining everything together', () => { + test('with a flat array', () => { + let query = new Query(['bar', 'foo', 'foobar', 'foofoobar']) + .search('foo') + .sort() + .paginate(1, 2) + .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('should sort by string name', () => { - let query = new Query(TestData) - .sort('name') - .results - - expect(query[0].name).toBe('Dudley Conner') -}) - -test('should filter', () => { - let isAgeOver33 = a => a.age > 33 - - 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') + expect(query.length).toBe(2) + expect(query[0]).toBe('foo') + expect(query[1]).toBe('foobar') + }) + + test('with an array of objects', () => { + let query = new Query(TestData) + .search(true, 'isActive') + .sortBy('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') + }) }) diff --git a/package.json b/package.json index 0b3f68f..1eaf40c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-query-chain", - "version": "1.1.2", + "version": "1.1.3", "description": "Chain queries onto POJOs to return precise results.", "main": "index.js", "scripts": {