Skip to content

Commit 0acd35b

Browse files
committed
Merge pull request #137 from holidayextras/parse-and-joi-validate-search-filter
Parse and joi validate search filter: update changelog and bump version
2 parents d37589d + 076d6ff commit 0acd35b

File tree

9 files changed

+218
-83
lines changed

9 files changed

+218
-83
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
- 2016-05-27 - v1.9.0
2+
- 2016-05-27 - Make parsed and validated filter available in request for handlers
13
- 2016-05-24 - v1.8.0
24
- 2016-05-24 - HTTPS support
35
- 2016-05-24 - v1.7.0

lib/filter.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/* @flow weak */
2+
"use strict";
3+
var filter = module.exports = { };
4+
5+
6+
var FILTER_OPERATORS = ["<", ">", "~", ":"];
7+
var STRING_ONLY_OPERATORS = ["~", ":"];
8+
9+
10+
filter._resourceDoesNotHaveProperty = function(resourceConfig, key) {
11+
if (resourceConfig.attributes[key]) return null;
12+
return {
13+
status: "403",
14+
code: "EFORBIDDEN",
15+
title: "Invalid filter",
16+
detail: resourceConfig.resource + " do not have attribute or relationship '" + key + "'"
17+
};
18+
};
19+
20+
filter._relationshipIsForeign = function(resourceConfig, key) {
21+
var relationSettings = resourceConfig.attributes[key]._settings;
22+
if (!relationSettings || !relationSettings.__as) return null;
23+
return {
24+
status: "403",
25+
code: "EFORBIDDEN",
26+
title: "Invalid filter",
27+
detail: "Filter relationship '" + key + "' is a foreign reference and does not exist on " + resourceConfig.resource
28+
};
29+
};
30+
31+
filter._splitElement = function(element) {
32+
if (!element) return null;
33+
if (FILTER_OPERATORS.indexOf(element[0]) !== -1) {
34+
return { operator: element[0], value: element.substring(1) };
35+
}
36+
return { operator: null, value: element };
37+
};
38+
39+
filter._stringOnlyOperator = function(operator, attributeConfig) {
40+
if (!operator || !attributeConfig) return null;
41+
if (STRING_ONLY_OPERATORS.indexOf(operator) !== -1 && attributeConfig._type !== "string") {
42+
return "operator " + operator + " can only be applied to string attributes";
43+
}
44+
return null;
45+
};
46+
47+
filter._parseScalarFilterElement = function(attributeConfig, scalarElement) {
48+
if (!scalarElement) return { error: "invalid or empty filter element" };
49+
50+
var splitElement = filter._splitElement(scalarElement);
51+
if (!splitElement) return { error: "empty filter" };
52+
53+
var error = filter._stringOnlyOperator(splitElement.operator, attributeConfig);
54+
if (error) return { error: error };
55+
56+
if (attributeConfig._settings) { // relationship attribute: no further validation
57+
return { result: splitElement };
58+
}
59+
60+
var validateResult = attributeConfig.validate(splitElement.value);
61+
if (validateResult.error) {
62+
return { error: validateResult.error.message };
63+
}
64+
65+
var validatedElement = { operator: splitElement.operator, value: validateResult.value };
66+
return { result: validatedElement };
67+
};
68+
69+
filter._parseFilterElementHelper = function(attributeConfig, filterElement) {
70+
if (!filterElement) return { error: "invalid or empty filter element" };
71+
72+
var parsedElements = [].concat(filterElement).map(function(scalarElement) {
73+
return filter._parseScalarFilterElement(attributeConfig, scalarElement);
74+
});
75+
76+
if (parsedElements.length === 1) return parsedElements[0];
77+
78+
var errors = parsedElements.reduce(function(combined, element) {
79+
if (!combined) {
80+
if (!element.error) return combined;
81+
return [ element.error ];
82+
}
83+
return combined.concat(element.error);
84+
}, null);
85+
86+
if (errors) return { error: errors };
87+
88+
var results = parsedElements.map(function(element) {
89+
return element.result;
90+
});
91+
92+
return { result: results };
93+
};
94+
95+
filter._parseFilterElement = function(attributeName, attributeConfig, filterElement) {
96+
var helperResult = filter._parseFilterElementHelper(attributeConfig, filterElement);
97+
98+
if (helperResult.error) {
99+
return {
100+
error: {
101+
status: "403",
102+
code: "EFORBIDDEN",
103+
title: "Invalid filter",
104+
detail: "Filter value for key '" + attributeName + "' is invalid: " + helperResult.error
105+
}
106+
};
107+
}
108+
return { result: helperResult.result };
109+
};
110+
111+
filter.parseAndValidate = function(request) {
112+
if (!request.params.filter) return null;
113+
114+
var resourceConfig = request.resourceConfig;
115+
116+
var processedFilter = { };
117+
var error;
118+
var filterElement;
119+
var parsedFilterElement;
120+
121+
for (var key in request.params.filter) {
122+
filterElement = request.params.filter[key];
123+
124+
if (!Array.isArray(filterElement) && filterElement instanceof Object) continue; // skip deep filters
125+
126+
error = filter._resourceDoesNotHaveProperty(resourceConfig, key);
127+
if (error) return error;
128+
129+
error = filter._relationshipIsForeign(resourceConfig, key);
130+
if (error) return error;
131+
132+
parsedFilterElement = filter._parseFilterElement(key, resourceConfig.attributes[key], filterElement);
133+
if (parsedFilterElement.error) return parsedFilterElement.error;
134+
135+
processedFilter[key] = [].concat(parsedFilterElement.result);
136+
}
137+
138+
request.processedFilter = processedFilter;
139+
140+
return null;
141+
};

