1 | import EmberObject from '@ember/object';
|
2 |
|
3 | /**
|
4 | An adapter is an object that receives requests from a store and
|
5 | translates them into the appropriate action to take against your
|
6 | persistence layer. The persistence layer is usually an HTTP API but
|
7 | may be anything, such as the browser's local storage. Typically the
|
8 | adapter is not invoked directly instead its functionality is accessed
|
9 | through the `store`.
|
10 |
|
11 | ### Creating an Adapter
|
12 |
|
13 | Create a new subclass of `Adapter` in the `app/adapters` folder:
|
14 |
|
15 | ```app/adapters/application.js
|
16 | import Adapter from '@ember-data/adapter';
|
17 |
|
18 | export default Adapter.extend({
|
19 | // ...your code here
|
20 | });
|
21 | ```
|
22 |
|
23 | Model-specific adapters can be created by putting your adapter
|
24 | class in an `app/adapters/` + `model-name` + `.js` file of the application.
|
25 |
|
26 | ```app/adapters/post.js
|
27 | import Adapter from '@ember-data/adapter';
|
28 |
|
29 | export default Adapter.extend({
|
30 | // ...Post-specific adapter code goes here
|
31 | });
|
32 | ```
|
33 |
|
34 | `Adapter` is an abstract base class that you should override in your
|
35 | application to customize it for your backend. The minimum set of methods
|
36 | that you should implement is:
|
37 |
|
38 | * `findRecord()`
|
39 | * `createRecord()`
|
40 | * `updateRecord()`
|
41 | * `deleteRecord()`
|
42 | * `findAll()`
|
43 | * `query()`
|
44 |
|
45 | To improve the network performance of your application, you can optimize
|
46 | your adapter by overriding these lower-level methods:
|
47 |
|
48 | * `findMany()`
|
49 |
|
50 |
|
51 | For an example of the implementation, see `RESTAdapter`, the
|
52 | included REST adapter.
|
53 |
|
54 | @module @ember-data/adapter
|
55 | @class Adapter
|
56 | @extends EmberObject
|
57 | */
|
58 | export default EmberObject.extend({
|
59 | /**
|
60 | If you would like your adapter to use a custom serializer you can
|
61 | set the `defaultSerializer` property to be the name of the custom
|
62 | serializer.
|
63 |
|
64 | Note the `defaultSerializer` serializer has a lower priority than
|
65 | a model specific serializer (i.e. `PostSerializer`) or the
|
66 | `application` serializer.
|
67 |
|
68 | ```app/adapters/django.js
|
69 | import Adapter from '@ember-data/adapter';
|
70 |
|
71 | export default Adapter.extend({
|
72 | defaultSerializer: 'django'
|
73 | });
|
74 | ```
|
75 |
|
76 | @deprecated
|
77 | @property defaultSerializer
|
78 | @type {String}
|
79 | */
|
80 | defaultSerializer: '-default',
|
81 |
|
82 | /**
|
83 | The `findRecord()` method is invoked when the store is asked for a record that
|
84 | has not previously been loaded. In response to `findRecord()` being called, you
|
85 | should query your persistence layer for a record with the given ID. The `findRecord`
|
86 | method should return a promise that will resolve to a JavaScript object that will be
|
87 | normalized by the serializer.
|
88 |
|
89 | Here is an example of the `findRecord` implementation:
|
90 |
|
91 | ```app/adapters/application.js
|
92 | import Adapter from '@ember-data/adapter';
|
93 | import RSVP from 'RSVP';
|
94 | import $ from 'jquery';
|
95 |
|
96 | export default Adapter.extend({
|
97 | findRecord(store, type, id, snapshot) {
|
98 | return new RSVP.Promise(function(resolve, reject) {
|
99 | $.getJSON(`/${type.modelName}/${id}`).then(function(data) {
|
100 | resolve(data);
|
101 | }, function(jqXHR) {
|
102 | reject(jqXHR);
|
103 | });
|
104 | });
|
105 | }
|
106 | });
|
107 | ```
|
108 |
|
109 | @method findRecord
|
110 | @param {Store} store
|
111 | @param {Model} type
|
112 | @param {String} id
|
113 | @param {Snapshot} snapshot
|
114 | @return {Promise} promise
|
115 | */
|
116 | findRecord: null,
|
117 |
|
118 | /**
|
119 | The `findAll()` method is used to retrieve all records for a given type.
|
120 |
|
121 | Example
|
122 |
|
123 | ```app/adapters/application.js
|
124 | import Adapter from '@ember-data/adapter';
|
125 | import RSVP from 'RSVP';
|
126 | import $ from 'jquery';
|
127 |
|
128 | export default Adapter.extend({
|
129 | findAll(store, type) {
|
130 | return new RSVP.Promise(function(resolve, reject) {
|
131 | $.getJSON(`/${type.modelName}`).then(function(data) {
|
132 | resolve(data);
|
133 | }, function(jqXHR) {
|
134 | reject(jqXHR);
|
135 | });
|
136 | });
|
137 | }
|
138 | });
|
139 | ```
|
140 |
|
141 | @method findAll
|
142 | @param {Store} store
|
143 | @param {Model} type
|
144 | @param {undefined} neverSet a value is never provided to this argument
|
145 | @param {SnapshotRecordArray} snapshotRecordArray
|
146 | @return {Promise} promise
|
147 | */
|
148 | findAll: null,
|
149 |
|
150 | /**
|
151 | This method is called when you call `query` on the store.
|
152 |
|
153 | Example
|
154 |
|
155 | ```app/adapters/application.js
|
156 | import Adapter from '@ember-data/adapter';
|
157 | import RSVP from 'RSVP';
|
158 | import $ from 'jquery';
|
159 |
|
160 | export default Adapter.extend({
|
161 | query(store, type, query) {
|
162 | return new RSVP.Promise(function(resolve, reject) {
|
163 | $.getJSON(`/${type.modelName}`, query).then(function(data) {
|
164 | resolve(data);
|
165 | }, function(jqXHR) {
|
166 | reject(jqXHR);
|
167 | });
|
168 | });
|
169 | }
|
170 | });
|
171 | ```
|
172 |
|
173 | @method query
|
174 | @param {Store} store
|
175 | @param {Model} type
|
176 | @param {Object} query
|
177 | @param {AdapterPopulatedRecordArray} recordArray
|
178 | @return {Promise} promise
|
179 | */
|
180 | query: null,
|
181 |
|
182 | /**
|
183 | The `queryRecord()` method is invoked when the store is asked for a single
|
184 | record through a query object.
|
185 |
|
186 | In response to `queryRecord()` being called, you should always fetch fresh
|
187 | data. Once found, you can asynchronously call the store's `push()` method
|
188 | to push the record into the store.
|
189 |
|
190 | Here is an example `queryRecord` implementation:
|
191 |
|
192 | Example
|
193 |
|
194 | ```app/adapters/application.js
|
195 | import Adapter, { BuildURLMixin } from '@ember-data/adapter';
|
196 | import RSVP from 'RSVP';
|
197 | import $ from 'jquery';
|
198 |
|
199 | export default Adapter.extend(BuildURLMixin, {
|
200 | queryRecord(store, type, query) {
|
201 | return new RSVP.Promise(function(resolve, reject) {
|
202 | $.getJSON(`/${type.modelName}`, query).then(function(data) {
|
203 | resolve(data);
|
204 | }, function(jqXHR) {
|
205 | reject(jqXHR);
|
206 | });
|
207 | });
|
208 | }
|
209 | });
|
210 | ```
|
211 |
|
212 | @method queryRecord
|
213 | @param {Store} store
|
214 | @param {subclass of Model} type
|
215 | @param {Object} query
|
216 | @return {Promise} promise
|
217 | */
|
218 | queryRecord: null,
|
219 |
|
220 | /**
|
221 | If the globally unique IDs for your records should be generated on the client,
|
222 | implement the `generateIdForRecord()` method. This method will be invoked
|
223 | each time you create a new record, and the value returned from it will be
|
224 | assigned to the record's `primaryKey`.
|
225 |
|
226 | Most traditional REST-like HTTP APIs will not use this method. Instead, the ID
|
227 | of the record will be set by the server, and your adapter will update the store
|
228 | with the new ID when it calls `didCreateRecord()`. Only implement this method if
|
229 | you intend to generate record IDs on the client-side.
|
230 |
|
231 | The `generateIdForRecord()` method will be invoked with the requesting store as
|
232 | the first parameter and the newly created record as the second parameter:
|
233 |
|
234 | ```javascript
|
235 | import Adapter from '@ember-data/adapter';
|
236 | import { v4 } from 'uuid';
|
237 |
|
238 | export default Adapter.extend({
|
239 | generateIdForRecord(store, type, inputProperties) {
|
240 | return v4();
|
241 | }
|
242 | });
|
243 | ```
|
244 |
|
245 | @method generateIdForRecord
|
246 | @param {Store} store
|
247 | @param {Model} type the Model class of the record
|
248 | @param {Object} inputProperties a hash of properties to set on the
|
249 | newly created record.
|
250 | @return {(String|Number)} id
|
251 | */
|
252 | generateIdForRecord: null,
|
253 |
|
254 | /**
|
255 | Proxies to the serializer's `serialize` method.
|
256 |
|
257 | Example
|
258 |
|
259 | ```app/adapters/application.js
|
260 | import Adapter from '@ember-data/adapter';
|
261 |
|
262 | export default Adapter.extend({
|
263 | createRecord(store, type, snapshot) {
|
264 | let data = this.serialize(snapshot, { includeId: true });
|
265 | let url = `/${type.modelName}`;
|
266 |
|
267 | // ...
|
268 | }
|
269 | });
|
270 | ```
|
271 |
|
272 | @method serialize
|
273 | @param {Snapshot} snapshot
|
274 | @param {Object} options
|
275 | @return {Object} serialized snapshot
|
276 | */
|
277 | serialize(snapshot, options) {
|
278 | return snapshot.serialize(options);
|
279 | },
|
280 |
|
281 | /**
|
282 | Implement this method in a subclass to handle the creation of
|
283 | new records.
|
284 |
|
285 | Serializes the record and sends it to the server.
|
286 |
|
287 | Example
|
288 |
|
289 | ```app/adapters/application.js
|
290 | import Adapter from '@ember-data/adapter';
|
291 | import { run } from '@ember/runloop';
|
292 | import RSVP from 'RSVP';
|
293 | import $ from 'jquery';
|
294 |
|
295 | export default Adapter.extend({
|
296 | createRecord(store, type, snapshot) {
|
297 | let data = this.serialize(snapshot, { includeId: true });
|
298 |
|
299 | return new RSVP.Promise(function(resolve, reject) {
|
300 | $.ajax({
|
301 | type: 'POST',
|
302 | url: `/${type.modelName}`,
|
303 | dataType: 'json',
|
304 | data: data
|
305 | }).then(function(data) {
|
306 | run(null, resolve, data);
|
307 | }, function(jqXHR) {
|
308 | jqXHR.then = null; // tame jQuery's ill mannered promises
|
309 | run(null, reject, jqXHR);
|
310 | });
|
311 | });
|
312 | }
|
313 | });
|
314 | ```
|
315 |
|
316 | @method createRecord
|
317 | @param {Store} store
|
318 | @param {Model} type the Model class of the record
|
319 | @param {Snapshot} snapshot
|
320 | @return {Promise} promise
|
321 | */
|
322 | createRecord: null,
|
323 |
|
324 | /**
|
325 | Implement this method in a subclass to handle the updating of
|
326 | a record.
|
327 |
|
328 | Serializes the record update and sends it to the server.
|
329 |
|
330 | The updateRecord method is expected to return a promise that will
|
331 | resolve with the serialized record. This allows the backend to
|
332 | inform the Ember Data store the current state of this record after
|
333 | the update. If it is not possible to return a serialized record
|
334 | the updateRecord promise can also resolve with `undefined` and the
|
335 | Ember Data store will assume all of the updates were successfully
|
336 | applied on the backend.
|
337 |
|
338 | Example
|
339 |
|
340 | ```app/adapters/application.js
|
341 | import Adapter from '@ember-data/adapter';
|
342 | import { run } from '@ember/runloop';
|
343 | import RSVP from 'RSVP';
|
344 | import $ from 'jquery';
|
345 |
|
346 | export default Adapter.extend({
|
347 | updateRecord(store, type, snapshot) {
|
348 | let data = this.serialize(snapshot, { includeId: true });
|
349 | let id = snapshot.id;
|
350 |
|
351 | return new RSVP.Promise(function(resolve, reject) {
|
352 | $.ajax({
|
353 | type: 'PUT',
|
354 | url: `/${type.modelName}/${id}`,
|
355 | dataType: 'json',
|
356 | data: data
|
357 | }).then(function(data) {
|
358 | run(null, resolve, data);
|
359 | }, function(jqXHR) {
|
360 | jqXHR.then = null; // tame jQuery's ill mannered promises
|
361 | run(null, reject, jqXHR);
|
362 | });
|
363 | });
|
364 | }
|
365 | });
|
366 | ```
|
367 |
|
368 | @method updateRecord
|
369 | @param {Store} store
|
370 | @param {Model} type the Model class of the record
|
371 | @param {Snapshot} snapshot
|
372 | @return {Promise} promise
|
373 | */
|
374 | updateRecord: null,
|
375 |
|
376 | /**
|
377 | Implement this method in a subclass to handle the deletion of
|
378 | a record.
|
379 |
|
380 | Sends a delete request for the record to the server.
|
381 |
|
382 | Example
|
383 |
|
384 | ```app/adapters/application.js
|
385 | import Adapter from '@ember-data/adapter';
|
386 | import { run } from '@ember/runloop';
|
387 | import RSVP from 'RSVP';
|
388 | import $ from 'jquery';
|
389 |
|
390 | export default Adapter.extend({
|
391 | deleteRecord(store, type, snapshot) {
|
392 | let data = this.serialize(snapshot, { includeId: true });
|
393 | let id = snapshot.id;
|
394 |
|
395 | return new RSVP.Promise(function(resolve, reject) {
|
396 | $.ajax({
|
397 | type: 'DELETE',
|
398 | url: `/${type.modelName}/${id}`,
|
399 | dataType: 'json',
|
400 | data: data
|
401 | }).then(function(data) {
|
402 | run(null, resolve, data);
|
403 | }, function(jqXHR) {
|
404 | jqXHR.then = null; // tame jQuery's ill mannered promises
|
405 | run(null, reject, jqXHR);
|
406 | });
|
407 | });
|
408 | }
|
409 | });
|
410 | ```
|
411 |
|
412 | @method deleteRecord
|
413 | @param {Store} store
|
414 | @param {Model} type the Model class of the record
|
415 | @param {Snapshot} snapshot
|
416 | @return {Promise} promise
|
417 | */
|
418 | deleteRecord: null,
|
419 |
|
420 | /**
|
421 | By default the store will try to coalesce all `fetchRecord` calls within the same runloop
|
422 | into as few requests as possible by calling groupRecordsForFindMany and passing it into a findMany call.
|
423 | You can opt out of this behaviour by either not implementing the findMany hook or by setting
|
424 | coalesceFindRequests to false.
|
425 |
|
426 | @property coalesceFindRequests
|
427 | @type {boolean}
|
428 | */
|
429 | coalesceFindRequests: true,
|
430 |
|
431 | /**
|
432 | The store will call `findMany` instead of multiple `findRecord`
|
433 | requests to find multiple records at once if coalesceFindRequests
|
434 | is true.
|
435 |
|
436 | ```app/adapters/application.js
|
437 | import Adapter from '@ember-data/adapter';
|
438 | import { run } from '@ember/runloop';
|
439 | import RSVP from 'RSVP';
|
440 | import $ from 'jquery';
|
441 |
|
442 | export default Adapter.extend({
|
443 | findMany(store, type, ids, snapshots) {
|
444 | return new RSVP.Promise(function(resolve, reject) {
|
445 | $.ajax({
|
446 | type: 'GET',
|
447 | url: `/${type.modelName}/`,
|
448 | dataType: 'json',
|
449 | data: { filter: { id: ids.join(',') } }
|
450 | }).then(function(data) {
|
451 | run(null, resolve, data);
|
452 | }, function(jqXHR) {
|
453 | jqXHR.then = null; // tame jQuery's ill mannered promises
|
454 | run(null, reject, jqXHR);
|
455 | });
|
456 | });
|
457 | }
|
458 | });
|
459 | ```
|
460 |
|
461 | @method findMany
|
462 | @param {Store} store
|
463 | @param {Model} type the Model class of the records
|
464 | @param {Array} ids
|
465 | @param {Array} snapshots
|
466 | @return {Promise} promise
|
467 | */
|
468 | findMany: null,
|
469 |
|
470 | /**
|
471 | Organize records into groups, each of which is to be passed to separate
|
472 | calls to `findMany`.
|
473 |
|
474 | For example, if your API has nested URLs that depend on the parent, you will
|
475 | want to group records by their parent.
|
476 |
|
477 | The default implementation returns the records as a single group.
|
478 |
|
479 | @method groupRecordsForFindMany
|
480 | @param {Store} store
|
481 | @param {Array} snapshots
|
482 | @return {Array} an array of arrays of records, each of which is to be
|
483 | loaded separately by `findMany`.
|
484 | */
|
485 | groupRecordsForFindMany(store, snapshots) {
|
486 | return [snapshots];
|
487 | },
|
488 |
|
489 | /**
|
490 | This method is used by the store to determine if the store should
|
491 | reload a record from the adapter when a record is requested by
|
492 | `store.findRecord`.
|
493 |
|
494 | If this method returns `true`, the store will re-fetch a record from
|
495 | the adapter. If this method returns `false`, the store will resolve
|
496 | immediately using the cached record.
|
497 |
|
498 | For example, if you are building an events ticketing system, in which users
|
499 | can only reserve tickets for 20 minutes at a time, and want to ensure that
|
500 | in each route you have data that is no more than 20 minutes old you could
|
501 | write:
|
502 |
|
503 | ```javascript
|
504 | shouldReloadRecord(store, ticketSnapshot) {
|
505 | let lastAccessedAt = ticketSnapshot.attr('lastAccessedAt');
|
506 | let timeDiff = moment().diff(lastAccessedAt, 'minutes');
|
507 |
|
508 | if (timeDiff > 20) {
|
509 | return true;
|
510 | } else {
|
511 | return false;
|
512 | }
|
513 | }
|
514 | ```
|
515 |
|
516 | This method would ensure that whenever you do `store.findRecord('ticket',
|
517 | id)` you will always get a ticket that is no more than 20 minutes old. In
|
518 | case the cached version is more than 20 minutes old, `findRecord` will not
|
519 | resolve until you fetched the latest version.
|
520 |
|
521 | By default this hook returns `false`, as most UIs should not block user
|
522 | interactions while waiting on data update.
|
523 |
|
524 | Note that, with default settings, `shouldBackgroundReloadRecord` will always
|
525 | re-fetch the records in the background even if `shouldReloadRecord` returns
|
526 | `false`. You can override `shouldBackgroundReloadRecord` if this does not
|
527 | suit your use case.
|
528 |
|
529 | @since 1.13.0
|
530 | @method shouldReloadRecord
|
531 | @param {Store} store
|
532 | @param {Snapshot} snapshot
|
533 | @return {Boolean}
|
534 | */
|
535 | shouldReloadRecord(store, snapshot) {
|
536 | return false;
|
537 | },
|
538 |
|
539 | /**
|
540 | This method is used by the store to determine if the store should
|
541 | reload all records from the adapter when records are requested by
|
542 | `store.findAll`.
|
543 |
|
544 | If this method returns `true`, the store will re-fetch all records from
|
545 | the adapter. If this method returns `false`, the store will resolve
|
546 | immediately using the cached records.
|
547 |
|
548 | For example, if you are building an events ticketing system, in which users
|
549 | can only reserve tickets for 20 minutes at a time, and want to ensure that
|
550 | in each route you have data that is no more than 20 minutes old you could
|
551 | write:
|
552 |
|
553 | ```javascript
|
554 | shouldReloadAll(store, snapshotArray) {
|
555 | let snapshots = snapshotArray.snapshots();
|
556 |
|
557 | return snapshots.any((ticketSnapshot) => {
|
558 | let lastAccessedAt = ticketSnapshot.attr('lastAccessedAt');
|
559 | let timeDiff = moment().diff(lastAccessedAt, 'minutes');
|
560 |
|
561 | if (timeDiff > 20) {
|
562 | return true;
|
563 | } else {
|
564 | return false;
|
565 | }
|
566 | });
|
567 | }
|
568 | ```
|
569 |
|
570 | This method would ensure that whenever you do `store.findAll('ticket')` you
|
571 | will always get a list of tickets that are no more than 20 minutes old. In
|
572 | case a cached version is more than 20 minutes old, `findAll` will not
|
573 | resolve until you fetched the latest versions.
|
574 |
|
575 | By default, this method returns `true` if the passed `snapshotRecordArray`
|
576 | is empty (meaning that there are no records locally available yet),
|
577 | otherwise, it returns `false`.
|
578 |
|
579 | Note that, with default settings, `shouldBackgroundReloadAll` will always
|
580 | re-fetch all the records in the background even if `shouldReloadAll` returns
|
581 | `false`. You can override `shouldBackgroundReloadAll` if this does not suit
|
582 | your use case.
|
583 |
|
584 | @since 1.13.0
|
585 | @method shouldReloadAll
|
586 | @param {Store} store
|
587 | @param {SnapshotRecordArray} snapshotRecordArray
|
588 | @return {Boolean}
|
589 | */
|
590 | shouldReloadAll(store, snapshotRecordArray) {
|
591 | return !snapshotRecordArray.length;
|
592 | },
|
593 |
|
594 | /**
|
595 | This method is used by the store to determine if the store should
|
596 | reload a record after the `store.findRecord` method resolves a
|
597 | cached record.
|
598 |
|
599 | This method is *only* checked by the store when the store is
|
600 | returning a cached record.
|
601 |
|
602 | If this method returns `true` the store will re-fetch a record from
|
603 | the adapter.
|
604 |
|
605 | For example, if you do not want to fetch complex data over a mobile
|
606 | connection, or if the network is down, you can implement
|
607 | `shouldBackgroundReloadRecord` as follows:
|
608 |
|
609 | ```javascript
|
610 | shouldBackgroundReloadRecord(store, snapshot) {
|
611 | let { downlink, effectiveType } = navigator.connection;
|
612 |
|
613 | return downlink > 0 && effectiveType === '4g';
|
614 | }
|
615 | ```
|
616 |
|
617 | By default, this hook returns `true` so the data for the record is updated
|
618 | in the background.
|
619 |
|
620 | @since 1.13.0
|
621 | @method shouldBackgroundReloadRecord
|
622 | @param {Store} store
|
623 | @param {Snapshot} snapshot
|
624 | @return {Boolean}
|
625 | */
|
626 | shouldBackgroundReloadRecord(store, snapshot) {
|
627 | return true;
|
628 | },
|
629 |
|
630 | /**
|
631 | This method is used by the store to determine if the store should
|
632 | reload a record array after the `store.findAll` method resolves
|
633 | with a cached record array.
|
634 |
|
635 | This method is *only* checked by the store when the store is
|
636 | returning a cached record array.
|
637 |
|
638 | If this method returns `true` the store will re-fetch all records
|
639 | from the adapter.
|
640 |
|
641 | For example, if you do not want to fetch complex data over a mobile
|
642 | connection, or if the network is down, you can implement
|
643 | `shouldBackgroundReloadAll` as follows:
|
644 |
|
645 | ```javascript
|
646 | shouldBackgroundReloadAll(store, snapshotArray) {
|
647 | let { downlink, effectiveType } = navigator.connection;
|
648 |
|
649 | return downlink > 0 && effectiveType === '4g';
|
650 | }
|
651 | ```
|
652 |
|
653 | By default this method returns `true`, indicating that a background reload
|
654 | should always be triggered.
|
655 |
|
656 | @since 1.13.0
|
657 | @method shouldBackgroundReloadAll
|
658 | @param {Store} store
|
659 | @param {SnapshotRecordArray} snapshotRecordArray
|
660 | @return {Boolean}
|
661 | */
|
662 | shouldBackgroundReloadAll(store, snapshotRecordArray) {
|
663 | return true;
|
664 | },
|
665 | });
|
666 |
|
667 | export { BuildURLMixin } from './-private';
|