UNPKG

@bigtest/mirage

Version:

A client-side server to develop, test and prototype your app.

494 lines (424 loc) 13.9 kB
import Model from './orm/model'; import Collection from './orm/collection'; import PolymorphicCollection from './orm/polymorphic-collection'; import extend from './utils/extend'; import { singularize, pluralize, camelize } from '@bigtest/mirage'; import _isFunction from 'lodash/isFunction'; import _isArray from 'lodash/isArray'; import _isEmpty from 'lodash/isEmpty'; import _includes from 'lodash/includes'; import _assign from 'lodash/assign'; import _get from 'lodash/get'; import _ from 'lodash'; class Serializer { constructor(registry, type, request) { this.registry = registry; this.type = type; this.request = request; } /** * Override this method to implement your own custom * serialize function. `primaryResource` is whatever was returned * from your route handler, and request is the Pretender * request object. Returns a plain JavaScript object or * array, which Mirage uses as the response data to your * app’s XHR request. You can also override this method, * call super, and manipulate the data before Mirage responds * with it. This is a great place to add metadata, or for * one-off operations that don’t fit neatly into any of * Mirage’s other abstractions. * @method serialize * @param response * @param request * @public */ serialize(primaryResource /* , request */) { return this.buildPayload(primaryResource); } buildPayload(primaryResource, toInclude, didSerialize, json) { if (!primaryResource && _isEmpty(toInclude)) { return json; } else if (primaryResource) { let [resourceHash, newIncludes] = this.getHashForPrimaryResource(primaryResource); let newDidSerialize = (this.isCollection(primaryResource) ? primaryResource.models : [primaryResource]); return this.buildPayload(undefined, newIncludes, newDidSerialize, resourceHash); } else { let nextIncludedResource = toInclude.shift(); let [resourceHash, newIncludes] = this.getHashForIncludedResource(nextIncludedResource); let newToInclude = newIncludes .filter((resource) => { return !_includes(didSerialize.map((m) => m.toString()), resource.toString()); }) .concat(toInclude); let newDidSerialize = (this.isCollection(nextIncludedResource) ? nextIncludedResource.models : [nextIncludedResource]) .concat(didSerialize); let newJson = this.mergePayloads(json, resourceHash); return this.buildPayload(undefined, newToInclude, newDidSerialize, newJson); } } getHashForPrimaryResource(resource) { let [hash, addToIncludes] = this.getHashForResource(resource); let hashWithRoot; if (this.root) { let serializer = this.serializerFor(resource.modelName); let rootKey = serializer.keyForResource(resource); hashWithRoot = { [rootKey]: hash }; } else { hashWithRoot = hash; } return [hashWithRoot, addToIncludes]; } getHashForIncludedResource(resource) { let hashWithRoot, addToIncludes; if (resource instanceof PolymorphicCollection) { hashWithRoot = {}; addToIncludes = resource.models; } else { let serializer = this.serializerFor(resource.modelName); let [ hash, newModels ] = serializer.getHashForResource(resource); // Included resources always have a root, and are always pushed to an array. let rootKey = serializer.keyForRelationship(resource.modelName); hashWithRoot = _isArray(hash) ? { [rootKey]: hash } : { [rootKey]: [hash] }; addToIncludes = newModels; } return [ hashWithRoot, addToIncludes ]; } getHashForResource(resource, removeForeignKeys = false, didSerialize = {}, lookupSerializer = false) { let hash; let serializer = lookupSerializer ? this.serializerFor(resource.modelName) : this; // this is used for embedded responses if (this.isModel(resource)) { hash = serializer._hashForModel(resource, removeForeignKeys, didSerialize); } else { hash = resource.models.map((m) => serializer._hashForModel(m, removeForeignKeys, didSerialize)); } if (this.embed) { return [hash]; } else { let addToIncludes = _(serializer.getKeysForIncluded()) .map((key) => { if (this.isCollection(resource)) { return resource.models.map((m) => m[key]); } else { return resource[key]; } }) .flatten() .compact() .uniqBy(m => m.toString()) .value(); return [hash, addToIncludes]; } } /* Merges new resource hash into json. If json already has root key, pushes value of resourceHash onto that key. For example, json = { post: { id: 1, title: 'Lorem Ipsum', comment_ids: [1, 3] }, comments: [ { id: 1, text: 'foo' } ] }; resourceHash = { comments: [ { id: 2, text: 'bar' } ] }; would yield { post: { id: 1, title: 'Lorem Ipsum', comment_ids: [1, 3] }, comments: [ { id: 1, text: 'foo' }, { id: 2, text: 'bar' } ] }; */ mergePayloads(json, resourceHash) { let newJson; let [resourceHashKey] = Object.keys(resourceHash); if (json[resourceHashKey]) { newJson = json; newJson[resourceHashKey] = json[resourceHashKey].concat(resourceHash[resourceHashKey]); } else { newJson = _assign(json, resourceHash); } return newJson; } keyForResource(resource) { let { modelName } = resource; return this.isModel(resource) ? this.keyForModel(modelName) : this.keyForCollection(modelName); } /** * Used to define a custom key when serializing a * primary model of modelName `modelName`. * @method keyForModel * @param modelName * @public */ keyForModel(modelName) { return camelize(modelName); } /** * Used to customize the key when serializing a primary * collection. By default this pluralizes the return * value of `keyForModel`. * @method keyForCollection * @param modelName * @public */ keyForCollection(modelName) { return pluralize(this.keyForModel(modelName)); } _hashForModel(model, removeForeignKeys, didSerialize = {}) { let attrs = this._attrsForModel(model); if (removeForeignKeys) { model.fks.forEach((fk) => { delete attrs[fk]; }); } if (this.embed) { let newDidSerialize = _assign({}, didSerialize); newDidSerialize[model.modelName] = newDidSerialize[model.modelName] || {}; newDidSerialize[model.modelName][model.id] = true; this.getKeysForIncluded().forEach((key) => { let associatedResource = model[key]; if (!_get(newDidSerialize, `${associatedResource.modelName}.${associatedResource.id}`)) { let [ associatedResourceHash ] = this.getHashForResource(associatedResource, true, newDidSerialize, true); let formattedKey = this.keyForEmbeddedRelationship(key); attrs[formattedKey] = associatedResourceHash; if (this.isModel(associatedResource)) { let fk = `${camelize(key)}Id`; delete attrs[fk]; } } }); return attrs; } else { return this._maybeAddAssociationIds(model, attrs); } } /** * @method _attrsForModel * @param model * @private */ _attrsForModel(model) { let attrs = {}; if (this.attrs) { attrs = this.attrs.reduce((memo, attr) => { memo[attr] = model[attr]; return memo; }, {}); } else { attrs = _assign(attrs, model.attrs); } // Remove fks model.fks.forEach(key => delete attrs[key]); return this._formatAttributeKeys(attrs); } /** * @method _maybeAddAssociationIds * @param model * @param attrs * @private */ _maybeAddAssociationIds(model, attrs) { let newHash = _assign({}, attrs); if (this.serializeIds === 'always') { model.associationKeys.forEach((key) => { let association = model[key]; if (this.isCollection(association)) { let formattedKey = this.keyForRelationshipIds(key); newHash[formattedKey] = model[key].models.map((m) => m.id); } else if (association) { let formattedKey = this.keyForForeignKey(key); newHash[formattedKey] = model[`${key}Id`]; } }); } else if (this.serializeIds === 'included') { this.getKeysForIncluded().forEach((key) => { let association = model[key]; if (model.associationFor(key).isPolymorphic) { if (association instanceof PolymorphicCollection) { let formattedKey = this.keyForRelationship(key); newHash[formattedKey] = model[`${singularize(key)}Ids`]; } else if (association instanceof Collection) { let formattedKey = this.keyForRelationshipIds(key); newHash[formattedKey] = model[key].models.map((m) => m.id); } else { let formattedTypeKey = this.keyForPolymorphicForeignKeyType(key); let formattedIdKey = this.keyForPolymorphicForeignKeyId(key); newHash[formattedTypeKey] = model[`${key}Id`].type; newHash[formattedIdKey] = model[`${key}Id`].id; } } else { if (this.isCollection(association)) { let formattedKey = this.keyForRelationshipIds(key); newHash[formattedKey] = model[key].models.map((m) => m.id); } else if (association) { let formattedKey = this.keyForForeignKey(key); newHash[formattedKey] = model[`${key}Id`]; } } }); } return newHash; } /** * Used to customize how a model’s attribute is * formatted in your JSON payload. * @method keyForAttribute * @param attr * @public */ keyForAttribute(attr) { return attr; } /** * Use this hook to format the key for collections * related to this model. * * For example, if you're serializing an author that * side loads many `blogPosts`, you would get `blogPost` * as an argument, and whatever you return would * end up as the collection key in your JSON: * * keyForRelationship(type) { * return dasherize(type); * } * * { * author: {...}, * 'blog-posts': [...] * } * @method keyForRelationship * @param modelName * @public */ keyForRelationship(modelName) { return camelize(pluralize(modelName)); } /** * @method keyForEmbeddedRelationship * @param attributeName * @public */ keyForEmbeddedRelationship(attributeName) { return camelize(attributeName); } /** * Use this hook to format the key for relationship ids * in this model's JSON representation. * * For example, if you're serializing an author that * side loads many `blogPosts`, you would get `blogPost` * as an argument, and whatever you return would * end up as part of the `author` JSON: * * keyForRelationshipIds(type) { * return dasherize(type) + '-ids'; * } * * { * author: { * ..., * 'blog-post-ids': [1, 2, 3] * }, * 'blog-posts': [...] * } * @method keyForRelationshipIds * @param modelName * @public */ keyForRelationshipIds(relationshipName) { return `${singularize(camelize(relationshipName))}Ids`; } keyForForeignKey(relationshipName) { return `${camelize(relationshipName)}Id`; } keyForPolymorphicForeignKeyId(relationshipName) { return `${camelize(relationshipName)}Id`; } keyForPolymorphicForeignKeyType(relationshipName) { return `${camelize(relationshipName)}Type`; } /** * This method is used by the POST and PUT shorthands. These shorthands * expect a valid JSON:API document as part of the request, so that * they know how to create or update the appropriate resouce. The normalize * method allows you to transform your request body into a JSON:API * document, which lets you take advantage of the shorthands when you * otherwise may not be able to. * * Note that this method is a noop if you’re using JSON:API already, * since request payloads sent along with POST and PUT requests will * already be in the correct format. * @method normalize * @param json * @public */ normalize(json) { return json; } /** * @method isModel * @param object * @return {Boolean} * @public */ isModel(object) { return object instanceof Model; } /** * @method isCollection * @param object * @return {Boolean} * @public */ isCollection(object) { return (object instanceof Collection) || (object instanceof PolymorphicCollection); } /** * @method isModelOrCollection * @param object * @return {Boolean} * @public */ isModelOrCollection(object) { return this.isModel(object) || this.isCollection(object); } /** * @method serializerFor * @param type * @public */ serializerFor(type) { return this.registry.serializerFor(type); } getKeysForIncluded() { return _isFunction(this.include) ? this.include(this.request) : this.include; } get schema() { return this.registry.schema; } /** * @method _formatAttributeKeys * @param attrs * @private */ _formatAttributeKeys(attrs) { let formattedAttrs = {}; for (let key in attrs) { let formattedKey = this.keyForAttribute(key); formattedAttrs[formattedKey] = attrs[key]; } return formattedAttrs; } } // Defaults Serializer.prototype.include = []; Serializer.prototype.root = true; Serializer.prototype.embed = false; Serializer.prototype.serializeIds = 'included'; // can be 'included', 'always', or 'never' Serializer.extend = extend; export default Serializer;