Skip to content

Commit aa1b49a

Browse files
authored
Merge pull request #20 from dynamiccast/test-custom-validation-message
Test custom validation message from sails-hook-validation
2 parents 595d7bc + 928f773 commit aa1b49a

File tree

9 files changed

+218
-26
lines changed

9 files changed

+218
-26
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ node_js:
33
- "6"
44
before_script:
55
- npm install -g mocha
6-
- cd ./tests/dummy && npm install
6+
- cd ./tests/dummy && npm install && cd ../../

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ As shown in [tests/dummy/api/controllers/UserController.js:24](https://github.co
4646
- `destroyOneRecord` DELETE /{model}
4747
- `updateOneRecord` PATCH /{model}/{id}
4848

49+
## Data validation
50+
51+
This module is compatible with default Sails.js waterline validations and *sails-hook-validation*. It will produce a JSON API error compliant object.
52+
53+
When a validation error object is returned by Waterline, you can reject the request with `res.invalid(err)` where `err` is your response object.
54+
`res.negociate(err)` will also forward the request to the `invalid` response as expected.
55+
4956
## Customize serialized JSON models' attributes keys case
5057

5158
While JSON API recommends multiple words variable to use a '-' as separator (http://jsonapi.org/recommendations/#naming) *sails-json-api-blueprints* remains open to `kebab-case` (the preferred), `snake_case`, `camelCase` or simply no change at all during serialization.

lib/api/responses/invalid.js

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* err must be returned from waterline with an error of type "E_VALIDATION"
88
*/
99

10-
module.exports = function badRequest(data, options) {
10+
module.exports = function invalid(data, options) {
1111

1212
// Get access to `req`, `res`, & `sails`
1313
var req = this.req;
@@ -23,26 +23,7 @@ module.exports = function badRequest(data, options) {
2323
return res.serverError('res.invalid was called with invalid waterline error data\n');
2424
}
2525

26-
var errors = [];
27-
28-
for (var attributeName in data.invalidAttributes) {
29-
30-
var attributes = data.invalidAttributes[attributeName];
31-
32-
for (var index in attributes) {
33-
34-
var error = attributes[index];
35-
36-
errors.push({
37-
detail: error.message,
38-
source: {
39-
pointer: "data/attributes/" + JsonApiService._convertCase(attributeName, JsonApiService.getAttributesSerializedCaseSetting())
40-
}
41-
});
42-
}
43-
}
44-
4526
return res.json({
46-
'errors': errors
27+
'errors': JsonApiService.serializeWaterlineValidationError(data)
4728
});
4829
};

lib/api/services/JsonApiService.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const _ = require("lodash");
12
const cleanObject = require('clean-object');
23
const JSONAPISerializer = require('json-api-serializer');
34
const Serializer = new JSONAPISerializer();
@@ -124,5 +125,33 @@ module.exports = {
124125
delete returnedValue.jsonapi; // Let's ignore the version for now
125126

126127
return returnedValue;
128+
},
129+
130+
// Turn a waterline validation error object into a JSON API compliant error object
131+
serializeWaterlineValidationError: function(data) {
132+
133+
var errors = [];
134+
135+
// data.Errors is populated by sails-hook-validations and data.invalidAttributes is the default
136+
var targetAttributes = (data.Errors !== undefined) ? data.Errors : data.invalidAttributes;
137+
138+
for (var attributeName in targetAttributes) {
139+
140+
var attributes = targetAttributes[attributeName];
141+
142+
for (var index in attributes) {
143+
144+
var error = attributes[index];
145+
146+
errors.push({
147+
detail: error.message,
148+
source: {
149+
pointer: "data/attributes/" + this._convertCase(attributeName, this.getAttributesSerializedCaseSetting())
150+
}
151+
});
152+
}
153+
}
154+
155+
return errors;
127156
}
128157
}

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"description": "Blueprints to turn a Sails.js API into a JSON API",
55
"main": "index.js",
66
"scripts": {
7-
"test": "cd tests/dummy ; npm test"
7+
"test": "npm run-script test-jsonapi ; npm run-script test-unit",
8+
"test-jsonapi": "cd tests/dummy ; npm test",
9+
"test-unit": "mocha tests/unit/**/*.test.js"
810
},
911
"repository": {
1012
"type": "git",
@@ -32,5 +34,10 @@
3234
},
3335
"sails": {
3436
"isHook": true
37+
},
38+
"devDependencies": {
39+
"chai": "^3.5.0",
40+
"mocha": "2.4.5",
41+
"jsonapi-validator": "1.0.1"
3542
}
3643
}

tests/dummy/api/models/Category.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,21 @@
88
module.exports = {
99

1010
attributes: {
11-
name: 'string'
11+
name: {
12+
type: 'string',
13+
minLength: 2,
14+
maxLength: 15,
15+
required: true
16+
}
17+
},
18+
19+
//model validation messages definitions
20+
validationMessages: { //hand for i18n & l10n
21+
name: {
22+
minLength: 'Minimum length is 2',
23+
maxLength: 'Minimum length is 15',
24+
required: 'Name is required'
25+
}
1226
},
1327

1428
autoCreatedAt: false,

tests/dummy/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"pluralize": "2.0.0",
2323
"rc": "1.0.1",
2424
"sails": "~0.12.3",
25-
"sails-disk": "~0.10.9"
25+
"sails-disk": "~0.10.9",
26+
"sails-hook-validation": "^0.4.6"
2627
},
2728
"devDependencies": {
2829
"supertest": "^1.2.0",

tests/dummy/test/integration/controllers/Errors.test.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ describe('Error handling', function() {
124124
.expect({
125125
"errors": [
126126
{
127-
"detail": "\"minLength\" validation rule failed for input: 'a'\nSpecifically, it threw an error. Details:\n undefined",
127+
"detail": "user.firstName.minLength",
128128
"source": {
129129
"pointer": "data/attributes/first-name"
130130
}
@@ -135,4 +135,34 @@ describe('Error handling', function() {
135135
});
136136
});
137137

138+
describe("POST /categories with invalid attributes", function() {
139+
it('Should return human readable errors from sails-hook-validation', function (done) {
140+
141+
var categoryToCreate = {
142+
'data': {
143+
'attributes': {
144+
name: "a" // Invalid because minLength is 2
145+
},
146+
'type':'categories'
147+
}
148+
};
149+
150+
request(sails.hooks.http.app)
151+
.post('/categories')
152+
.send(categoryToCreate)
153+
.expect(400)
154+
.expect(validateJSONapi)
155+
.expect({
156+
errors: [
157+
{
158+
detail: "Minimum length is 2",
159+
source: {
160+
pointer: "data/attributes/name"
161+
}
162+
}
163+
]
164+
})
165+
.end(done);
166+
});
167+
});
138168
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const JsonApiService = require("../../../lib/api/services/JsonApiService");
2+
var JSONAPIValidator = require('jsonapi-validator').Validator;
3+
const expect = require('chai').expect;
4+
5+
validateJSONapi = function(data) {
6+
var validator = new JSONAPIValidator();
7+
8+
try {
9+
validator.validate(data);
10+
return true;
11+
} catch (e) {
12+
return false;
13+
}
14+
};
15+
16+
sails = {
17+
config: {}
18+
};
19+
20+
describe("Is JsonApiService present", function() {
21+
22+
it("Should be an object", function() {
23+
24+
expect(JsonApiService).to.be.an('object');
25+
});
26+
});
27+
28+
describe("Serialize Waterline validation error", function() {
29+
30+
describe("Serialize default Waterline error object", function() {
31+
32+
const waterlineErr = {
33+
"error": "E_VALIDATION",
34+
"status": 400,
35+
"summary": "1 attribute is invalid",
36+
"model": "User",
37+
"invalidAttributes": {
38+
"firstName": [
39+
{
40+
"rule": "minLength",
41+
"message": "\"minLength\" validation rule failed for input: 'a'\nSpecifically, it threw an error. Details:\n undefined"
42+
}
43+
]
44+
}
45+
};
46+
47+
const expectedOutput = {
48+
"errors": [
49+
{
50+
"detail": "\"minLength\" validation rule failed for input: 'a'\nSpecifically, it threw an error. Details:\n undefined",
51+
"source": {
52+
"pointer": "data/attributes/firstName"
53+
}
54+
}
55+
]
56+
};
57+
58+
var jsonApiError = {
59+
errors: JsonApiService.serializeWaterlineValidationError(waterlineErr)
60+
};
61+
62+
it("Should be valid JSON API", function() {
63+
64+
expect(jsonApiError).to.satisfy(validateJSONapi);
65+
});
66+
67+
it("Should produce expected output", function() {
68+
69+
expect(jsonApiError).to.deep.equal(expectedOutput);
70+
});
71+
});
72+
73+
describe("Serialize Waterline error object produces by sails-validation-hook", function() {
74+
75+
const waterlineErr = {
76+
"error": "E_VALIDATION",
77+
"status": 400,
78+
"summary": "1 attribute is invalid",
79+
"model": "User",
80+
"invalidAttributes": {
81+
"firstName": [
82+
{
83+
"rule": "minLength",
84+
"message": "\"minLength\" validation rule failed for input: 'a'\nSpecifically, it threw an error. Details:\n undefined"
85+
}
86+
]
87+
},
88+
"Errors": {
89+
"firstName": [
90+
{
91+
"rule": "minLength",
92+
"message": "Minimum length is 2"
93+
}
94+
]
95+
}
96+
};
97+
98+
const expectedOutput = {
99+
"errors": [
100+
{
101+
"detail": "Minimum length is 2",
102+
"source": {
103+
"pointer": "data/attributes/firstName"
104+
}
105+
}
106+
]
107+
};
108+
109+
var jsonApiError = {
110+
errors: JsonApiService.serializeWaterlineValidationError(waterlineErr)
111+
};
112+
113+
it("Should be valid JSON API", function() {
114+
115+
expect(jsonApiError).to.satisfy(validateJSONapi);
116+
});
117+
118+
it("Should produce expected output", function() {
119+
120+
expect(jsonApiError).to.deep.equal(expectedOutput);
121+
});
122+
});
123+
});

0 commit comments

Comments
 (0)