Skip to content

Commit 4419026

Browse files
authored
Merge pull request #28 from richmolj/master
Add fetch middleware; improve promise handling
2 parents e6b9f55 + 0fd6a61 commit 4419026

File tree

7 files changed

+465
-26
lines changed

7 files changed

+465
-26
lines changed

src/configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export default class Config {
1717
static logger: Logger = new Logger();
1818
static jwtLocalStorage: string | false = 'jwt';
1919
static localStorage;
20+
static beforeFetch: Array<Function> = []
21+
static afterFetch: Array<Function> = []
2022

2123
static setup(options? : Object) : void {
2224
if (!options) options = {};

src/model.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ export default class Model {
8888
return options
8989
}
9090

91+
static beforeFetch(url: RequestInfo, options: RequestInit) : void {
92+
Config.beforeFetch.forEach((fn) => {
93+
fn(url, options)
94+
})
95+
}
96+
97+
static afterFetch(response: Response, json: JSON) : void {
98+
Config.afterFetch.forEach((fn) => {
99+
fn(response, json)
100+
})
101+
}
102+
91103
static getJWTOwner() : typeof Model {
92104
if (this.isJWTOwner) {
93105
return this;
@@ -215,7 +227,7 @@ export default class Model {
215227
if (this.klass.camelizeKeys) {
216228
attributeName = camelize(key);
217229
}
218-
230+
219231
if (key == 'id' || this.klass.attributeList[attributeName]) {
220232
this[attributeName] = attrs[key];
221233
}
@@ -281,7 +293,7 @@ export default class Model {
281293
destroy() : Promise<any> {
282294
let url = this.klass.url(this.id);
283295
let verb = 'delete';
284-
let request = new Request();
296+
let request = new Request(this.klass);
285297

286298
let requestPromise = request.delete(url, this._fetchOptions());
287299
return this._writeRequest(requestPromise, () => {
@@ -292,7 +304,7 @@ export default class Model {
292304
save(options: Object = {}) : Promise<any> {
293305
let url = this.klass.url();
294306
let verb = 'post';
295-
let request = new Request();
307+
let request = new Request(this.klass);
296308
let payload = new WritePayload(this, options['with']);
297309

298310
if (this.isPersisted()) {
@@ -304,7 +316,6 @@ export default class Model {
304316
let requestPromise = request[verb](url, json, this._fetchOptions());
305317
return this._writeRequest(requestPromise, (response) => {
306318
this.fromJsonapi(response['jsonPayload'].data, response['jsonPayload'], payload.includeDirective);
307-
//this.isPersisted(true);
308319
payload.postProcess();
309320
});
310321
}
@@ -320,10 +331,9 @@ export default class Model {
320331

321332
private _writeRequest(requestPromise : Promise<any>, callback: Function) : Promise<any> {
322333
return new Promise((resolve, reject) => {
323-
requestPromise.catch((e) => { throw(e) });
324334
return requestPromise.then((response) => {
325335
this._handleResponse(response, resolve, reject, callback);
326-
});
336+
}).catch(reject)
327337
});
328338
}
329339

@@ -333,8 +343,6 @@ export default class Model {
333343
if (response.status == 422) {
334344
ValidationErrors.apply(this, response['jsonPayload']);
335345
resolve(false);
336-
} else if (response.status >= 500) {
337-
reject('Server Error');
338346
} else {
339347
callback(response);
340348
resolve(true);

src/request.ts

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
import Config from './configuration';
2+
import Model from './model';
23
import colorize from './util/colorize';
4+
import patchExtends from './custom-extend';
5+
patchExtends()
6+
7+
class RequestError extends Error {
8+
url: string
9+
options: RequestInit
10+
originalError: Error
11+
12+
constructor(message: string, url: string, options: RequestInit, originalError: Error) {
13+
super(message)
14+
this.url = url
15+
this.options = options
16+
this.originalError = originalError
17+
}
18+
}
19+
20+
class ResponseError extends Error {
21+
response: Response
22+
originalError: Error
23+
24+
constructor(response: Response | null, message?: string, originalError?: Error) {
25+
super(message || 'Invalid Response')
26+
this.response = response
27+
this.originalError = originalError
28+
}
29+
}
330

431
export default class Request {
32+
modelClass: typeof Model
33+
34+
constructor(modelClass: typeof Model) {
35+
this.modelClass = modelClass
36+
}
37+
538
get(url : string, options: RequestInit) : Promise<any> {
639
options.method = 'GET';
740
return this._fetchWithLogging(url, options);
@@ -39,23 +72,54 @@ export default class Request {
3972
private _fetchWithLogging(url: string, options: RequestInit) : Promise<any> {
4073
this._logRequest(options.method, url);
4174
let promise = this._fetch(url, options);
42-
promise.then((response : any) => {
75+
return promise.then((response : any) => {
4376
this._logResponse(response['jsonPayload']);
77+
return response
4478
});
45-
return promise;
4679
}
4780

4881
private _fetch(url: string, options: RequestInit) : Promise<any> {
4982
return new Promise((resolve, reject) => {
83+
try {
84+
this.modelClass.beforeFetch(url, options)
85+
} catch(e) {
86+
reject(new RequestError('beforeFetch failed; review Config.beforeFetch', url, options, e))
87+
}
88+
5089
let fetchPromise = fetch(url, options);
5190
fetchPromise.then((response) => {
52-
response.json().then((json) => {
53-
response['jsonPayload'] = json;
54-
resolve(response);
55-
}).catch((e) => { throw(e); });
91+
this._handleResponse(response, resolve, reject)
5692
});
5793

58-
fetchPromise.catch(reject);
94+
fetchPromise.catch((e) => {
95+
// Fetch itself failed (usually network error)
96+
reject(new ResponseError(null, e.message, e))
97+
})
98+
});
99+
}
100+
101+
private _handleResponse(response: Response, resolve: Function, reject: Function) : void {
102+
response.json().then((json) => {
103+
try {
104+
this.modelClass.afterFetch(response, json)
105+
} catch(e) {
106+
// afterFetch middleware failed
107+
reject(new ResponseError(response, 'afterFetch failed; review Config.afterFetch', e))
108+
}
109+
110+
if (response.status >= 500) {
111+
reject(new ResponseError(response, 'Server Error'))
112+
} else if (response.status !== 422 && json['data'] === undefined) {
113+
// Bad JSON, for instance an errors payload
114+
// Allow 422 since we specially handle validation errors
115+
reject(new ResponseError(response, 'invalid json'))
116+
}
117+
118+
response['jsonPayload'] = json;
119+
resolve(response);
120+
}).catch((e) => {
121+
// The response was probably not in JSON format
122+
reject(new ResponseError(response, 'invalid json', e))
59123
});
60124
}
61125
}

src/scope.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default class Scope {
2929
return this._fetch(this.model.url()).then((json : japiDoc) => {
3030
let collection = new CollectionProxy<Model>(json);
3131
return collection;
32-
});
32+
})
3333
}
3434

3535
find(id : string | number) : Promise<RecordProxy<Model>> {
@@ -215,16 +215,16 @@ export default class Scope {
215215
}
216216

217217
private _fetch(url : string) : Promise<Object> {
218-
let qp = this.toQueryParams();
218+
let qp = this.toQueryParams()
219219
if (qp) {
220-
url = `${url}?${qp}`;
220+
url = `${url}?${qp}`
221221
}
222-
let request = new Request();
222+
let request = new Request(this.model)
223223
let fetchOpts = this.model.fetchOptions()
224-
225-
return request.get(url, fetchOpts).then((response) => {
226-
refreshJWT(this.model, response);
227-
return response['jsonPayload'];
228-
});
224+
let promise = request.get(url, fetchOpts)
225+
return promise.then((response) => {
226+
refreshJWT(this.model, response)
227+
return response['jsonPayload']
228+
})
229229
}
230230
}

test/fixtures.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const configSetup = function(opts = {}) {
106106
configSetup();
107107

108108
export {
109+
Config,
109110
configSetup,
110111
ApplicationRecord,
111112
TestJWTSubclass,

0 commit comments

Comments
 (0)