UNPKG

19.9 kBJavaScriptView Raw
1import 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*/
58export 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
667export { BuildURLMixin } from './-private';