├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── benchmark.js ├── package.json ├── scripts ├── postpublish.sh └── prepublish.sh ├── src ├── index.d.ts └── index.js └── test └── sort.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "syntax-flow", 4 | "transform-flow-strip-types", 5 | "transform-decorators-legacy", 6 | ["transform-runtime", { 7 | "polyfill": false, 8 | "regenerator": true 9 | }], 10 | "babel-plugin-loop-optimizer" 11 | ], 12 | "presets": ["flow", "es2015", "stage-0", "stage-2"] 13 | } 14 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: c4d0MUGNLmDFuQP4YIitzY7aEroFjR9Jk 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.{html,js,json,md,scss,sh}] 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.sublime-project] 14 | indent_style = tab 15 | indent_size = 2 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [Makefile] 22 | indent_style=tab 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "rules": { 7 | "strict": 0, 8 | "no-underscore-dangle": 0, 9 | "space-unary-ops": 0, 10 | "arrow-parens": [2, "as-needed"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | yarn.lock 3 | yarn-error.log 4 | npm-debug.log 5 | dist/ 6 | node_modules/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | after_success: 5 | - npm run coveralls 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Reachify, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataLoader Sort 2 | Sort function for DataLoader to ensure the correct data is returned for the matching keys 3 | 4 | [![npm version](https://badge.fury.io/js/dataloader-sort.svg)](https://badge.fury.io/js/dataloader-sort) [![Build Status](https://travis-ci.org/reachifyio/dataloader-sort.svg?branch=master)](https://travis-ci.org/reachifyio/dataloader-sort) [![Coverage Status](https://coveralls.io/repos/github/reachifyio/dataloader-sort/badge.svg?branch=master)](https://coveralls.io/github/reachifyio/dataloader-sort?branch=master) 5 | 6 | ## Installation 7 | `npm i -S dataloader-sort` 8 | 9 | ## Notes 10 | * If no match is found it will return `null` for this key 11 | * Includes Flow types 12 | 13 | ## Usage 14 | ### Basic Usage 15 | ``` 16 | import sort from 'dataloader-sort'; 17 | 18 | const keys = [1, 2, 3]; 19 | const data = [ 20 | { id: 3, value: 'three' }, 21 | { id: 1, value: 'one' }, 22 | { id: 4, value: 'four' }, 23 | ]; 24 | 25 | const result = sort(keys, data); 26 | ``` 27 | 28 | ##### Output 29 | ``` 30 | [ 31 | { id: 1, value: 'one' }, 32 | null, 33 | { id: 3, value: 'three' }, 34 | ] 35 | ``` 36 | 37 | ### Custom Prop Usage 38 | ``` 39 | const keys = [1, 2, 3]; 40 | const data = [ 41 | { other: 3, value: 'three' }, 42 | { other: 1, value: 'one' }, 43 | { other: 2, value: 'two' }, 44 | ]; 45 | 46 | const result = sort(keys, data, 'other'); 47 | ``` 48 | 49 | ##### Output 50 | ``` 51 | [ 52 | { other: 1, value: 'one' }, 53 | { other: 2, value: 'two' }, 54 | { other: 3, value: 'three' }, 55 | ] 56 | ``` 57 | 58 | ### Object Keys Usage 59 | ``` 60 | const keys = [ 61 | { userId: 1, messageId: 3 }, 62 | { userId: 2, messageId: 4 }, 63 | { userId: 3, messageId: 9 }, 64 | { userId: 3, messageId: 7 }, 65 | { userId: 1, messageId: 2 }, 66 | ]; 67 | const data = [ 68 | { userId: 1, messageId: 2, value: 'yayy' }, 69 | { userId: 3, messageId: 7, value: 'ya' }, 70 | { userId: 1, messageId: 3, value: 'woot' }, 71 | { userId: 2, messageId: 4, value: 'blue' }, 72 | { userId: 3, messageId: 9, value: 'green' }, 73 | ]; 74 | 75 | const result = sort(keys, data); 76 | ``` 77 | 78 | ##### Output 79 | ``` 80 | [ 81 | { userId: 1, messageId: 3, value: 'woot' }, 82 | { userId: 2, messageId: 4, value: 'blue' }, 83 | { userId: 3, messageId: 9, value: 'green' }, 84 | { userId: 3, messageId: 7, value: 'ya' }, 85 | { userId: 1, messageId: 2, value: 'yayy' }, 86 | ] 87 | ``` 88 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | import sort from './src'; 2 | 3 | const keys = []; 4 | const keysNotUnique = []; 5 | const results = []; 6 | const resultsUnique = []; 7 | const resultsNotUnique = []; 8 | const keyCount = 100; 9 | const uniqueResultMap = new Array(keyCount); 10 | const iterations = 10000; 11 | let id; 12 | let element; 13 | let i; 14 | let ordered; 15 | 16 | function getRandomInt(min, max) { 17 | return Math.floor((Math.random() * ((max - min) + 1)) + min); 18 | } 19 | 20 | // generate keys unique keys and unique results. 21 | // use case number 1 22 | for (i = 0; i < keyCount; i++) { 23 | keys.push(i); 24 | results.push({ id: i }); 25 | } 26 | 27 | // use case number 2 and 3 28 | for (i = 0; i < keyCount; i++) { 29 | id = getRandomInt(0, 80); 30 | element = { id }; 31 | 32 | keysNotUnique.push(id); 33 | resultsNotUnique.push(element); 34 | 35 | if (uniqueResultMap[i]) continue; 36 | 37 | resultsUnique.push(element); 38 | uniqueResultMap[i] = true; 39 | } 40 | 41 | // order keys randomly 42 | keys.sort(k => Math.random() - 0.5); 43 | 44 | console.time('not-repeated'); 45 | for (let j = 0; j < iterations; j++) { 46 | try { 47 | ordered = sort(keys, results); 48 | } catch (err) { 49 | // console.log(err); 50 | } 51 | } 52 | console.timeEnd('not-repeated'); 53 | 54 | console.time('repeated-keys-repeated-values'); 55 | for (let j = 0; j < iterations; j++) { 56 | try { 57 | ordered = sort(keysNotUnique, resultsNotUnique); 58 | } catch (err) { 59 | // console.log(err); 60 | } 61 | } 62 | console.timeEnd('repeated-keys-repeated-values'); 63 | 64 | console.time('repeated-keys-unique-values'); 65 | for (let j = 0; j < iterations; j++) { 66 | try { 67 | ordered = sort(keysNotUnique, resultsUnique); 68 | } catch (err) { 69 | // console.log(err); 70 | } 71 | } 72 | console.timeEnd('repeated-keys-unique-values'); 73 | 74 | const withNulls = keysNotUnique.concat([ 75 | getRandomInt(90, 200), 76 | getRandomInt(90, 200), 77 | getRandomInt(90, 200), 78 | ]); 79 | withNulls.sort(() => Math.random() - 0.5); 80 | 81 | console.time('repeated-keys-unique-values-and-nulls'); 82 | 83 | for (let j = 0; j < iterations; j++) { 84 | try { 85 | ordered = sort(withNulls, resultsUnique); 86 | } catch (err) { 87 | // console.log(err); 88 | } 89 | } 90 | console.timeEnd('repeated-keys-unique-values-and-nulls'); 91 | 92 | console.time('empty-keys-with-values'); 93 | try { 94 | ordered = sort(null, results); 95 | } catch (err) { 96 | // console.log(err); 97 | } 98 | console.timeEnd('empty-keys-with-values'); 99 | 100 | console.time('with-keys-empty-values'); 101 | try { 102 | ordered = sort(keys, []); 103 | } catch (err) { 104 | // console.log(err); 105 | } 106 | console.timeEnd('with-keys-empty-values'); 107 | 108 | process.exit(0); 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dataloader-sort", 3 | "version": "0.0.5", 4 | "description": "DataLoader Sort", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "rm -rf dist && babel src --out-dir dist", 8 | "benchmark": "babel-node benchmark.js", 9 | "test": "mocha --compilers js:babel-core/register", 10 | "cover": "babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- 'test/*.test.js' --compilers js:babel-register", 11 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls && rm -rf coverage", 12 | "lint": "eslint ./src --ext .js", 13 | "lintfix": "npm run lint -- --fix", 14 | "prepublish": "sh scripts/prepublish.sh", 15 | "postpublish": "sh scripts/postpublish.sh" 16 | }, 17 | "files": [ 18 | "LICENSE", 19 | "README.md", 20 | "dist/" 21 | ], 22 | "typings": "dist/index.d.ts", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/reachifyio/dataloader-sort.git" 26 | }, 27 | "author": "", 28 | "license": "MIT", 29 | "homepage": "https://github.com/reachifyio/dataloader-sort#readme", 30 | "devDependencies": { 31 | "babel-cli": "^6.24.0", 32 | "babel-core": "^6.22.1", 33 | "babel-eslint": "^7.1.1", 34 | "babel-istanbul": "^0.12.2", 35 | "babel-loader": "^6.2.10", 36 | "babel-plugin-tcomb": "^0.3.25", 37 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 38 | "babel-plugin-transform-runtime": "^6.22.0", 39 | "babel-preset-babili": "0.0.10", 40 | "babel-preset-es2015": "^6.22.0", 41 | "babel-preset-flow": "^6.23.0", 42 | "babel-preset-stage-0": "^6.22.0", 43 | "babel-relay-plugin": "^0.10.0", 44 | "babel-relay-plugin-loader": "^0.10.0", 45 | "coveralls": "^2.13.0", 46 | "eslint": "^3.14.0", 47 | "eslint-config-airbnb": "^14.0.0", 48 | "eslint-plugin-import": "^2.2.0", 49 | "eslint-plugin-jsx-a11y": "^3.0.2", 50 | "eslint-plugin-react": "^6.9.0", 51 | "flow-copy-source": "^1.1.0", 52 | "istanbul": "^1.0.0-alpha", 53 | "mocha-lcov-reporter": "^1.3.0", 54 | "tcomb": "^3.2.20" 55 | }, 56 | "dependencies": { 57 | "babel-plugin-loop-optimizer": "^1.2.3", 58 | "babel-polyfill": "^6.20.0", 59 | "chai": "^3.5.0", 60 | "dataloader": "^1.2.0", 61 | "mocha": "^3.2.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /scripts/postpublish.sh: -------------------------------------------------------------------------------- 1 | rm -rf dist 2 | -------------------------------------------------------------------------------- /scripts/prepublish.sh: -------------------------------------------------------------------------------- 1 | rm -rf dist 2 | babel src --out-dir dist 3 | flow-copy-source src dist 4 | cp src/index.d.ts dist 5 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export = DataLoaderSort; 2 | 3 | declare function DataLoaderSort(keys: Array, data: T[], prop?: string): Array; 4 | 5 | declare namespace DataLoaderSort {} 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const getMapKey = (data : Object, keyObject : Object) : string => { 3 | const filteredData = {}; 4 | const keys = Object.keys(keyObject); 5 | keys.sort(); 6 | keys.forEach(key => (filteredData[key] = data[key])); 7 | return JSON.stringify(filteredData); 8 | }; 9 | 10 | const sort = ( 11 | keys: (number | string | Object)[], 12 | data: Object[], 13 | prop?: string = 'id', 14 | ) : (Object | null)[] => { 15 | if (!keys.length) return []; 16 | if (!data.length) return new Array(keys.length).fill(null); 17 | 18 | const map = []; 19 | 20 | // Map data with retrievable keys 21 | data.forEach(d => { 22 | const mapKey = (typeof keys[0] === 'object') ? getMapKey(d, keys[0]) : d[prop]; 23 | 24 | if (map[mapKey]) { 25 | throw new Error(`Multiple options in data matching key ${String(mapKey)}`); 26 | } 27 | 28 | map[mapKey] = d; 29 | }); 30 | 31 | 32 | return keys.map(key => { 33 | const mapKey = (typeof key === 'object') ? getMapKey(key, key) : key; 34 | return map[mapKey] || null; 35 | }); 36 | }; 37 | 38 | export default sort; 39 | -------------------------------------------------------------------------------- /test/sort.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import sort from '../src'; 5 | 6 | describe('dataloaderHelpers', () => { 7 | describe('sort', () => { 8 | it('should sort the data based on id field by default using number keys', done => { 9 | const keys = [1, 2, 3]; 10 | const data = [ 11 | { id: 3, value: 'three' }, 12 | { id: 1, value: 'one' }, 13 | { id: 2, value: 'two' }, 14 | ]; 15 | const sortedData = [ 16 | { id: 1, value: 'one' }, 17 | { id: 2, value: 'two' }, 18 | { id: 3, value: 'three' }, 19 | ]; 20 | 21 | const result = sort(keys, data); 22 | 23 | expect(sortedData).to.deep.equal(result); 24 | done(); 25 | }); 26 | 27 | it('should be able to handle keys which are strings', done => { 28 | const keys = ['1', '2', '3']; 29 | const data = [ 30 | { id: '3', value: 'three' }, 31 | { id: '1', value: 'one' }, 32 | { id: '2', value: 'two' }, 33 | ]; 34 | const sortedData = [ 35 | { id: '1', value: 'one' }, 36 | { id: '2', value: 'two' }, 37 | { id: '3', value: 'three' }, 38 | ]; 39 | 40 | const result = sort(keys, data); 41 | 42 | expect(sortedData).to.deep.equal(result); 43 | done(); 44 | }); 45 | 46 | it('should be able to handle repeated keys', done => { 47 | const keys = [1, 1, 2, 3]; 48 | const data = [ 49 | { id: 3, value: 'three' }, 50 | { id: 1, value: 'one' }, 51 | { id: 2, value: 'two' }, 52 | ]; 53 | const sortedData = [ 54 | { id: 1, value: 'one' }, 55 | { id: 1, value: 'one' }, 56 | { id: 2, value: 'two' }, 57 | { id: 3, value: 'three' }, 58 | ]; 59 | 60 | const result = sort(keys, data); 61 | 62 | expect(sortedData).to.deep.equal(result); 63 | done(); 64 | }); 65 | 66 | it('should sort the data based on the the provided prop field', done => { 67 | const keys = [1, 2, 3]; 68 | const data = [ 69 | { other: 3, value: 'three' }, 70 | { other: 1, value: 'one' }, 71 | { other: 2, value: 'two' }, 72 | ]; 73 | const sortedData = [ 74 | { other: 1, value: 'one' }, 75 | { other: 2, value: 'two' }, 76 | { other: 3, value: 'three' }, 77 | ]; 78 | 79 | const result = sort(keys, data, 'other'); 80 | 81 | expect(sortedData).to.deep.equal(result); 82 | done(); 83 | }); 84 | 85 | it('should sort and match based on the fields in the keys, if key is an object', done => { 86 | const keys = [ 87 | { userId: 1, messageId: 3 }, 88 | { userId: 2, messageId: 4 }, 89 | { userId: 3, messageId: 9 }, 90 | { userId: 3, messageId: 7 }, 91 | { userId: 1, messageId: 2 }, 92 | ]; 93 | 94 | const data = [ 95 | { userId: 1, messageId: 2, value: 'yayy' }, 96 | { userId: 3, messageId: 7, value: 'ya' }, 97 | { userId: 1, messageId: 3, value: 'woot' }, 98 | { userId: 2, messageId: 4, value: 'blue' }, 99 | { userId: 3, messageId: 9, value: 'green' }, 100 | ]; 101 | 102 | const sortedData = [ 103 | { userId: 1, messageId: 3, value: 'woot' }, 104 | { userId: 2, messageId: 4, value: 'blue' }, 105 | { userId: 3, messageId: 9, value: 'green' }, 106 | { userId: 3, messageId: 7, value: 'ya' }, 107 | { userId: 1, messageId: 2, value: 'yayy' }, 108 | ]; 109 | 110 | const result = sort(keys, data); 111 | 112 | expect(sortedData).to.deep.equal(result); 113 | done(); 114 | }); 115 | 116 | it('should return null for any keys which don\'t have matching data', done => { 117 | const keys = [1, 2, 3]; 118 | const data = [ 119 | { id: 3, value: 'three' }, 120 | { id: 1, value: 'one' }, 121 | ]; 122 | const sortedData = [ 123 | { id: 1, value: 'one' }, 124 | null, 125 | { id: 3, value: 'three' }, 126 | ]; 127 | 128 | const result = sort(keys, data); 129 | 130 | expect(sortedData).to.deep.equal(result); 131 | done(); 132 | }); 133 | 134 | it('should return null for all if data is empty', done => { 135 | const keys = [1, 2, 3]; 136 | const data = []; 137 | const sortedData = [ 138 | null, 139 | null, 140 | null, 141 | ]; 142 | 143 | const result = sort(keys, data); 144 | 145 | expect(sortedData).to.deep.equal(result); 146 | done(); 147 | }); 148 | 149 | it('should return empty array if keys is empty', done => { 150 | const keys = []; 151 | const data = [{ id: 1 }]; 152 | const sortedData = []; 153 | 154 | const result = sort(keys, data); 155 | 156 | expect(sortedData).to.deep.equal(result); 157 | done(); 158 | }); 159 | 160 | it('should throw an error if multiple results match a single key', done => { 161 | const keys = [1, 2, 3]; 162 | const data = [ 163 | { id: 1, value: 'three' }, 164 | { id: 1, value: 'one' }, 165 | { id: 2, value: 'two' }, 166 | ]; 167 | 168 | expect(() => sort(keys, data)).to.throw(Error, /Multiple options in data matching key 1/); 169 | done(); 170 | }); 171 | 172 | // it('should throw an error if passed another type besides Array for keys', done => { 173 | // const keys = 'test'; 174 | // const data = []; 175 | // 176 | // expect(() => sort(keys, data)).to.throw(Error, /Invalid value "test"/); 177 | // done(); 178 | // }); 179 | // 180 | // it('should throw an error if passed another type besides Array for data', done => { 181 | // const keys = [1, 2]; 182 | // const data = false; 183 | // 184 | // expect(() => sort(keys, data)).to.throw(Error, /Invalid value false/); 185 | // done(); 186 | // }); 187 | }); 188 | }); 189 | --------------------------------------------------------------------------------