Skip to content

Commit 595d7bc

Browse files
authored
Merge pull request #19 from dynamiccast/sails-validation
Return sails validation errors as JSON API compliant object
2 parents f6e61fd + eff0999 commit 595d7bc

7 files changed

Lines changed: 173 additions & 30 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ This will expect sails Model attributes keys to follow the camelCase naming conv
8989
- [X] Allow the use of auto CreatedAt and UpdatedAt (see #3)
9090
- [ ] Pubsub integration
9191
- [X] Provide a service to serialize as JSON API for custom endpoints
92-
- [ ] Compatible with waterline data validation
92+
- [X] Compatible with waterline data validation
9393
- Repository
9494
- [X] Add tests on travis
9595
- [X] Provide status on the build on Github

lib/api/responses/invalid.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* 400 (Bad Request) Invalid data handler
3+
*
4+
* Usage:
5+
* return res.invalid(err);
6+
*
7+
* err must be returned from waterline with an error of type "E_VALIDATION"
8+
*/
9+
10+
module.exports = function badRequest(data, options) {
11+
12+
// Get access to `req`, `res`, & `sails`
13+
var req = this.req;
14+
var res = this.res;
15+
var sails = req._sails;
16+
17+
// Set status code
18+
res.status(400);
19+
20+
// Return 500 if no error was provided
21+
if (data === undefined || data.code !== "E_VALIDATION") {
22+
sails.log.verbose('res.invalid was called with invalid waterline error data: \n', data);
23+
return res.serverError('res.invalid was called with invalid waterline error data\n');
24+
}
25+
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+
45+
return res.json({
46+
'errors': errors
47+
});
48+
};

lib/api/responses/negotiate.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Generic Error Handler / Classifier with validation handler
3+
*
4+
* Copied from https://github.com/balderdashy/sails/blob/109254834fe7202c4f630c2da9c1cbf97bf4c016/lib/hooks/responses/defaults/negotiate.js
5+
*
6+
* Calls the appropriate custom response for a given error,
7+
* out of the bundled response modules:
8+
* badRequest, forbidden, notFound, & serverError
9+
*
10+
* Defaults to `res.serverError`
11+
*
12+
* Usage:
13+
* ```javascript
14+
* if (err) return res.negotiate(err);
15+
* ```
16+
*
17+
* @param {*} error(s)
18+
*
19+
*/
20+
21+
module.exports = function negotiate (err) {
22+
23+
// Get access to response object (`res`)
24+
var res = this.res;
25+
26+
var statusCode = 500;
27+
var body = err;
28+
29+
try {
30+
31+
statusCode = err.status || 500;
32+
33+
// Set the status
34+
// (should be taken care of by res.* methods, but this sets a default just in case)
35+
res.status(statusCode);
36+
37+
} catch (e) {}
38+
39+
// Respond using the appropriate custom response
40+
if (statusCode === 403) return res.forbidden(body);
41+
if (statusCode === 404) return res.notFound(body);
42+
43+
console.log(body);
44+
// This check is specific to sails-json-api-blueprints
45+
if (statusCode === 400 && err.code === "E_VALIDATION") return res.invalid(body);
46+
47+
if (statusCode >= 400 && statusCode < 500) return res.badRequest(body);
48+
return res.serverError(body);
49+
};

lib/api/services/JsonApiService.js

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -55,42 +55,44 @@ module.exports = {
5555
return caseSetting;
5656
},
5757

58-
deserialize: function(data) {
58+
// Code inspired from the MIT licenced module https://github.com/danivek/json-api-serializer
59+
_convertCase: function(data, convertCaseOptions) {
60+
61+
let converted;
5962

60-
// Code inspired from the MIT licenced module https://github.com/danivek/json-api-serializer
61-
const convertCase = function(data, convertCaseOptions) {
62-
let converted;
63-
if (_.isArray(data) || _.isPlainObject(data)) {
64-
converted = _.transform(data, (result, value, key) => {
65-
if (_.isArray(value) || _.isPlainObject(value)) {
66-
result[convertCase(key, convertCaseOptions)] = convertCase(value, convertCaseOptions);
67-
} else {
68-
result[convertCase(key, convertCaseOptions)] = value;
69-
}
70-
});
71-
} else {
72-
switch (convertCaseOptions) {
73-
case 'snake_case':
74-
converted = _.snakeCase(data);
75-
break;
76-
case 'kebab-case':
77-
converted = _.kebabCase(data);
78-
break;
79-
case 'camelCase':
80-
converted = _.camelCase(data);
81-
break;
82-
default:
83-
converted = data;
84-
break;
63+
if (_.isArray(data) || _.isPlainObject(data)) {
64+
converted = _.transform(data, (result, value, key) => {
65+
if (_.isArray(value) || _.isPlainObject(value)) {
66+
result[this._convertCase(key, convertCaseOptions)] = this._convertCase(value, convertCaseOptions);
67+
} else {
68+
result[this._convertCase(key, convertCaseOptions)] = value;
8569
}
70+
});
71+
} else {
72+
switch (convertCaseOptions) {
73+
case 'snake_case':
74+
converted = _.snakeCase(data);
75+
break;
76+
case 'kebab-case':
77+
converted = _.kebabCase(data);
78+
break;
79+
case 'camelCase':
80+
converted = _.camelCase(data);
81+
break;
82+
default:
83+
converted = data;
84+
break;
8685
}
86+
}
8787

88-
return converted;
89-
};
88+
return converted;
89+
},
90+
91+
deserialize: function(data) {
9092

9193
var caseSetting = this.getAttributesDeserializedCaseSetting();
9294
if (caseSetting !== undefined) {
93-
return convertCase(data, caseSetting);
95+
return this._convertCase(data, caseSetting);
9496
}
9597

9698
return data;

lib/hook.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ var responseNotFound = require('./api/responses/notFound');
2121
var responseBadRequest = require('./api/responses/badRequest');
2222
var responseForbidden = require('./api/responses/forbidden');
2323
var responseServerError = require('./api/responses/serverError');
24+
var responseNegotiate = require('./api/responses/negotiate');
25+
var responseInvalid = require('./api/responses/invalid');
2426

2527
function strncmp(a, b, n){
2628
return a.substring(0, n) == b.substring(0, n);
@@ -115,6 +117,8 @@ module.exports = function(sails) {
115117
sails.hooks.responses.middleware.badRequest = responseBadRequest;
116118
sails.hooks.responses.middleware.forbidden = responseForbidden;
117119
sails.hooks.responses.middleware.serverError = responseServerError;
120+
sails.hooks.responses.middleware.negotiate = responseNegotiate;
121+
sails.hooks.responses.middleware.invalid = responseInvalid;
118122

119123
// Load blueprint middleware and continue.
120124
loadMiddleware(cb);

tests/dummy/api/models/User.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,20 @@ module.exports = {
1010
attributes: {
1111
email: {
1212
type: 'string',
13+
email: true,
14+
unique: true,
1315
required: true
1416
},
1517
firstName: {
1618
type: 'string',
19+
minLength: 2,
20+
maxLength: 32,
1721
required: true
1822
},
1923
lastName: {
2024
type: 'string',
25+
minLength: 3,
26+
maxLength: 32,
2127
required: true
2228
}
2329
},

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,38 @@ describe('Error handling', function() {
101101
.end(done);
102102
});
103103
});
104+
105+
describe('POST /users with invalid attributes', function() {
106+
it('Should return 400 with error details', function (done) {
107+
108+
var userToCreate = {
109+
'data': {
110+
'attributes': {
111+
'email': 'test@jsonapi.com',
112+
'first-name':'a', // This is invalid. Minimum length is 3
113+
'last-name':'api'
114+
},
115+
'type':'users'
116+
}
117+
};
118+
119+
request(sails.hooks.http.app)
120+
.post('/users')
121+
.send(userToCreate)
122+
.expect(400)
123+
.expect(validateJSONapi)
124+
.expect({
125+
"errors": [
126+
{
127+
"detail": "\"minLength\" validation rule failed for input: 'a'\nSpecifically, it threw an error. Details:\n undefined",
128+
"source": {
129+
"pointer": "data/attributes/first-name"
130+
}
131+
}
132+
]
133+
})
134+
.end(done);
135+
});
136+
});
137+
104138
});

0 commit comments

Comments
 (0)