1 | import { assert } from '@ember/debug';
|
2 | import EmberError from '@ember/error';
|
3 |
|
4 | /**
|
5 | @module @ember-data/adapter
|
6 | */
|
7 |
|
8 | /**
|
9 | A `AdapterError` is used by an adapter to signal that an error occurred
|
10 | during a request to an external API. It indicates a generic error, and
|
11 | subclasses are used to indicate specific error states. The following
|
12 | subclasses are provided:
|
13 |
|
14 | - `InvalidError`
|
15 | - `TimeoutError`
|
16 | - `AbortError`
|
17 | - `UnauthorizedError`
|
18 | - `ForbiddenError`
|
19 | - `NotFoundError`
|
20 | - `ConflictError`
|
21 | - `ServerError`
|
22 |
|
23 | To create a custom error to signal a specific error state in communicating
|
24 | with an external API, extend the `AdapterError`. For example, if the
|
25 | external API exclusively used HTTP `503 Service Unavailable` to indicate
|
26 | it was closed for maintenance:
|
27 |
|
28 | ```app/adapters/maintenance-error.js
|
29 | import AdapterError from '@ember-data/adapter/error';
|
30 |
|
31 | export default AdapterError.extend({ message: "Down for maintenance." });
|
32 | ```
|
33 |
|
34 | This error would then be returned by an adapter's `handleResponse` method:
|
35 |
|
36 | ```app/adapters/application.js
|
37 | import JSONAPIAdapter from '@ember-data/adapter/json-api';
|
38 | import MaintenanceError from './maintenance-error';
|
39 |
|
40 | export default JSONAPIAdapter.extend({
|
41 | handleResponse(status) {
|
42 | if (503 === status) {
|
43 | return new MaintenanceError();
|
44 | }
|
45 |
|
46 | return this._super(...arguments);
|
47 | }
|
48 | });
|
49 | ```
|
50 |
|
51 | And can then be detected in an application and used to send the user to an
|
52 | `under-maintenance` route:
|
53 |
|
54 | ```app/routes/application.js
|
55 | import Route from '@ember/routing/route';
|
56 | import MaintenanceError from '../adapters/maintenance-error';
|
57 |
|
58 | export default Route.extend({
|
59 | actions: {
|
60 | error(error, transition) {
|
61 | if (error instanceof MaintenanceError) {
|
62 | this.transitionTo('under-maintenance');
|
63 | return;
|
64 | }
|
65 |
|
66 | // ...other error handling logic
|
67 | }
|
68 | }
|
69 | });
|
70 | ```
|
71 |
|
72 | @class AdapterError
|
73 | */
|
74 | function AdapterError(errors, message = 'Adapter operation failed') {
|
75 | this.isAdapterError = true;
|
76 | let error = EmberError.call(this, message);
|
77 |
|
78 | // in ember 3.8+ Error is a Native Error and we don't
|
79 | // gain these automatically from the EmberError.call
|
80 | if (error) {
|
81 | this.stack = error.stack;
|
82 | this.description = error.description;
|
83 | this.fileName = error.fileName;
|
84 | this.lineNumber = error.lineNumber;
|
85 | this.message = error.message;
|
86 | this.name = error.name;
|
87 | this.number = error.number;
|
88 | }
|
89 |
|
90 | this.errors = errors || [
|
91 | {
|
92 | title: 'Adapter Error',
|
93 | detail: message,
|
94 | },
|
95 | ];
|
96 | }
|
97 |
|
98 | export default AdapterError;
|
99 |
|
100 | function extendFn(ErrorClass) {
|
101 | return function({ message: defaultMessage } = {}) {
|
102 | return extend(ErrorClass, defaultMessage);
|
103 | };
|
104 | }
|
105 |
|
106 | function extend(ParentErrorClass, defaultMessage) {
|
107 | let ErrorClass = function(errors, message) {
|
108 | assert('`AdapterError` expects json-api formatted errors array.', Array.isArray(errors || []));
|
109 | ParentErrorClass.call(this, errors, message || defaultMessage);
|
110 | };
|
111 | ErrorClass.prototype = Object.create(ParentErrorClass.prototype);
|
112 | ErrorClass.extend = extendFn(ErrorClass);
|
113 |
|
114 | return ErrorClass;
|
115 | }
|
116 |
|
117 | AdapterError.prototype = Object.create(EmberError.prototype);
|
118 | AdapterError.prototype.code = 'AdapterError';
|
119 | AdapterError.extend = extendFn(AdapterError);
|
120 |
|
121 | /**
|
122 | A `InvalidError` is used by an adapter to signal the external API
|
123 | was unable to process a request because the content was not
|
124 | semantically correct or meaningful per the API. Usually, this means a
|
125 | record failed some form of server-side validation. When a promise
|
126 | from an adapter is rejected with a `InvalidError` the record will
|
127 | transition to the `invalid` state and the errors will be set to the
|
128 | `errors` property on the record.
|
129 |
|
130 | For Ember Data to correctly map errors to their corresponding
|
131 | properties on the model, Ember Data expects each error to be
|
132 | a valid JSON-API error object with a `source/pointer` that matches
|
133 | the property name. For example, if you had a Post model that
|
134 | looked like this.
|
135 |
|
136 | ```app/models/post.js
|
137 | import Model, { attr } from '@ember-data/model';
|
138 |
|
139 | export default Model.extend({
|
140 | title: attr('string'),
|
141 | content: attr('string')
|
142 | });
|
143 | ```
|
144 |
|
145 | To show an error from the server related to the `title` and
|
146 | `content` properties your adapter could return a promise that
|
147 | rejects with a `InvalidError` object that looks like this:
|
148 |
|
149 | ```app/adapters/post.js
|
150 | import RSVP from 'RSVP';
|
151 | import RESTAdapter from '@ember-data/adapter/rest';
|
152 | import { InvalidError } from '@ember-data/adapter/error';
|
153 |
|
154 | export default RESTAdapter.extend({
|
155 | updateRecord() {
|
156 | // Fictional adapter that always rejects
|
157 | return RSVP.reject(new InvalidError([
|
158 | {
|
159 | detail: 'Must be unique',
|
160 | source: { pointer: '/data/attributes/title' }
|
161 | },
|
162 | {
|
163 | detail: 'Must not be blank',
|
164 | source: { pointer: '/data/attributes/content'}
|
165 | }
|
166 | ]));
|
167 | }
|
168 | });
|
169 | ```
|
170 |
|
171 | Your backend may use different property names for your records the
|
172 | store will attempt to extract and normalize the errors using the
|
173 | serializer's `extractErrors` method before the errors get added to
|
174 | the model. As a result, it is safe for the `InvalidError` to
|
175 | wrap the error payload unaltered.
|
176 |
|
177 | @class InvalidError
|
178 | @extends AdapterError
|
179 | */
|
180 | export const InvalidError = extend(AdapterError, 'The adapter rejected the commit because it was invalid');
|
181 | InvalidError.prototype.code = 'InvalidError';
|
182 |
|
183 | /**
|
184 | A `TimeoutError` is used by an adapter to signal that a request
|
185 | to the external API has timed out. I.e. no response was received from
|
186 | the external API within an allowed time period.
|
187 |
|
188 | An example use case would be to warn the user to check their internet
|
189 | connection if an adapter operation has timed out:
|
190 |
|
191 | ```app/routes/application.js
|
192 | import Route from '@ember/routing/route';
|
193 | import { TimeoutError } from '@ember-data/adapter/error';
|
194 | import { action } from '@ember/object';
|
195 |
|
196 | export default class ApplicationRoute extends Route {
|
197 | @action
|
198 | error(error, transition) {
|
199 | if (error instanceof TimeoutError) {
|
200 | // alert the user
|
201 | alert('Are you still connected to the Internet?');
|
202 | return;
|
203 | }
|
204 |
|
205 | // ...other error handling logic
|
206 | }
|
207 | }
|
208 | ```
|
209 |
|
210 | @class TimeoutError
|
211 | @extends AdapterError
|
212 | */
|
213 | export const TimeoutError = extend(AdapterError, 'The adapter operation timed out');
|
214 | TimeoutError.prototype.code = 'TimeoutError';
|
215 |
|
216 | /**
|
217 | A `AbortError` is used by an adapter to signal that a request to
|
218 | the external API was aborted. For example, this can occur if the user
|
219 | navigates away from the current page after a request to the external API
|
220 | has been initiated but before a response has been received.
|
221 |
|
222 | @class AbortError
|
223 | @extends AdapterError
|
224 | */
|
225 | export const AbortError = extend(AdapterError, 'The adapter operation was aborted');
|
226 | AbortError.prototype.code = 'AbortError';
|
227 |
|
228 | /**
|
229 | A `UnauthorizedError` equates to a HTTP `401 Unauthorized` response
|
230 | status. It is used by an adapter to signal that a request to the external
|
231 | API was rejected because authorization is required and has failed or has not
|
232 | yet been provided.
|
233 |
|
234 | An example use case would be to redirect the user to a login route if a
|
235 | request is unauthorized:
|
236 |
|
237 | ```app/routes/application.js
|
238 | import Route from '@ember/routing/route';
|
239 | import { UnauthorizedError } from '@ember-data/adapter/error';
|
240 | import { action } from '@ember/object';
|
241 |
|
242 | export default class ApplicationRoute extends Route {
|
243 | @action
|
244 | error(error, transition) {
|
245 | if (error instanceof UnauthorizedError) {
|
246 | // go to the login route
|
247 | this.transitionTo('login');
|
248 | return;
|
249 | }
|
250 |
|
251 | // ...other error handling logic
|
252 | }
|
253 | }
|
254 | ```
|
255 |
|
256 | @class UnauthorizedError
|
257 | @extends AdapterError
|
258 | */
|
259 | export const UnauthorizedError = extend(AdapterError, 'The adapter operation is unauthorized');
|
260 | UnauthorizedError.prototype.code = 'UnauthorizedError';
|
261 |
|
262 | /**
|
263 | A `ForbiddenError` equates to a HTTP `403 Forbidden` response status.
|
264 | It is used by an adapter to signal that a request to the external API was
|
265 | valid but the server is refusing to respond to it. If authorization was
|
266 | provided and is valid, then the authenticated user does not have the
|
267 | necessary permissions for the request.
|
268 |
|
269 | @class ForbiddenError
|
270 | @extends AdapterError
|
271 | */
|
272 | export const ForbiddenError = extend(AdapterError, 'The adapter operation is forbidden');
|
273 | ForbiddenError.prototype.code = 'ForbiddenError';
|
274 |
|
275 | /**
|
276 | A `NotFoundError` equates to a HTTP `404 Not Found` response status.
|
277 | It is used by an adapter to signal that a request to the external API
|
278 | was rejected because the resource could not be found on the API.
|
279 |
|
280 | An example use case would be to detect if the user has entered a route
|
281 | for a specific model that does not exist. For example:
|
282 |
|
283 | ```app/routes/post.js
|
284 | import Route from '@ember/routing/route';
|
285 | import { NotFoundError } from '@ember-data/adapter/error';
|
286 | import { inject as service } from '@ember/service';
|
287 | import { action } from '@ember/object';
|
288 |
|
289 | export default class PostRoute extends Route {
|
290 | @service store;
|
291 | model(params) {
|
292 | return this.get('store').findRecord('post', params.post_id);
|
293 | }
|
294 | @action
|
295 | error(error, transition) {
|
296 | if (error instanceof NotFoundError) {
|
297 | // redirect to a list of all posts instead
|
298 | this.transitionTo('posts');
|
299 | } else {
|
300 | // otherwise let the error bubble
|
301 | return true;
|
302 | }
|
303 | }
|
304 | }
|
305 | ```
|
306 |
|
307 | @class NotFoundError
|
308 | @extends AdapterError
|
309 | */
|
310 | export const NotFoundError = extend(AdapterError, 'The adapter could not find the resource');
|
311 | NotFoundError.prototype.code = 'NotFoundError';
|
312 |
|
313 | /**
|
314 | A `ConflictError` equates to a HTTP `409 Conflict` response status.
|
315 | It is used by an adapter to indicate that the request could not be processed
|
316 | because of a conflict in the request. An example scenario would be when
|
317 | creating a record with a client-generated ID but that ID is already known
|
318 | to the external API.
|
319 |
|
320 | @class ConflictError
|
321 | @extends AdapterError
|
322 | */
|
323 | export const ConflictError = extend(AdapterError, 'The adapter operation failed due to a conflict');
|
324 | ConflictError.prototype.code = 'ConflictError';
|
325 |
|
326 | /**
|
327 | A `ServerError` equates to a HTTP `500 Internal Server Error` response
|
328 | status. It is used by the adapter to indicate that a request has failed
|
329 | because of an error in the external API.
|
330 |
|
331 | @class ServerError
|
332 | @extends AdapterError
|
333 | */
|
334 | export const ServerError = extend(AdapterError, 'The adapter operation failed due to a server error');
|
335 | ServerError.prototype.code = 'ServerError';
|
336 |
|
337 | export { errorsHashToArray, errorsArrayToHash } from '@ember-data/store/-private';
|