UNPKG

active-model-adapter

Version:
335 lines (288 loc) 9.33 kB
/* 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 { @service 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; } }