lib/postProcess.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ postProcess._fetchRelatedResources = function(request, mainResource, callback) {
4949
var ids = resourcesToFetch[type];
5050
var urlJoiner = "&filter[id]=";
5151
ids = urlJoiner + ids.join(urlJoiner);
52-
return jsonApi._apiConfig.pathPrefix + type + "/?" + ids;
52+
var uri = jsonApi._apiConfig.pathPrefix + type + "/?" + ids;
53+
if (request.route.query) {
54+
uri += "&" + request.route.query;
55+
}
56+
return uri;
5357
});
5458

5559
async.map(resourcesToFetch, function(related, done) {

lib/postProcessing/filter.js

Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,20 @@ var _ = {
88
};
99
var debug = require("../debugging.js");
1010

11-
var FILTER_OPERATORS = ["<", ">", "~", ":"];
12-
1311
filter.action = function(request, response, callback) {
14-
var allFilters = _.assign({ }, request.params.filter);
15-
if (!allFilters) return callback();
16-
17-
var filters = { };
18-
for (var i in allFilters) {
19-
if (!request.resourceConfig.attributes[i]) {
20-
return callback({
21-
status: "403",
22-
code: "EFORBIDDEN",
23-
title: "Invalid filter",
24-
detail: request.resourceConfig.resource + " do not have property " + i
25-
});
26-
}
27-
if (allFilters[i] instanceof Array) {
28-
allFilters[i] = allFilters[i].join(",");
29-
}
30-
if (typeof allFilters[i] === "string") {
31-
filters[i] = allFilters[i];
32-
}
33-
}
12+
var filters = request.processedFilter;
13+
if (!filters) return callback();
3414

3515
if (response.data instanceof Array) {
3616
for (var j = 0; j < response.data.length; j++) {
37-
if (!filter._filterKeepObject(response.data[j], filters, request.resourceConfig.attributes)) {
17+
if (!filter._filterKeepObject(response.data[j], filters)) {
3818
debug.filter("removed", filters, JSON.stringify(response.data[j].attributes));
3919
response.data.splice(j, 1);
4020
j--;
4121
}
4222
}
4323
} else if (response.data instanceof Object) {
44-
if (!filter._filterKeepObject(response.data, filters, request.resourceConfig.attributes)) {
24+
if (!filter._filterKeepObject(response.data, filters)) {
4525
debug.filter("removed", filters, JSON.stringify(response.data.attributes));
4626
response.data = null;
4727
}
@@ -50,27 +30,10 @@ filter.action = function(request, response, callback) {
5030
return callback();
5131
};
5232

53-
filter._splitFilterElement = function(filterElementStr) {
54-
if (FILTER_OPERATORS.indexOf(filterElementStr[0]) !== -1) {
55-
return { operator: filterElementStr[0], value: filterElementStr.substring(1) };
56-
}
57-
return { operator: null, value: filterElementStr };
58-
};
59-
60-
filter._filterMatches = function(filterElementStr, attributeValue, attributeConfig) {
61-
var filterElement = filter._splitFilterElement(filterElementStr);
62-
var validationResult = attributeConfig.validate(filterElement.value);
63-
if (validationResult.error) {
64-
debug.filter("invalid filter condition value:", validationResult.error);
65-
return false;
66-
}
67-
filterElement.value = validationResult.value;
33+
filter._filterMatches = function(filterElement, attributeValue) {
6834
if (!filterElement.operator) {
6935
return _.isEqual(attributeValue, filterElement.value);
7036
}
71-
if (["~", ":"].indexOf(filterElement.operator) !== -1 && typeof filterElement.value !== "string") {
72-
return false;
73-
}
7437
var filterFunction = {
7538
">": function filterGreaterThan(attrValue, filterValue) {
7639
return attrValue > filterValue;
@@ -89,15 +52,14 @@ filter._filterMatches = function(filterElementStr, attributeValue, attributeConf
8952
return result;
9053
};
9154

92-
filter._filterKeepObject = function(someObject, filters, attributesConfig) {
55+
filter._filterKeepObject = function(someObject, filters) {
9356
for (var filterName in filters) {
94-
var whitelist = filters[filterName].split(",");
95-
var attributeConfig = attributesConfig[filterName];
57+
var whitelist = filters[filterName];
9658

9759
if (someObject.attributes.hasOwnProperty(filterName) || (filterName === "id")) {
9860
var attributeValue = someObject.attributes[filterName];
9961
if (filterName === "id") attributeValue = someObject.id;
100-
var attributeMatches = filter._attributesMatchesOR(attributeValue, attributeConfig, whitelist);
62+
var attributeMatches = filter._attributesMatchesOR(attributeValue, whitelist);
10163
if (!attributeMatches) return false;
10264
} else if (someObject.relationships.hasOwnProperty(filterName)) {
10365
var relationships = someObject.relationships[filterName];
@@ -110,10 +72,10 @@ filter._filterKeepObject = function(someObject, filters, attributesConfig) {
11072
return true;
11173
};
11274

113-
filter._attributesMatchesOR = function(attributeValue, attributeConfig, whitelist) {
75+
filter._attributesMatchesOR = function(attributeValue, whitelist) {
11476
var matchOR = false;
115-
whitelist.forEach(function(filterElementStr) {
116-
if (filter._filterMatches(filterElementStr, attributeValue, attributeConfig)) {
77+
whitelist.forEach(function(filterElement) {
78+
if (filter._filterMatches(filterElement, attributeValue)) {
11779
matchOR = true;
11880
}
11981
});
@@ -131,8 +93,8 @@ filter._relationshipMatchesOR = function(relationships, whitelist) {
13193
return relation.id;
13294
});
13395

134-
whitelist.forEach(function(filterElementStr) {
135-
if (data.indexOf(filterElementStr) !== -1) {
96+
whitelist.forEach(function(filterElement) {
97+
if (data.indexOf(filterElement.value) !== -1) {
13698
matchOR = true;
13799
}
138100
});

lib/routes/find.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var findRoute = module.exports = { };
55
var async = require("async");
66
var helper = require("./helper.js");
77
var router = require("../router.js");
8+
var filter = require("../filter.js");
89
var postProcess = require("../postProcess.js");
910
var responseHelper = require("../responseHelper.js");
1011

@@ -21,6 +22,9 @@ findRoute.register = function() {
2122
function(callback) {
2223
helper.verifyRequest(request, resourceConfig, res, "find", callback);
2324
},
25+
function parseAndValidateFilter(callback) {
26+
return callback(filter.parseAndValidate(request));
27+
},
2428
function(callback) {
2529
resourceConfig.handlers.find(request, callback);
2630
},

lib/routes/search.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var searchRoute = module.exports = { };
55
var async = require("async");
66
var helper = require("./helper.js");
77
var router = require("../router.js");
8+
var filter = require("../filter.js");
89
var pagination = require("../pagination.js");
910
var postProcess = require("../postProcess.js");
1011
var responseHelper = require("../responseHelper.js");
@@ -26,31 +27,8 @@ searchRoute.register = function() {
2627
function(callback) {
2728
helper.validate(request.params, resourceConfig.searchParams, callback);
2829
},
29-
function validateFilterParams(callback) {
30-
if (!request.params.filter) return callback();
31-
32-
for (var i in request.params.filter) {
33-
if (request.params.filter[i] instanceof Object) continue;
34-
if (!request.resourceConfig.attributes[i]) {
35-
return callback({
36-
status: "403",
37-
code: "EFORBIDDEN",
38-
title: "Invalid filter",
39-
detail: request.resourceConfig.resource + " do not have property " + i
40-
});
41-
}
42-
var relationSettings = request.resourceConfig.attributes[i]._settings;
43-
if (relationSettings && relationSettings.__as) {
44-
return callback({
45-
status: "403",
46-
code: "EFORBIDDEN",
47-
title: "Request validation failed",
48-
detail: "Requested relation \"" + i + "\" is a foreign reference and does not exist on " + request.params.type
49-
});
50-
}
51-
}
52-
53-
return callback();
30+
function parseAndValidateFilter(callback) {
31+
return callback(filter.parseAndValidate(request));
5432
},
5533
function validatePaginationParams(callback) {
5634
pagination.validatePaginationParams(request);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jsonapi-server",
3-
"version": "1.8.0",
3+
"version": "1.9.0",
44
"description": "A config driven NodeJS framework implementing json:api",
55
"keywords": [
66
"jsonapi",

test/get-resource-id-related.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe("Testing jsonapi-server", function() {
102102
});
103103

104104
it("with filter", function(done) {
105-
var url = "http://localhost:16006/rest/articles/de305d54-75b4-431b-adb2-eb6b9e546014/author?filter[email]=email";
105+
var url = "http://localhost:16006/rest/articles/de305d54-75b4-431b-adb2-eb6b9e546014/author?filter[email]=email@example.com";
106106
helpers.request({
107107
method: "GET",
108108
url: url
@@ -111,7 +111,7 @@ describe("Testing jsonapi-server", function() {
111111
json = helpers.validateJson(json);
112112

113113
assert.equal(res.statusCode, "200", "Expecting 200 OK");
114-
assert.deepEqual(json.data, null);
114+
assert(!json.data);
115115

116116
done();
117117
});

0 commit comments

Comments
 (0)