1 | import RESTAdapter from '@ember-data/adapter/rest';
|
2 | import AdapterError, {
|
3 | InvalidError,
|
4 | errorsHashToArray,
|
5 | } from '@ember-data/adapter/error';
|
6 | import { pluralize } from 'ember-inflector';
|
7 | import { AnyObject } from 'active-model-adapter';
|
8 | import { decamelize, underscore } from '@ember/string';
|
9 | // eslint-disable-next-line ember/use-ember-data-rfc-395-imports
|
10 | import ModelRegistry from 'ember-data/types/registries/model';
|
11 |
|
12 | interface ActiveModelPayload {
|
13 | errors: AnyObject;
|
14 | }
|
15 |
|
16 | /**
|
17 | @module ember-data
|
18 | */
|
19 |
|
20 | /**
|
21 | * The ActiveModelAdapter is a subclass of the RESTAdapter designed to integrate
|
22 | * with a JSON API that uses an underscored naming convention instead of camelCasing.
|
23 | * It has been designed to work out of the box with the
|
24 | * [active\_model\_serializers](http://github.com/rails-api/active_model_serializers)
|
25 | * Ruby gem. This Adapter expects specific settings using ActiveModel::Serializers,
|
26 | * `embed :ids, embed_in_root: true` which sideloads the records.
|
27 | *
|
28 | * This adapter extends the DS.RESTAdapter by making consistent use of the camelization,
|
29 | * decamelization and pluralization methods to normalize the serialized JSON into a
|
30 | * format that is compatible with a conventional Rails backend and Ember Data.
|
31 | *
|
32 | * ## JSON Structure
|
33 | *
|
34 | * The ActiveModelAdapter expects the JSON returned from your server to follow
|
35 | * the REST adapter conventions substituting underscored keys for camelcased ones.
|
36 | *
|
37 | * Unlike the DS.RESTAdapter, async relationship keys must be the singular form
|
38 | * of the relationship name, followed by "_id" for DS.belongsTo relationships,
|
39 | * or "_ids" for DS.hasMany relationships.
|
40 | *
|
41 | * ### Conventional Names
|
42 | *
|
43 | * Attribute names in your JSON payload should be the underscored versions of
|
44 | * the attributes in your Ember.js models.
|
45 | *
|
46 | * For example, if you have a `Person` model:
|
47 | *
|
48 | * ```javascript
|
49 | * export default class FamousPerson extends Model {
|
50 | * @attr() firstName;
|
51 | * @attr() lastName;
|
52 | * @attr() occupation;
|
53 | * }
|
54 | * ```
|
55 | *
|
56 | * The JSON returned should look like this:
|
57 | *
|
58 | * ```json
|
59 | * {
|
60 | * "famous_person": {
|
61 | * "id": 1,
|
62 | * "first_name": "Barack",
|
63 | * "last_name": "Obama",
|
64 | * "occupation": "President"
|
65 | * }
|
66 | * }
|
67 | * ```
|
68 | *
|
69 | * Let's imagine that `Occupation` is just another model:
|
70 | *
|
71 | * ```javascript
|
72 | * export default class Person extends Model {
|
73 | * @attr() firstName;
|
74 | * @attr() lastName;
|
75 | * @belongsTo('occupation') occupation;
|
76 | * }
|
77 | *
|
78 | * export default class Occupation extends Model {
|
79 | * @attr() name;
|
80 | * @attr('number') salary;
|
81 | * @hasMany('person') people;
|
82 | * }
|
83 | * ```
|
84 | *
|
85 | * The JSON needed to avoid extra server calls, should look like this:
|
86 | *
|
87 | * ```json
|
88 | * {
|
89 | * "people": [{
|
90 | * "id": 1,
|
91 | * "first_name": "Barack",
|
92 | * "last_name": "Obama",
|
93 | * "occupation_id": 1
|
94 | * }],
|
95 | *
|
96 | * "occupations": [{
|
97 | * "id": 1,
|
98 | * "name": "President",
|
99 | * "salary": 100000,
|
100 | * "person_ids": [1]
|
101 | * }]
|
102 | * }
|
103 | * ```
|
104 | *
|
105 | * @class ActiveModelAdapter
|
106 | * @constructor
|
107 | * @namespace DS
|
108 | * @extends DS.RESTAdapter
|
109 | */
|
110 | export default class ActiveModelAdapter extends RESTAdapter {
|
111 | defaultSerializer = '-active-model';
|
112 | /**
|
113 | * The ActiveModelAdapter overrides the `pathForType` method to build
|
114 | * underscored URLs by decamelizing and pluralizing the object type name.
|
115 | *
|
116 | * ```js
|
117 | * this.pathForType("famousPerson");
|
118 | * //=> "famous_people"
|
119 | * ```
|
120 | *
|
121 | * @method pathForType
|
122 | * @param {String} modelName
|
123 | * @return String
|
124 | */
|
125 | pathForType<K extends keyof ModelRegistry>(modelName: K): string {
|
126 | const decamelized = decamelize(modelName as string);
|
127 | const underscored = underscore(decamelized);
|
128 | return pluralize(underscored);
|
129 | }
|
130 |
|
131 | /**
|
132 | * The ActiveModelAdapter overrides the `handleResponse` method
|
133 | * to format errors passed to a DS.InvalidError for all
|
134 | * 422 Unprocessable Entity responses.
|
135 | *
|
136 | * A 422 HTTP response from the server generally implies that the request
|
137 | * was well formed but the API was unable to process it because the
|
138 | * content was not semantically correct or meaningful per the API.
|
139 | *
|
140 | * For more information on 422 HTTP Error code see 11.2 WebDAV RFC 4918
|
141 | * https://tools.ietf.org/html/rfc4918#section-11.2
|
142 | *
|
143 | * @method handleResponse
|
144 | * @param {Number} status
|
145 | * @param {Object} headers
|
146 | * @param {Object} payload
|
147 | * @return {Object | AdapterError} response
|
148 | */
|
149 | handleResponse(
|
150 | status: number,
|
151 | headers: AnyObject,
|
152 | payload: ActiveModelPayload,
|
153 | requestData: AnyObject | AdapterError
|
154 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
155 | ): any {
|
156 | if (this.isInvalid(status, headers, payload)) {
|
157 | const errors = errorsHashToArray(payload.errors);
|
158 |
|
159 | return new InvalidError(errors);
|
160 | } else {
|
161 | return super.handleResponse(status, headers, payload, requestData);
|
162 | }
|
163 | }
|
164 | }
|