-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
json-api.js
271 lines (228 loc) · 6.38 KB
/
json-api.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/* global heimdall */
/**
@module ember-data
*/
import { dasherize } from '@ember/string';
import RESTAdapter from './rest';
import { instrument } from 'ember-data/-debug';
import { pluralize } from 'ember-inflector';
/**
The `JSONAPIAdapter` is the default adapter used by Ember Data. It
is responsible for transforming the store's requests into HTTP
requests that follow the [JSON API](http://jsonapi.org/format/)
format.
## JSON API Conventions
The JSONAPIAdapter uses JSON API conventions for building the URL
for a record and selecting the HTTP verb to use with a request. The
actions you can take on a record map onto the following URLs in the
JSON API adapter:
<table>
<tr>
<th>
Action
</th>
<th>
HTTP Verb
</th>
<th>
URL
</th>
</tr>
<tr>
<th>
`store.findRecord('post', 123)`
</th>
<td>
GET
</td>
<td>
/posts/123
</td>
</tr>
<tr>
<th>
`store.findAll('post')`
</th>
<td>
GET
</td>
<td>
/posts
</td>
</tr>
<tr>
<th>
Update `postRecord.save()`
</th>
<td>
PATCH
</td>
<td>
/posts/123
</td>
</tr>
<tr>
<th>
Create `store.createRecord('post').save()`
</th>
<td>
POST
</td>
<td>
/posts
</td>
</tr>
<tr>
<th>
Delete `postRecord.destroyRecord()`
</th>
<td>
DELETE
</td>
<td>
/posts/123
</td>
</tr>
</table>
## Success and failure
The JSONAPIAdapter will consider a success any response with a
status code of the 2xx family ("Success"), as well as 304 ("Not
Modified"). Any other status code will be considered a failure.
On success, the request promise will be resolved with the full
response payload.
Failed responses with status code 422 ("Unprocessable Entity") will
be considered "invalid". The response will be discarded, except for
the `errors` key. The request promise will be rejected with a
`DS.InvalidError`. This error object will encapsulate the saved
`errors` value.
Any other status codes will be treated as an adapter error. The
request promise will be rejected, similarly to the invalid case,
but with an instance of `DS.AdapterError` instead.
### Endpoint path customization
Endpoint paths can be prefixed with a `namespace` by setting the
namespace property on the adapter:
```app/adapters/application.js
import DS from 'ember-data';
export default DS.JSONAPIAdapter.extend({
namespace: 'api/1'
});
```
Requests for the `person` model would now target `/api/1/people/1`.
### Host customization
An adapter can target other hosts by setting the `host` property.
```app/adapters/application.js
import DS from 'ember-data';
export default DS.JSONAPIAdapter.extend({
host: 'https://api.example.com'
});
```
Requests for the `person` model would now target
`https://api.example.com/people/1`.
@since 1.13.0
@class JSONAPIAdapter
@constructor
@namespace DS
@extends DS.RESTAdapter
*/
const JSONAPIAdapter = RESTAdapter.extend({
defaultSerializer: '-json-api',
/**
@method ajaxOptions
@private
@param {String} url
@param {String} type The request type GET, POST, PUT, DELETE etc.
@param {Object} options
@return {Object}
*/
ajaxOptions(url, type, options) {
let hash = this._super(...arguments);
if (hash.contentType) {
hash.contentType = 'application/vnd.api+json';
}
instrument(function() {
hash.converters = {
'text json': function(payload) {
let token = heimdall.start('json.parse');
let json;
try {
json = JSON.parse(payload);
} catch (e) {
json = payload;
}
heimdall.stop(token);
return json;
},
};
});
let beforeSend = hash.beforeSend;
hash.beforeSend = function(xhr) {
xhr.setRequestHeader('Accept', 'application/vnd.api+json');
if (beforeSend) {
beforeSend(xhr);
}
};
return hash;
},
/**
By default the JSONAPIAdapter will send each find request coming from a `store.find`
or from accessing a relationship separately to the server. If your server supports passing
ids as a query string, you can set coalesceFindRequests to true to coalesce all find requests
within a single runloop.
For example, if you have an initial payload of:
```javascript
{
data: {
id: 1,
type: 'post',
relationship: {
comments: {
data: [
{ id: 1, type: 'comment' },
{ id: 2, type: 'comment' }
]
}
}
}
}
```
By default calling `post.get('comments')` will trigger the following requests(assuming the
comments haven't been loaded before):
```
GET /comments/1
GET /comments/2
```
If you set coalesceFindRequests to `true` it will instead trigger the following request:
```
GET /comments?filter[id]=1,2
```
Setting coalesceFindRequests to `true` also works for `store.find` requests and `belongsTo`
relationships accessed within the same runloop. If you set `coalesceFindRequests: true`
```javascript
store.findRecord('comment', 1);
store.findRecord('comment', 2);
```
will also send a request to: `GET /comments?filter[id]=1,2`
Note: Requests coalescing rely on URL building strategy. So if you override `buildURL` in your app
`groupRecordsForFindMany` more likely should be overridden as well in order for coalescing to work.
@property coalesceFindRequests
@type {boolean}
*/
coalesceFindRequests: false,
findMany(store, type, ids, snapshots) {
let url = this.buildURL(type.modelName, ids, snapshots, 'findMany');
return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } });
},
pathForType(modelName) {
let dasherized = dasherize(modelName);
return pluralize(dasherized);
},
// TODO: Remove this once we have a better way to override HTTP verbs.
updateRecord(store, type, snapshot) {
let data = {};
let serializer = store.serializerFor(type.modelName);
serializer.serializeIntoHash(data, type, snapshot, { includeId: true });
let url = this.buildURL(type.modelName, snapshot.id, snapshot, 'updateRecord');
return this.ajax(url, 'PATCH', { data: data });
},
});
export default JSONAPIAdapter;