active-model-adapter
Version:
Adapters and Serializers for Rails's ActiveModel::Serializers
335 lines (288 loc) • 9.33 kB
text/typescript
/* 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 */
import RESTSerializer from '@ember-data/serializer/rest';
import type Store from '@ember-data/store';
import type Model from '@ember-data/model';
import { singularize, pluralize } from 'ember-inflector';
import { classify, decamelize, camelize, underscore } from '@ember/string';
import { inject as service } from '@ember/service';
import { dasherize } from '@ember/string';
import { isNone } from '@ember/utils';
import type { AnyObject } from './index.ts';
import type { Snapshot } from '@ember-data/legacy-compat';
import type ModelRegistry from 'ember-data/types/registries/model';
type ModelKeys<K> = Exclude<keyof K, keyof Model>;
type RelationshipsFor<K extends keyof ModelRegistry> = ModelKeys<
ModelRegistry[K]
>;
interface RelationshipMetaOptions {
async?: boolean; // unspecified defaults relationship to "true"
inverse?: string; // unspecified defaults to a lookup, which could be null but could find an inverse
polymorphic?: boolean; // unspecified defaults to false
[k: string]: unknown;
}
interface RelationshipMeta<K extends keyof ModelRegistry> {
key: RelationshipsFor<K>;
kind: 'belongsTo' | 'hasMany';
type: keyof ModelRegistry;
options: RelationshipMetaOptions;
name: RelationshipsFor<K>;
isRelationship: true;
}
interface Payload {
[key: string]: unknown;
}
/**
@module ember-data
*/
type RelationshipKind = 'belongsTo' | 'hasMany';
/**
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]
}]
}
```
*/
export default class ActiveModelSerializer extends RESTSerializer {
declare store: Store;
// SERIALIZE
/**
Converts camelCased attributes to underscored when serializing.
*/
keyForAttribute(attr: string): string {
return decamelize(attr);
}
/**
Underscores relationship names and appends "_id" or "_ids" when serializing
relationship keys.
*/
keyForRelationship(relationshipModelName: string, kind?: string): string {
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: string, _relationshipKind: RelationshipKind): string {
return camelize(key);
}
/*
Does not serialize hasMany relationships by default.
*/
serializeHasMany() {}
/**
Underscores the JSON root keys when serializing.
*/
payloadKeyFromModelName(modelName: string | number): string {
return underscore(decamelize(modelName as string));
}
/**
Serializes a polymorphic type as a fully capitalized model name.
*/
serializePolymorphicType<K extends keyof ModelRegistry>(
snapshot: Snapshot<K>,
json: Payload,
relationship: RelationshipMeta<K>
): void {
const key = relationship.key as string;
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: Model, hash: AnyObject, prop: string): AnyObject {
this.normalizeLinks(hash);
return super.normalize(typeClass, hash, prop);
}
/**
Convert `snake_cased` links to `camelCase`
*/
normalizeLinks(data: any): void {
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: string,
relationshipType: RelationshipKind
): string {
if (relationshipType === 'hasMany') {
return underscore(pluralize(key));
} else {
return underscore(singularize(key));
}
}
extractRelationships(modelClass: Model, resourceHash: AnyObject): AnyObject {
modelClass.eachRelationship<Model>(
(key: string, relationshipMeta: Record<string, any>) => {
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<K extends keyof ModelRegistry>(key: K): string {
const convertedFromRubyModule = singularize((key as string).replace('::', '/'));
return dasherize(convertedFromRubyModule);
}
}
function extractPolymorphicRelationships(
key: string,
_relationshipMeta: any,
resourceHash: any,
relationshipKey: string
) {
const polymorphicKey = decamelize(key);
const hash = resourceHash[polymorphicKey];
if (hash !== null && typeof hash === 'object') {
resourceHash[relationshipKey] = hash;
}
}