├── .gitignore ├── package.json ├── README.md ├── src └── JsonApiQueryParser.js └── test └── JsonApiQueryParser.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonapi-query-parser", 3 | "version": "1.3.1", 4 | "description": "Class to parse endpoint and its query parameters to a usable request object", 5 | "main": "src/JsonApiQueryParser.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/kideh88/node-jsonapi-query-parser.git" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "nodejs", 16 | "jsonapi", 17 | "query", 18 | "string", 19 | "parser", 20 | "parameters", 21 | "converter", 22 | "request", 23 | "parsing", 24 | "ES6" 25 | ], 26 | "author": "Kim Dehmlow", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/kideh88/node-jsonapi-query-parser/issues" 30 | }, 31 | "homepage": "https://github.com/kideh88/node-jsonapi-query-parser#readme", 32 | "devDependencies": { 33 | "chai": "^3.4.1", 34 | "mocha": "^2.3.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonapi-query-parser 2 | 3 | JsonApiQueryParser class to parse endpoint and its query string parameters to a usable request object. 4 | 5 | To be used for node projects that make use of [JSON API](http://jsonapi.org/) 6 | 7 | 8 | ## Installation 9 | 10 | ```sh 11 | $ npm install jsonapi-query-parser 12 | ``` 13 | 14 | ## Usage 15 | 16 | Require the module 'JsonApiQueryParser' into your application and use the 'parseRequest' function to convert the request.url to an easy 17 | usable requestData object. 18 | 19 | ```js 20 | let JsonApiQueryParserClass = require('jsonapi-query-parser'); 21 | let JsonApiQueryParser = new JsonApiQueryParserClass(); 22 | 23 | http.createServer(function (request, response) { 24 | let requestData = JsonApiQueryParser.parseRequest(request.url); 25 | 26 | // .. Do stuff with your requestData object 27 | 28 | }).listen(1337, '127.0.0.1'); 29 | ``` 30 | 31 | ## Return data information (requestData) 32 | 33 | The object returned by the JsonApiQueryParser.parseRequest will always be the same structure. 34 | Please note that the query parameters are decoded when parsed! 35 | Below you can see 2 parsed examples: 36 | 37 | ```js 38 | //EXAMPLE 1 39 | let url = '/article/5/relationships/comment' 40 | let requestData = { 41 | resourceType: 'article', 42 | identifier: '5', 43 | relationships: true, 44 | relationshipType: 'comment', 45 | queryData: { 46 | include: [], 47 | fields: {}, 48 | sort: [], 49 | page: {}, 50 | filter: { 51 | like: {}, 52 | not: {}, 53 | lt: {}, 54 | lte: {}, 55 | gt: {}, 56 | gte: {} 57 | } 58 | } 59 | }; 60 | 61 | //EXAMPLE 2 62 | let url = '/article/5/?include=user,comment.user&fields[article]=title%2Cbody&page[limit]=20&sort=-createdon' 63 | let requestData = { 64 | resourceType: 'article', 65 | identifier: '5', 66 | relationships: false, 67 | relationshipType: null, 68 | queryData: { 69 | include: ['user', 'comment.user'], 70 | fields: { 71 | article: ['title', 'body'] 72 | }, 73 | sort: ['-createdon'], 74 | page: { 75 | limit: 20 76 | }, 77 | filter: { 78 | like: {}, 79 | not: {}, 80 | lt: {}, 81 | lte: {}, 82 | gt: {}, 83 | gte: {} 84 | } 85 | } 86 | }; 87 | ``` 88 | 89 | 90 | ## Important 91 | 92 | If your endpoints contain versioning or other application specific pointers please remove them before parsing! 93 | Here are some examples of a request url: 94 | 95 | ```js 96 | let CORRECT1 = '/article/'; 97 | let CORRECT2 = '/article/5?include=comments'; 98 | let CORRECT3 = '/article/5/relationships/comments'; 99 | let CORRECT4 = '/article/?include=user,comment.rating&fields[article]=title,body&fields[user]=name'; 100 | 101 | // Contains '/v1/api' which cannot be parsed properly 102 | let INVALID = '/v1/api/article?include=user'; 103 | ``` 104 | 105 | ## Custom 'filter' implementation! 106 | 107 | Filters might not be properly parsed since there are no specifications for this query yet! I hope to update this package 108 | as soon as filtering specs are available. 109 | For now the filters are handled like the fields parameter. 110 | 111 | FilterType is also not supported by JSON API spec. This feature can be used to filter on partial matches, less than, greater than, and more. 112 | The feature is currently implemented for [bookshelf-jsonapi-params](https://github.com/scoutforpets/bookshelf-jsonapi-params). 113 | 114 | ```js 115 | //EXAMPLE 1 116 | let url = '/article/5?filter[name]=john%20doe&&filter[age][lt]=15' 117 | let requestData = { 118 | resourceType: 'article', 119 | identifier: '5', 120 | relationships: false, 121 | relationshipType: null, 122 | queryData: { 123 | include: [], 124 | fields: {}, 125 | sort: [], 126 | page: {}, 127 | filter: { 128 | name: 'john doe', 129 | age: '15', 130 | like: {}, 131 | not: {}, 132 | lt: { 133 | age: '15' 134 | }, 135 | lte: {}, 136 | gt: {}, 137 | gte: {} 138 | } 139 | } 140 | }; 141 | 142 | // alechirsh filter type implementation: 143 | let url = '/article/5?filter[not][name]=jack' 144 | let requestData = { 145 | resourceType: 'article', 146 | identifier: '5', 147 | relationships: false, 148 | relationshipType: null, 149 | queryData: { 150 | include: [], 151 | fields: {}, 152 | sort: [], 153 | page: {}, 154 | filter: { 155 | like: {}, 156 | not: { 157 | name: 'jack' 158 | }, 159 | lt: {}, 160 | lte: {}, 161 | gt: {}, 162 | gte: {} 163 | } 164 | } 165 | }; 166 | 167 | ``` 168 | 169 | ## Tests! 170 | 171 | Tests running using mocha & chai in /test 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/JsonApiQueryParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * [Defines the available parse function names and their matching patterns.] 5 | **/ 6 | let PARSE_PARAM = Object.freeze({ 7 | parseInclude: /^include\=(.*?)/i, 8 | parseFields: /^fields\[(.*?)\]\=.*?$/i, 9 | parsePage: /^page\[(.*?)\]\=.*?$/i, 10 | parseSort: /^sort\=(.*?)/i, 11 | parseFilter: /^filter\[([^\]]*?)\]\=.*?$/i, 12 | parseFilterType: /^filter\[(.*?)\]\[(.*?)\]\=(.*?)$/i 13 | }); 14 | 15 | 16 | class JsonApiQueryParser { 17 | 18 | /** 19 | * [Defines the requestData object to modify via given queryString. NOTE: filter query is not implemented due to lack of specs.] 20 | * 21 | * @param {[string]} url [Required url containing the endpoint path and query string.] 22 | * @return {[object]} requestData [Parsed request information as object.] 23 | * 24 | **/ 25 | parseRequest (url) { 26 | let requestData = { 27 | resourceType: null, 28 | identifier: null, 29 | relationships: false, 30 | relationshipType: null, 31 | queryData: { 32 | include: [], 33 | fields: {}, 34 | sort: [], 35 | page: {}, 36 | filter: { 37 | like: {}, 38 | not: {}, 39 | lt: {}, 40 | lte: {}, 41 | gt: {}, 42 | gte: {} 43 | } 44 | } 45 | }; 46 | 47 | let urlSplit = url.split('?'); 48 | requestData = this.parseEndpoint(urlSplit[0], requestData); 49 | 50 | if(urlSplit[1]) { 51 | requestData.queryData = this.parseQueryParameters(urlSplit[1], requestData.queryData); 52 | } 53 | 54 | return requestData; 55 | } 56 | 57 | /** 58 | * [Cuts up the endpoint path to define the requested resource, identifier and relationships.] 59 | * 60 | * @param {[string]} endpointString [Required endpoint string. Example: "articles/6/comments".] 61 | * @param {[object]} requestObject [Required reference to the main requestData object.] 62 | * @return {[object]} requestData [Parsed request information as object.] 63 | * 64 | **/ 65 | parseEndpoint (endpointString, requestObject) { 66 | let requestSplit = JsonApiQueryParser.trimSlashes(endpointString).split('/'); 67 | 68 | requestObject.resourceType = requestSplit[0]; 69 | requestObject.identifier = (requestSplit.length >= 2 ? requestSplit[1] : null); 70 | requestObject.relationships = (requestSplit.length >= 3 && requestSplit[2].toLowerCase() === 'relationships'); 71 | 72 | if(requestObject.relationships) { 73 | if(!requestSplit[3]) { 74 | throw new ReferenceError('Request missing relationship type', 'JsonApiQueryParser.js'); 75 | } else { 76 | requestObject.relationshipType = requestSplit[3] 77 | } 78 | } else { 79 | requestObject.relationshipType = (requestSplit.length === 3 ? requestSplit[2] : null); 80 | } 81 | 82 | return requestObject; 83 | } 84 | 85 | /** 86 | * [Cuts up the query parameters and sends each piece to the delegate function.] 87 | * 88 | * @param {[string]} queryString [Required query string. Example: "?include=comments,user&fields[article]=title,body" ] 89 | * @param {[object]} requestDataSubset [Required reference to the main requestData object.] 90 | * @return {[object]} requestData [Parsed request information as object.] 91 | * 92 | **/ 93 | parseQueryParameters (queryString, requestDataSubset) { 94 | let querySplit = queryString.split('&'); 95 | querySplit = querySplit.map(function(queryPart){ 96 | return decodeURIComponent(queryPart); 97 | }); 98 | querySplit.forEach(this.delegateToParser, requestDataSubset); 99 | 100 | return requestDataSubset; 101 | } 102 | 103 | /** 104 | * [Delegates each query string piece to its own parser function.] 105 | * 106 | * @param {[string]} query [Required query string piece. Example: "fields[article]=title,body".] 107 | * @return {[object]} requestData [Parsed request information as object.] 108 | * 109 | **/ 110 | delegateToParser (query) { 111 | // NOTE: 'this' points to requestObject! 112 | let _requestDataSubset = this; 113 | let functionName; 114 | 115 | for(functionName in PARSE_PARAM) { 116 | if(PARSE_PARAM[functionName].test(query)) { 117 | _requestDataSubset = JsonApiQueryParser[functionName](query, _requestDataSubset); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * [Parses the include query string piece and returns the modified _requestDataSubset.] 124 | * 125 | * @param {[string]} includeString [Required include string piece. Example: "include=comments,user".] 126 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 127 | * @return {[object]} requestDataSubset [Returning the modified request data.] 128 | * 129 | **/ 130 | static parseInclude (includeString, requestDataSubset) { 131 | // Kept simple for now, does not parse dot-separated relationships (comment.user) 132 | let targetString = includeString.split('=')[1]; 133 | requestDataSubset.include = targetString.split(','); 134 | 135 | return requestDataSubset; 136 | } 137 | 138 | /** 139 | * [Parses the fields query string piece and returns the modified _requestDataSubset.] 140 | * 141 | * @param {[string]} fieldsString [Required fields query string piece. Example: "fields[article]=title,body".] 142 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 143 | * @return {[object]} requestDataSubset [Returning the modified request data.] 144 | * 145 | **/ 146 | static parseFields (fieldsString, requestDataSubset) { 147 | let targetResource; 148 | let targetFields; 149 | let targetFieldsString; 150 | let fieldNameRegex = /^fields.*?\=(.*?)$/i; 151 | 152 | targetResource = fieldsString.replace(PARSE_PARAM.parseFields, function(match, $1, $2, $3) { 153 | return $1; 154 | }); 155 | 156 | targetFieldsString = fieldsString.replace(fieldNameRegex, function(match, $1, $2, $3) { 157 | return $1; 158 | }); 159 | 160 | requestDataSubset.fields[targetResource] = (!requestDataSubset.fields[targetResource] ? [] : requestDataSubset.fields[targetResource]); 161 | targetFields = targetFieldsString.split(','); 162 | 163 | targetFields.forEach(function(targetField) { 164 | requestDataSubset.fields[targetResource].push(targetField); 165 | }); 166 | 167 | return requestDataSubset; 168 | } 169 | 170 | /** 171 | * [Parses the page query string piece and returns the modified _requestDataSubset.] 172 | * 173 | * @param {[string]} pageString [Required page query string piece. Example: "page[offset]=20".] 174 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 175 | * @return {[object]} requestDataSubset [Returning the modified request data.] 176 | * 177 | **/ 178 | static parsePage (pageString, requestDataSubset) { 179 | let pageSettingKey; 180 | let pageSettingValue; 181 | let pageValueRegex = /^page.*?\=(.*?)$/i; 182 | 183 | pageSettingKey = pageString.replace(PARSE_PARAM.parsePage, function(match, $1, $2, $3) { 184 | return $1; 185 | }); 186 | 187 | pageSettingValue = pageString.replace(pageValueRegex, function(match, $1, $2, $3) { 188 | return $1; 189 | }); 190 | 191 | requestDataSubset.page[pageSettingKey] = pageSettingValue; 192 | 193 | return requestDataSubset; 194 | } 195 | 196 | /** 197 | * [Parses the sort query string piece and returns the modified _requestDataSubset.] 198 | * 199 | * @param {[string]} sortString [Required sort query string piece. Example: "sort=-created,title".] 200 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 201 | * @return {[object]} requestDataSubset [Returning the modified request data.] 202 | * 203 | **/ 204 | static parseSort (sortString, requestDataSubset) { 205 | let targetString = sortString.split('=')[1]; 206 | requestDataSubset.sort = targetString.split(','); 207 | 208 | return requestDataSubset; 209 | } 210 | 211 | /** 212 | * [Note: The are no proper specifications for this parameter yet. 213 | * For now the filter is implemented similar to the fields parameter. Values should be url encoded to allow for special characters.] 214 | * 215 | * @param {[string]} filterString [Required sort query string piece. Example: "filter[name]=John%20Doe".] 216 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 217 | * @return {[object]} requestDataSubset [Returning the modified request data.] 218 | * 219 | **/ 220 | static parseFilter (filterString, requestDataSubset) { 221 | let targetColumn; 222 | let targetFilterString; 223 | let filterNameRegex = /^filter.*?\=(.*?)$/i; 224 | 225 | targetColumn = filterString.replace(PARSE_PARAM.parseFilter, function(match, $1, $2, $3) { 226 | return $1; 227 | }); 228 | 229 | targetFilterString = filterString.replace(filterNameRegex, function(match, $1, $2, $3) { 230 | return $1; 231 | }); 232 | 233 | requestDataSubset.filter[targetColumn] = targetFilterString; 234 | 235 | return requestDataSubset; 236 | } 237 | 238 | /** 239 | * [Note: The are no proper specifications for this parameter yet. 240 | * For now the filter is implemented similar to the fields parameter. Values should be url encoded to allow for special characters.] 241 | * 242 | * @param {[string]} filterString [Required sort query string piece. Example: "filter[name][like]=John%20Doe".] 243 | * @param {[object]} requestDataSubset [Required reference to the requestData.queryData object.] 244 | * @return {[object]} requestDataSubset [Returning the modified request data.] 245 | * 246 | **/ 247 | static parseFilterType (filterString, requestDataSubset) { 248 | let targetType; 249 | let targetColumn; 250 | let targetFilterString; 251 | 252 | targetType = filterString.replace(PARSE_PARAM.parseFilterType, function(match, $1) { 253 | return $1; 254 | }); 255 | 256 | targetColumn = filterString.replace(PARSE_PARAM.parseFilterType, function(match, $1, $2) { 257 | return $2; 258 | }); 259 | 260 | targetFilterString = filterString.replace(PARSE_PARAM.parseFilterType, function(match, $1, $2, $3) { 261 | return $3; 262 | }); 263 | 264 | if(requestDataSubset.filter[targetType]){ 265 | requestDataSubset.filter[targetType][targetColumn] = targetFilterString; 266 | } 267 | 268 | return requestDataSubset; 269 | } 270 | 271 | /** 272 | * [Slash trim to avoid faulty endpoint mapping. Runs recursively to remove any double slash errors] 273 | * 274 | * @param {[string]} input [Required input to be trimmed. Example: "/article/1/".] 275 | * @return {[string]} [Returning the modified string.] 276 | * 277 | **/ 278 | static trimSlashes (input) { 279 | let slashPattern = /(^\/)|(\/$)/; 280 | let trimmed = input.replace(slashPattern, ""); 281 | if(slashPattern.test(trimmed)) { 282 | return JsonApiQueryParser.trimSlashes(trimmed); 283 | } else { 284 | return trimmed; 285 | } 286 | }; 287 | 288 | } 289 | 290 | module.exports = JsonApiQueryParser; 291 | -------------------------------------------------------------------------------- /test/JsonApiQueryParser.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var JsonApiQueryParser = require('../src/JsonApiQueryParser'); 4 | var chai = require('chai'); 5 | var should = chai.should(); 6 | var expect = chai.expect; 7 | 8 | describe('JsonApiQueryParser', function () { 9 | 10 | var requestData; 11 | var requestDataSubset; 12 | 13 | beforeEach(function () { 14 | requestData = { 15 | resourceType: null, 16 | identifier: null, 17 | relationships: false, 18 | relationshipType: null, 19 | queryData: { 20 | include: [], 21 | fields: {}, 22 | sort: [], 23 | page: {}, 24 | filter: { 25 | like: {}, 26 | not: {}, 27 | lt: {}, 28 | lte: {}, 29 | gt: {}, 30 | gte: {} 31 | } 32 | } 33 | }; 34 | 35 | requestDataSubset = { 36 | include: [], 37 | fields: {}, 38 | sort: [], 39 | page: {}, 40 | filter: { 41 | like: {}, 42 | not: {}, 43 | lt: {}, 44 | lte: {}, 45 | gt: {}, 46 | gte: {} 47 | } 48 | }; 49 | }); 50 | 51 | afterEach(function () { 52 | requestData = null; 53 | requestDataSubset = null; 54 | }); 55 | 56 | 57 | describe('parseRequest function', function() { 58 | it('should parse the whole given request.url by splitting it and calling parseEndpoint and parseQueryParameters', function() { 59 | var testString, testData, expectedData; 60 | var parserClass = new JsonApiQueryParser(); 61 | 62 | testString = '//article/5/relationships/comment?include=user,testComment&sort=Age%2CfirstName&&fields[user]=name,email&page[limit]=20' 63 | + '&filter[name]=john%20doe&filter[age]=15&filter[like][name]=john,joe&filter[not][age]=30&filter[gt][age]=17'; 64 | testData = parserClass.parseRequest(testString, requestData); 65 | 66 | expectedData = { 67 | resourceType: 'article', 68 | identifier: '5', 69 | relationships: true, 70 | relationshipType: 'comment', 71 | queryData: { 72 | include: ['user', 'testComment'], 73 | sort: ['Age', 'firstName'], 74 | fields: { 75 | user: ['name', 'email'] 76 | }, 77 | page: { 78 | limit: '20' 79 | }, 80 | filter: { 81 | name: 'john doe', 82 | age: '15', 83 | like: { 84 | name: 'john,joe' 85 | }, 86 | not: { 87 | age: '30' 88 | }, 89 | lt: {}, 90 | lte: {}, 91 | gt: { 92 | age: '17' 93 | }, 94 | gte: {} 95 | } 96 | } 97 | }; 98 | 99 | expect(testData).to.deep.equal(expectedData); 100 | }); 101 | }); 102 | 103 | describe('parseEndpoint function', function() { 104 | it('should parse the correct splits to each requestData definition.', function() { 105 | var testString, testData, expectedData; 106 | var parserClass = new JsonApiQueryParser(); 107 | 108 | testString = 'article/5/relationships/comment'; 109 | testData = parserClass.parseEndpoint(testString, requestData); 110 | 111 | expectedData = { 112 | resourceType: 'article', 113 | identifier: '5', 114 | relationships: true, 115 | relationshipType: 'comment', 116 | queryData: { 117 | include: [], 118 | fields: {}, 119 | sort: [], 120 | page: {}, 121 | filter: { 122 | like: {}, 123 | not: {}, 124 | lt: {}, 125 | lte: {}, 126 | gt: {}, 127 | gte: {} 128 | } 129 | } 130 | }; 131 | 132 | expect(testData).to.deep.equal(expectedData); 133 | 134 | testString = '//article/5/comment//'; 135 | testData = parserClass.parseEndpoint(testString, requestData); 136 | 137 | expectedData = { 138 | resourceType: 'article', 139 | identifier: '5', 140 | relationships: false, 141 | relationshipType: 'comment', 142 | queryData: { 143 | include: [], 144 | fields: {}, 145 | sort: [], 146 | page: {}, 147 | filter: { 148 | like: {}, 149 | not: {}, 150 | lt: {}, 151 | lte: {}, 152 | gt: {}, 153 | gte: {} 154 | } 155 | } 156 | }; 157 | 158 | expect(testData).to.deep.equal(expectedData); 159 | 160 | }); 161 | 162 | it('should throw a ReferenceError if the request asked for /relationships but did not define the name of it.', function() { 163 | var testString; 164 | var parserClass = new JsonApiQueryParser(); 165 | 166 | testString = 'article/5/relationships/'; 167 | var testFunction = function() { 168 | parserClass.parseEndpoint(testString, requestData); 169 | }; 170 | 171 | expect(testFunction).to.throw(ReferenceError); 172 | 173 | }); 174 | }); 175 | 176 | 177 | describe('parseQueryParameters/delegateToParser function', function() { 178 | it('should split the query into pieces and delegate them to their matching parser function.', function() { 179 | 180 | var testString, testData, expectedData; 181 | var parserClass = new JsonApiQueryParser(); 182 | 183 | testString = 'include=user,comment&sort=age&fields[user]=name,email&page[limit]=20&filter[name]=test'; 184 | testData = parserClass.parseQueryParameters(testString, requestDataSubset); 185 | 186 | expectedData = { 187 | include: ['user', 'comment'], 188 | fields: { 189 | user: ['name', 'email'] 190 | }, 191 | sort: ['age'], 192 | page: { 193 | limit: '20' 194 | }, 195 | filter: { 196 | name: 'test', 197 | like: {}, 198 | not: {}, 199 | lt: {}, 200 | lte: {}, 201 | gt: {}, 202 | gte: {} 203 | } 204 | }; 205 | 206 | expect(testData).to.deep.equal(expectedData); 207 | 208 | requestDataSubset = { 209 | include: [], 210 | fields: {}, 211 | sort: [], 212 | page: {}, 213 | filter: { 214 | like: {}, 215 | not: {}, 216 | lt: {}, 217 | lte: {}, 218 | gt: {}, 219 | gte: {} 220 | } 221 | }; 222 | 223 | testString = '&&include=user&page[offset]=200&sort=age,-id&fields[user]=name,email&&fields[article]=title,body&page[limit]=20' 224 | + '&filter[name]=test&filter[lastname]=another&filter[like][name]=boo'; 225 | testData = parserClass.parseQueryParameters(testString, requestDataSubset); 226 | 227 | expectedData = { 228 | include: ['user'], 229 | fields: { 230 | user: ['name', 'email'], 231 | article: ['title', 'body'] 232 | }, 233 | sort: ['age', '-id'], 234 | page: { 235 | offset: '200', 236 | limit: '20' 237 | }, 238 | filter: { 239 | name: 'test', 240 | lastname: 'another', 241 | like: { 242 | name: 'boo' 243 | }, 244 | not: {}, 245 | lt: {}, 246 | lte: {}, 247 | gt: {}, 248 | gte: {} 249 | } 250 | }; 251 | 252 | expect(testData).to.deep.equal(expectedData); 253 | }); 254 | }); 255 | 256 | describe('parseInclude function', function() { 257 | it('should push the values of the include string to the queryData include array.', function() { 258 | let includeString = 'include=user,comment.user'; 259 | 260 | let testData = JsonApiQueryParser.parseInclude(includeString, requestDataSubset); 261 | let expectedData = { 262 | include: ['user', 'comment.user'], 263 | fields: {}, 264 | sort: [], 265 | page: {}, 266 | filter: { 267 | like: {}, 268 | not: {}, 269 | lt: {}, 270 | lte: {}, 271 | gt: {}, 272 | gte: {} 273 | } 274 | }; 275 | 276 | expect(testData).to.deep.equal(expectedData); 277 | }); 278 | }); 279 | 280 | describe('parseFields function', function() { 281 | it('should push the values of the fields strings to their matching queryData field objects.', function() { 282 | let fieldsStrings = [ 283 | 'fields[article]=title,body', 284 | 'fields[comment]=body,createdon', 285 | 'fields[rating]=stars' 286 | ]; 287 | 288 | let testData = requestDataSubset; 289 | fieldsStrings.forEach(function(fieldsString) { 290 | testData = JsonApiQueryParser.parseFields(fieldsString, testData); 291 | }); 292 | 293 | let expectedData = { 294 | include: [], 295 | fields: { 296 | article: ['title', 'body'], 297 | comment: ['body', 'createdon'], 298 | rating: ['stars'] 299 | }, 300 | sort: [], 301 | page: {}, 302 | filter: { 303 | like: {}, 304 | not: {}, 305 | lt: {}, 306 | lte: {}, 307 | gt: {}, 308 | gte: {} 309 | } 310 | }; 311 | 312 | expect(testData).to.deep.equal(expectedData); 313 | }); 314 | }); 315 | 316 | describe('parsePage function', function() { 317 | it('should push the values of the page strings to their matching queryData page objects.', function() { 318 | let pageStrings = [ 319 | 'page[limit]=20', 320 | 'page[offset]=180' 321 | ]; 322 | 323 | let testData = requestDataSubset; 324 | pageStrings.forEach(function(pageString) { 325 | testData = JsonApiQueryParser.parsePage(pageString, testData); 326 | }); 327 | 328 | let expectedData = { 329 | include: [], 330 | fields: {}, 331 | sort: [], 332 | page: { 333 | limit: '20', 334 | offset: '180' 335 | }, 336 | filter: { 337 | like: {}, 338 | not: {}, 339 | lt: {}, 340 | lte: {}, 341 | gt: {}, 342 | gte: {} 343 | } 344 | }; 345 | 346 | expect(testData).to.deep.equal(expectedData); 347 | }); 348 | }); 349 | 350 | describe('parseSort function', function() { 351 | it('should push the values of the sort string to the queryData sort array.', function() { 352 | let sortString = 'sort=-createdon,type'; 353 | 354 | let testData = JsonApiQueryParser.parseSort(sortString, requestDataSubset); 355 | let expectedData = { 356 | include: [], 357 | fields: {}, 358 | sort: ['-createdon', 'type'], 359 | page: {}, 360 | filter: { 361 | like: {}, 362 | not: {}, 363 | lt: {}, 364 | lte: {}, 365 | gt: {}, 366 | gte: {} 367 | } 368 | }; 369 | 370 | expect(testData).to.deep.equal(expectedData); 371 | }); 372 | }); 373 | 374 | describe('parseFilter function', function() { 375 | it('should place the values of the filter strings to their matching queryData filter objects.', function() { 376 | let filterString = 'filter[id]=5'; 377 | 378 | let testData = JsonApiQueryParser.parseFilter(filterString, requestDataSubset); 379 | let expectedData = { 380 | include: [], 381 | fields: {}, 382 | sort: [], 383 | page: {}, 384 | filter: { 385 | id: '5', 386 | like: {}, 387 | not: {}, 388 | lt: {}, 389 | lte: {}, 390 | gt: {}, 391 | gte: {} 392 | } 393 | }; 394 | 395 | expect(testData).to.deep.equal(expectedData); 396 | 397 | let filterString2 = 'filter[name]=john doe'; 398 | let testData2 = JsonApiQueryParser.parseFilter(filterString2, testData); 399 | let expectedData2 = { 400 | include: [], 401 | fields: {}, 402 | sort: [], 403 | page: {}, 404 | filter: { 405 | id: '5', 406 | name: 'john doe', 407 | like: {}, 408 | not: {}, 409 | lt: {}, 410 | lte: {}, 411 | gt: {}, 412 | gte: {} 413 | } 414 | }; 415 | expect(testData2).to.deep.equal(expectedData2); 416 | }); 417 | }); 418 | 419 | describe('parseFilterType function', function() { 420 | it('should place the values of the filterType strings to their matching queryData filterType objects.', function() { 421 | let filterString = 'filter[not][name]=jack'; 422 | 423 | let testData = JsonApiQueryParser.parseFilterType(filterString, requestDataSubset); 424 | let expectedData = { 425 | include: [], 426 | fields: {}, 427 | sort: [], 428 | page: {}, 429 | filter: { 430 | like: {}, 431 | not: { 432 | name: 'jack' 433 | }, 434 | lt: {}, 435 | lte: {}, 436 | gt: {}, 437 | gte: {} 438 | } 439 | }; 440 | 441 | expect(testData).to.deep.equal(expectedData); 442 | 443 | let filterString2 = 'filter[lt][age]=24'; 444 | let testData2 = JsonApiQueryParser.parseFilterType(filterString2, testData); 445 | let expectedData2 = { 446 | include: [], 447 | fields: {}, 448 | sort: [], 449 | page: {}, 450 | filter: { 451 | like: {}, 452 | not: { 453 | name: 'jack' 454 | }, 455 | lt: { 456 | age: '24' 457 | }, 458 | lte: {}, 459 | gt: {}, 460 | gte: {} 461 | } 462 | }; 463 | expect(testData2).to.deep.equal(expectedData2); 464 | }); 465 | }); 466 | 467 | describe('trimSlashes function', function() { 468 | it('should trim leading and trailing slashes recursively.', function() { 469 | let testString = '//article/5//'; 470 | let expectedString = 'article/5'; 471 | let result = JsonApiQueryParser.trimSlashes(testString); 472 | expect(result).to.equal(expectedString); 473 | 474 | testString = '/article/5/comments'; 475 | expectedString = 'article/5/comments'; 476 | result = JsonApiQueryParser.trimSlashes(testString); 477 | expect(result).to.equal(expectedString); 478 | 479 | testString = 'article/5/'; 480 | expectedString = 'article/5'; 481 | result = JsonApiQueryParser.trimSlashes(testString); 482 | expect(result).to.equal(expectedString) 483 | }); 484 | }); 485 | 486 | }); 487 | 488 | --------------------------------------------------------------------------------