-
Notifications
You must be signed in to change notification settings - Fork 28
/
core.js
153 lines (134 loc) · 5.88 KB
/
core.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*$AMPERSAND_VERSION*/
var result = require('lodash/result');
var defaults = require('lodash/defaults');
var includes = require('lodash/includes');
var assign = require('lodash/assign');
var qs = require('qs');
var mediaType = require('media-type');
module.exports = function (xhr) {
// Throw an error when a URL is needed, and none is supplied.
var urlError = function () {
throw new Error('A "url" property or function must be specified');
};
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
return function (method, model, optionsInput) {
//Copy the options object. It's using assign instead of clonedeep as an optimization.
//The only object we could expect in options is headers, which is safely transfered below.
var options = assign({},optionsInput);
var type = methodMap[method];
var headers = {};
// Default options, unless specified.
defaults(options || (options = {}), {
emulateHTTP: false,
emulateJSON: false,
// overrideable primarily to enable testing
xhrImplementation: xhr
});
// Default request options.
var params = {type: type};
var ajaxConfig = result(model, 'ajaxConfig', {});
var key;
// Combine generated headers with user's headers.
if (ajaxConfig.headers) {
for (key in ajaxConfig.headers) {
headers[key.toLowerCase()] = ajaxConfig.headers[key];
}
}
if (options.headers) {
for (key in options.headers) {
headers[key.toLowerCase()] = options.headers[key];
}
delete options.headers;
}
//ajaxConfig has to be merged into params before other options take effect, so it is in fact a 2lvl default
assign(params, ajaxConfig);
params.headers = headers;
// Ensure that we have a URL.
if (!options.url) {
options.url = result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.json = options.attrs || model.toJSON(options);
}
// If passed a data param, we add it to the URL or body depending on request type
if (options.data && type === 'GET') {
// make sure we've got a '?'
options.url += includes(options.url, '?') ? '&' : '?';
// set stringify encoding options and create a different URI output if qsOption is defined
// ex) qsOptions = { indices: false }
// https://www.npmjs.com/package/qs/v/4.0.0#stringifying
options.url += qs.stringify(options.data, options.qsOptions);
//delete `data` so `xhr` doesn't use it as a body
delete options.data;
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.headers['content-type'] = 'application/x-www-form-urlencoded';
params.body = params.json ? {model: params.json} : {};
delete params.json;
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.body._method = type;
params.headers['x-http-method-override'] = type;
}
// When emulating JSON, we turn the body into a querystring.
// We do this later to let the emulateHTTP run its course.
if (options.emulateJSON) {
params.body = qs.stringify(params.body);
}
// Set raw XMLHttpRequest options.
if (ajaxConfig.xhrFields) {
var beforeSend = ajaxConfig.beforeSend;
params.beforeSend = function (req) {
assign(req, ajaxConfig.xhrFields);
if (beforeSend) return beforeSend.apply(this, arguments);
};
params.xhrFields = ajaxConfig.xhrFields;
}
// Turn a jQuery.ajax formatted request into xhr compatible
params.method = params.type;
var ajaxSettings = assign(params, options);
// Make the request. The callback executes functions that are compatible
// With jQuery.ajax's syntax.
var request = options.xhrImplementation(ajaxSettings, function (err, resp, body) {
if (err || resp.statusCode >= 400) {
if (options.error) {
try {
body = JSON.parse(body);
} catch(e){}
var message = (err? err.message : (body || "HTTP"+resp.statusCode));
options.error(resp, 'error', message);
}
} else {
// Parse body as JSON
var accept = mediaType.fromString(params.headers.accept);
var parseJson = accept.isValid() && accept.type === 'application' && (accept.subtype === 'json' || accept.suffix === 'json');
if (typeof body === 'string' && body !== '' && (!params.headers.accept || parseJson)) {
try {
body = JSON.parse(body);
} catch (err) {
if (options.error) options.error(resp, 'error', err.message);
if (options.always) options.always(err, resp, body);
return;
}
}
if (options.success) options.success(body, 'success', resp);
}
if (options.always) options.always(err, resp, body);
});
if (model) model.trigger('request', model, request, optionsInput, ajaxSettings);
request.ajaxSettings = ajaxSettings;
return request;
};
};