1 | /**
|
2 | @module @ember-data/adapter
|
3 | */
|
4 | import { dasherize } from '@ember/string';
|
5 |
|
6 | import { pluralize } from 'ember-inflector';
|
7 |
|
8 | import { serializeIntoHash } from './-private';
|
9 | import RESTAdapter from './rest';
|
10 |
|
11 | /**
|
12 | The `JSONAPIAdapter` is the default adapter used by Ember Data. It
|
13 | is responsible for transforming the store's requests into HTTP
|
14 | requests that follow the [JSON API](http://jsonapi.org/format/)
|
15 | format.
|
16 |
|
17 | ## JSON API Conventions
|
18 |
|
19 | The JSONAPIAdapter uses JSON API conventions for building the URL
|
20 | for a record and selecting the HTTP verb to use with a request. The
|
21 | actions you can take on a record map onto the following URLs in the
|
22 | JSON API adapter:
|
23 |
|
24 | <table>
|
25 | <tr>
|
26 | <th>
|
27 | Action
|
28 | </th>
|
29 | <th>
|
30 | HTTP Verb
|
31 | </th>
|
32 | <th>
|
33 | URL
|
34 | </th>
|
35 | </tr>
|
36 | <tr>
|
37 | <th>
|
38 | `store.findRecord('post', 123)`
|
39 | </th>
|
40 | <td>
|
41 | GET
|
42 | </td>
|
43 | <td>
|
44 | /posts/123
|
45 | </td>
|
46 | </tr>
|
47 | <tr>
|
48 | <th>
|
49 | `store.findAll('post')`
|
50 | </th>
|
51 | <td>
|
52 | GET
|
53 | </td>
|
54 | <td>
|
55 | /posts
|
56 | </td>
|
57 | </tr>
|
58 | <tr>
|
59 | <th>
|
60 | Update `postRecord.save()`
|
61 | </th>
|
62 | <td>
|
63 | PATCH
|
64 | </td>
|
65 | <td>
|
66 | /posts/123
|
67 | </td>
|
68 | </tr>
|
69 | <tr>
|
70 | <th>
|
71 | Create `store.createRecord('post').save()`
|
72 | </th>
|
73 | <td>
|
74 | POST
|
75 | </td>
|
76 | <td>
|
77 | /posts
|
78 | </td>
|
79 | </tr>
|
80 | <tr>
|
81 | <th>
|
82 | Delete `postRecord.destroyRecord()`
|
83 | </th>
|
84 | <td>
|
85 | DELETE
|
86 | </td>
|
87 | <td>
|
88 | /posts/123
|
89 | </td>
|
90 | </tr>
|
91 | </table>
|
92 |
|
93 | ## Success and failure
|
94 |
|
95 | The JSONAPIAdapter will consider a success any response with a
|
96 | status code of the 2xx family ("Success"), as well as 304 ("Not
|
97 | Modified"). Any other status code will be considered a failure.
|
98 |
|
99 | On success, the request promise will be resolved with the full
|
100 | response payload.
|
101 |
|
102 | Failed responses with status code 422 ("Unprocessable Entity") will
|
103 | be considered "invalid". The response will be discarded, except for
|
104 | the `errors` key. The request promise will be rejected with a
|
105 | `InvalidError`. This error object will encapsulate the saved
|
106 | `errors` value.
|
107 |
|
108 | Any other status codes will be treated as an adapter error. The
|
109 | request promise will be rejected, similarly to the invalid case,
|
110 | but with an instance of `AdapterError` instead.
|
111 |
|
112 | ### Endpoint path customization
|
113 |
|
114 | Endpoint paths can be prefixed with a `namespace` by setting the
|
115 | namespace property on the adapter:
|
116 |
|
117 | ```app/adapters/application.js
|
118 | import JSONAPIAdapter from '@ember-data/adapter/json-api';
|
119 |
|
120 | export default JSONAPIAdapter.extend({
|
121 | namespace: 'api/1'
|
122 | });
|
123 | ```
|
124 | Requests for the `person` model would now target `/api/1/people/1`.
|
125 |
|
126 | ### Host customization
|
127 |
|
128 | An adapter can target other hosts by setting the `host` property.
|
129 |
|
130 | ```app/adapters/application.js
|
131 | import JSONAPIAdapter from '@ember-data/adapter/json-api';
|
132 |
|
133 | export default JSONAPIAdapter.extend({
|
134 | host: 'https://api.example.com'
|
135 | });
|
136 | ```
|
137 |
|
138 | Requests for the `person` model would now target
|
139 | `https://api.example.com/people/1`.
|
140 |
|
141 | @since 1.13.0
|
142 | @class JSONAPIAdapter
|
143 | @constructor
|
144 | @extends RESTAdapter
|
145 | */
|
146 | const JSONAPIAdapter = RESTAdapter.extend({
|
147 | defaultSerializer: '-json-api',
|
148 |
|
149 | _defaultContentType: 'application/vnd.api+json',
|
150 |
|
151 | /**
|
152 | @method ajaxOptions
|
153 | @private
|
154 | @param {String} url
|
155 | @param {String} type The request type GET, POST, PUT, DELETE etc.
|
156 | @param {Object} options
|
157 | @return {Object}
|
158 | */
|
159 | ajaxOptions(url, type, options = {}) {
|
160 | let hash = this._super(url, type, options);
|
161 |
|
162 | hash.headers['Accept'] = hash.headers['Accept'] || 'application/vnd.api+json';
|
163 |
|
164 | return hash;
|
165 | },
|
166 |
|
167 | /**
|
168 | By default the JSONAPIAdapter will send each find request coming from a `store.find`
|
169 | or from accessing a relationship separately to the server. If your server supports passing
|
170 | ids as a query string, you can set coalesceFindRequests to true to coalesce all find requests
|
171 | within a single runloop.
|
172 |
|
173 | For example, if you have an initial payload of:
|
174 |
|
175 | ```javascript
|
176 | {
|
177 | data: {
|
178 | id: 1,
|
179 | type: 'post',
|
180 | relationship: {
|
181 | comments: {
|
182 | data: [
|
183 | { id: 1, type: 'comment' },
|
184 | { id: 2, type: 'comment' }
|
185 | ]
|
186 | }
|
187 | }
|
188 | }
|
189 | }
|
190 | ```
|
191 |
|
192 | By default calling `post.get('comments')` will trigger the following requests(assuming the
|
193 | comments haven't been loaded before):
|
194 |
|
195 | ```
|
196 | GET /comments/1
|
197 | GET /comments/2
|
198 | ```
|
199 |
|
200 | If you set coalesceFindRequests to `true` it will instead trigger the following request:
|
201 |
|
202 | ```
|
203 | GET /comments?filter[id]=1,2
|
204 | ```
|
205 |
|
206 | Setting coalesceFindRequests to `true` also works for `store.find` requests and `belongsTo`
|
207 | relationships accessed within the same runloop. If you set `coalesceFindRequests: true`
|
208 |
|
209 | ```javascript
|
210 | store.findRecord('comment', 1);
|
211 | store.findRecord('comment', 2);
|
212 | ```
|
213 |
|
214 | will also send a request to: `GET /comments?filter[id]=1,2`
|
215 |
|
216 | Note: Requests coalescing rely on URL building strategy. So if you override `buildURL` in your app
|
217 | `groupRecordsForFindMany` more likely should be overridden as well in order for coalescing to work.
|
218 |
|
219 | @property coalesceFindRequests
|
220 | @type {boolean}
|
221 | */
|
222 | coalesceFindRequests: false,
|
223 |
|
224 | findMany(store, type, ids, snapshots) {
|
225 | let url = this.buildURL(type.modelName, ids, snapshots, 'findMany');
|
226 | return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } });
|
227 | },
|
228 |
|
229 | pathForType(modelName) {
|
230 | let dasherized = dasherize(modelName);
|
231 | return pluralize(dasherized);
|
232 | },
|
233 |
|
234 | updateRecord(store, type, snapshot) {
|
235 | const data = serializeIntoHash(store, type, snapshot);
|
236 |
|
237 | let url = this.buildURL(type.modelName, snapshot.id, snapshot, 'updateRecord');
|
238 |
|
239 | return this.ajax(url, 'PATCH', { data: data });
|
240 | },
|
241 | });
|
242 |
|
243 | export default JSONAPIAdapter;
|