Skip to content

Commit 6fcaaf0

Browse files
author
Lee Richmond
committed
Add validation handling
1 parent 21956a5 commit 6fcaaf0

File tree

6 files changed

+202
-2
lines changed

6 files changed

+202
-2
lines changed

src/model.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { camelize } from './util/string';
1010
import WritePayload from './util/write-payload';
1111
import IncludeDirective from './util/include-directive';
1212
import DirtyChecker from './util/dirty-check';
13+
import ValidationErrors from './util/validation-errors';
1314
import relationshipIdentifiersFor from './util/relationship-identifiers';
1415
import Request from './request';
1516
import * as _cloneDeep from './util/clonedeep';
@@ -33,6 +34,7 @@ export default class Model {
3334
_originalAttributes: Object = {};
3435
_originalRelationships: Object = {};
3536
relationships: Object = {};
37+
errors: Object = {};
3638
__meta__: Object | void = null;
3739
_persisted: boolean = false;
3840
_markedForDestruction: boolean = false;
@@ -149,6 +151,10 @@ export default class Model {
149151
this._originalRelationships = this.relationshipResourceIdentifiers(Object.keys(this.relationships));
150152
}
151153

154+
clearErrors() {
155+
this.errors = {};
156+
}
157+
152158
// Todo:
153159
// * needs to recurse the directive
154160
// * remove the corresponding code from isPersisted and handle here (likely
@@ -207,6 +213,10 @@ export default class Model {
207213
return deserializeInstance(this, resource, payload);
208214
}
209215

216+
get hasError() {
217+
return Object.keys(this.errors).length > 1;
218+
}
219+
210220
isDirty(relationships?: Object | Array<any> | string) : boolean {
211221
let dc = new DirtyChecker(this);
212222
return dc.check(relationships);
@@ -243,14 +253,17 @@ export default class Model {
243253

244254
let json = payload.asJSON();
245255
let requestPromise = request[verb](url, json, { jwt });
256+
console.log('b4 req')
246257
return this._writeRequest(requestPromise, () => {
258+
console.log('HERE PRERS')
247259
this.isPersisted(true);
248260
payload.postProcess();
249261
});
250262
}
251263

252264
private _writeRequest(requestPromise : Promise<any>, callback: Function) : Promise<any> {
253265
return new Promise((resolve, reject) => {
266+
requestPromise.catch((e) => { throw(e) });
254267
return requestPromise.then((response) => {
255268
this._handleResponse(response, resolve, reject, callback);
256269
});
@@ -259,6 +272,7 @@ export default class Model {
259272

260273
private _handleResponse(response: any, resolve: Function, reject: Function, callback: Function) : void {
261274
if (response.status == 422) {
275+
ValidationErrors.apply(this, response['jsonPayload']);
262276
resolve(false);
263277
} else if (response.status >= 500) {
264278
reject('Server Error');

src/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default class Request {
5555
response.json().then((json) => {
5656
response['jsonPayload'] = json;
5757
resolve(response);
58-
});
58+
}).catch((e) => { throw(e); });
5959
});
6060
fetchPromise.catch(reject);
6161
});

src/util/validation-errors.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Model from '../model';
2+
3+
export default class ValidationErrors {
4+
model: Model;
5+
payload: Array<Object> = [];
6+
7+
constructor(model: Model, payload: Array<Object>) {
8+
this.model = model;
9+
this.payload = payload;
10+
}
11+
12+
static apply(model: Model, payload: Array<Object>) {
13+
let instance = new ValidationErrors(model, payload);
14+
instance.apply();
15+
}
16+
17+
apply() {
18+
this.payload['errors'].forEach((err) => {
19+
let meta = err['meta'];
20+
let metaRelationship = meta['relationship'];
21+
22+
if (metaRelationship) {
23+
this._processRelationship(this.model, metaRelationship);
24+
} else {
25+
this._processResource(this.model, meta);
26+
}
27+
});
28+
}
29+
30+
private _processResource(model: Model, meta: Object) {
31+
model.errors[meta['attribute']] = meta['message'];
32+
}
33+
34+
private _processRelationship(model: Model, meta: Object) {
35+
let relatedObject = model[meta['name']];
36+
if (Array.isArray(relatedObject)) {
37+
relatedObject = relatedObject.find((r) => {
38+
return (r.id === meta['id'] || r.temp_id === meta['temp-id']);
39+
});
40+
}
41+
if (meta['relationship']) {
42+
this._processRelationship(relatedObject, meta['relationship']);
43+
} else {
44+
this._processResource(relatedObject, meta);
45+
}
46+
}
47+
}

src/util/write-payload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export default class WritePayload {
9595
asJSON() : Object {
9696
let data = {}
9797

98+
this.model.clearErrors();
99+
98100
if (this.model.id) {
99101
data['id'] = this.model.id;
100102
}
@@ -120,12 +122,15 @@ export default class WritePayload {
120122
json['included'] = this.included
121123
}
122124

125+
console.log(json)
123126
return json;
124127
}
125128

126129
// private
127130

128131
private _processRelatedModel(model: Model, nested: Object) {
132+
model.clearErrors();
133+
129134
if (!model.isPersisted()) {
130135
model.temp_id = uuid.generate();
131136
}

test/integration/authorization-test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ describe('authorization headers', function() {
5959
describe('when header is returned in response', function() {
6060
beforeEach(function() {
6161
fetchMock.mock({
62-
name: 'route',
6362
matcher: '*',
6463
response: {
6564
status: 200,

test/integration/validations-test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { expect, sinon, fetchMock } from '../test-helper';
2+
import { Author, Book, Genre } from '../fixtures';
3+
import uuid from '../../src/util/uuid';
4+
5+
let serverResponse;
6+
7+
const resetMocks = function() {
8+
fetchMock.restore();
9+
10+
fetchMock.mock({
11+
matcher: '*',
12+
response: {
13+
status: 422,
14+
body: {
15+
errors: [
16+
{
17+
code: 'unprocessable_entity',
18+
status: '422',
19+
title: 'Validation Error',
20+
detail: 'First Name cannot be blank',
21+
meta: { attribute: 'first_name', message: 'cannot be blank' }
22+
},
23+
{
24+
code: 'unprocessable_entity',
25+
status: '422',
26+
title: 'Validation Error',
27+
detail: 'Last Name cannot be blank',
28+
meta: { attribute: 'last_name', message: 'cannot be blank' }
29+
},
30+
{
31+
code: 'unprocessable_entity',
32+
status: '422',
33+
title: 'Validation Error',
34+
detail: 'Title cannot be blank',
35+
meta: {
36+
relationship: {
37+
name: 'books',
38+
type: 'books',
39+
['temp-id']: 'abc1',
40+
attribute: 'title',
41+
message: 'cannot be blank'
42+
}
43+
}
44+
},
45+
{
46+
code: 'unprocessable_entity',
47+
status: '422',
48+
title: 'Validation Error',
49+
detail: 'Name cannot be blank',
50+
meta: {
51+
relationship: {
52+
name: 'books',
53+
type: 'books',
54+
['temp-id']: 'abc1',
55+
relationship: {
56+
name: 'genre',
57+
type: 'genres',
58+
id: '1',
59+
attribute: 'name',
60+
message: 'cannot be blank'
61+
}
62+
}
63+
}
64+
}
65+
]
66+
}
67+
}
68+
});
69+
70+
fetchMock.post('http://example.com/api/v1/people', function(url, payload) {
71+
return serverResponse;
72+
});
73+
}
74+
75+
let instance;
76+
let tempIdIndex = 0;
77+
describe('validations', function() {
78+
beforeEach(function () {
79+
resetMocks();
80+
});
81+
82+
beforeEach(function() {
83+
sinon.stub(uuid, 'generate').callsFake(function() {
84+
tempIdIndex++
85+
return `abc${tempIdIndex}`;
86+
});
87+
88+
instance = new Author({ lastName: 'King' });
89+
let genre = new Genre({ id: '1' });
90+
genre.isPersisted(true);
91+
let book = new Book({ title: 'blah', genre });
92+
instance.books = [book]
93+
});
94+
95+
afterEach(function() {
96+
tempIdIndex = 0;
97+
uuid.generate['restore']();
98+
});
99+
100+
// todo on next save, remove errs
101+
it('applies errors to the instance', function(done) {
102+
instance.save({ with: { books: 'genre' }}).then((success) => {
103+
expect(instance.isPersisted()).to.eq(false);
104+
expect(success).to.eq(false);
105+
expect(instance.errors).to.deep.equal({
106+
first_name: 'cannot be blank',
107+
last_name: 'cannot be blank'
108+
});
109+
done();
110+
});
111+
});
112+
113+
it('applies errors to nested hasMany relationships', function(done) {
114+
instance.save({ with: { books: 'genre' }}).then((success) => {
115+
expect(instance.isPersisted()).to.eq(false);
116+
expect(success).to.eq(false);
117+
expect(instance.books[0].errors).to.deep.equal({
118+
title: 'cannot be blank',
119+
});
120+
done();
121+
});
122+
});
123+
124+
it('applies errors to nested belongsTo relationships', function(done) {
125+
instance.save({ with: { books: 'genre' }}).then((success) => {
126+
expect(instance.isPersisted()).to.eq(false);
127+
expect(success).to.eq(false);
128+
console.log(instance.books[0].genre.errors)
129+
expect(instance.books[0].genre.errors).to.deep.equal({
130+
name: 'cannot be blank',
131+
});
132+
done();
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)