active-model-adapter
Version:
Adapters and Serializers for Rails's ActiveModel::Serializers
258 lines (228 loc) • 7.69 kB
JavaScript
import RESTSerializer from '@ember-data/serializer/rest';
import { singularize, pluralize } from 'ember-inflector';
import { decamelize, camelize, underscore, classify, dasherize } from '@ember/string';
import { inject } from '@ember/service';
import { isNone } from '@ember/utils';
import { g, i } from 'decorator-transforms/runtime-esm';
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
/**
The ActiveModelSerializer is a subclass of the RESTSerializer designed to integrate
with a JSON API that uses an underscored naming convention instead of camelCasing.
It has been designed to work out of the box with the
[active\_model\_serializers](http://github.com/rails-api/active_model_serializers)
Ruby gem. This Serializer expects specific settings using ActiveModel::Serializers,
`embed :ids, embed_in_root: true` which sideloads the records.
This serializer extends the DS.RESTSerializer by making consistent
use of the camelization, decamelization and pluralization methods to
normalize the serialized JSON into a format that is compatible with
a conventional Rails backend and Ember Data.
## JSON Structure
The ActiveModelSerializer expects the JSON returned from your server
to follow the REST adapter conventions substituting underscored keys
for camelcased ones.
### Conventional Names
Attribute names in your JSON payload should be the underscored versions of
the attributes in your Ember.js models.
For example, if you have a `Person` model:
```javascript
export default class Person extends Model {
@attr() firstName;
@attr() lastName;
@belongsTo('occupation') occupation;
}
```
The JSON returned should look like this:
```json
{
"famous_person": {
"id": 1,
"first_name": "Barack",
"last_name": "Obama",
"occupation": "President"
}
}
```
Let's imagine that `Occupation` is just another model:
```javascript
export default class Person extends Model {
@attr() firstName;
@attr() lastName;
@belongsTo('occupation') occupation;
}
export default class Occupation extends Model {
@attr() name;
@attr('number') salary;
@hasMany('person') people;
}
```
The JSON needed to avoid extra server calls, should look like this:
```json
{
"people": [{
"id": 1,
"first_name": "Barack",
"last_name": "Obama",
"occupation_id": 1
}],
"occupations": [{
"id": 1,
"name": "President",
"salary": 100000,
"person_ids": [1]
}]
}
```
*/
class ActiveModelSerializer extends RESTSerializer {
static {
g(this.prototype, "store", [inject]);
}
#store = (i(this, "store"), void 0);
// SERIALIZE
/**
Converts camelCased attributes to underscored when serializing.
*/
keyForAttribute(attr) {
return decamelize(attr);
}
/**
Underscores relationship names and appends "_id" or "_ids" when serializing
relationship keys.
*/
keyForRelationship(relationshipModelName, kind) {
const key = decamelize(relationshipModelName);
if (kind === 'belongsTo') {
return key + '_id';
} else if (kind === 'hasMany') {
return singularize(key) + '_ids';
} else {
return key;
}
}
/**
`keyForLink` can be used to define a custom key when deserializing link
properties. The `ActiveModelSerializer` camelizes link keys by default.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
keyForLink(key, _relationshipKind) {
return camelize(key);
}
/*
Does not serialize hasMany relationships by default.
*/
serializeHasMany() {}
/**
Underscores the JSON root keys when serializing.
*/
payloadKeyFromModelName(modelName) {
return underscore(decamelize(modelName));
}
/**
Serializes a polymorphic type as a fully capitalized model name.
*/
serializePolymorphicType(snapshot, json, relationship) {
const key = relationship.key;
const belongsTo = snapshot.belongsTo(key);
const jsonKey = underscore(key + '_type');
if (isNone(belongsTo)) {
json[jsonKey] = null;
} else {
json[jsonKey] = classify(belongsTo.modelName).replace('/', '::');
}
}
/**
Add extra step to `DS.RESTSerializer.normalize` so links are normalized.
If your payload looks like:
```json
{
"post": {
"id": 1,
"title": "Rails is omakase",
"links": { "flagged_comments": "api/comments/flagged" }
}
}
```
The normalized version would look like this
```json
{
"post": {
"id": 1,
"title": "Rails is omakase",
"links": { "flaggedComments": "api/comments/flagged" }
}
}
```
*/
normalize(typeClass, hash, prop) {
this.normalizeLinks(hash);
return super.normalize(typeClass, hash, prop);
}
/**
Convert `snake_cased` links to `camelCase`
*/
normalizeLinks(data) {
if (data.links) {
const links = data.links;
for (const link in links) {
const camelizedLink = camelize(link);
if (camelizedLink !== link) {
links[camelizedLink] = links[link];
delete links[link];
}
}
}
}
/**
* @private
*/
_keyForIDLessRelationship(key, relationshipType) {
if (relationshipType === 'hasMany') {
return underscore(pluralize(key));
} else {
return underscore(singularize(key));
}
}
extractRelationships(modelClass, resourceHash) {
modelClass.eachRelationship((key, relationshipMeta) => {
const relationshipKey = this.keyForRelationship(key, relationshipMeta.kind);
const idLessKey = this._keyForIDLessRelationship(key, relationshipMeta.kind);
// converts post to post_id, posts to post_ids
if (resourceHash[idLessKey] && typeof relationshipMeta[relationshipKey] === 'undefined') {
resourceHash[relationshipKey] = resourceHash[idLessKey];
}
// prefer the format the AMS gem expects, e.g.:
// relationship: {id: id, type: type}
if (relationshipMeta.options.polymorphic) {
extractPolymorphicRelationships(key, relationshipMeta, resourceHash, relationshipKey);
}
// If the preferred format is not found, use {relationship_name_id, relationship_name_type}
if (Object.prototype.hasOwnProperty.call(resourceHash, relationshipKey) && typeof resourceHash[relationshipKey] !== 'object') {
const polymorphicTypeKey = this.keyForRelationship(key) + '_type';
if (resourceHash[polymorphicTypeKey] && relationshipMeta.options.polymorphic) {
const id = resourceHash[relationshipKey];
const type = resourceHash[polymorphicTypeKey];
delete resourceHash[polymorphicTypeKey];
delete resourceHash[relationshipKey];
resourceHash[relationshipKey] = {
id: id,
type: type
};
}
}
}, this);
return super.extractRelationships(modelClass, resourceHash);
}
modelNameFromPayloadKey(key) {
const convertedFromRubyModule = singularize(key.replace('::', '/'));
return dasherize(convertedFromRubyModule);
}
}
function extractPolymorphicRelationships(key, _relationshipMeta, resourceHash, relationshipKey) {
const polymorphicKey = decamelize(key);
const hash = resourceHash[polymorphicKey];
if (hash !== null && typeof hash === 'object') {
resourceHash[relationshipKey] = hash;
}
}
export { ActiveModelSerializer as default };
//# sourceMappingURL=active-model-serializer.js.map