text: /**
   * The `Model` class represents a local data resource. It provides methods to
   * persist changes via the API.
   *
   * @abstract
   */
  export default class Model {
    /**
     * @param {Object} data A resource object from the API.
     * @param {Store} store The data store that this model should be persisted to.
     * @public
     */
    constructor(data = {}, store = null) {
      /**
       * The resource object from the API.
       *
       * @type {Object}
       * @public
       */
      this.data = data;
  
      /**
       * The time at which the model's data was last updated. Watching the value
       * of this property is a fast way to retain/cache a subtree if data hasn't
       * changed.
       *
       * @type {Date}
       * @public
       */
      this.freshness = new Date();
  
      /**
       * Whether or not the resource exists on the server.
       *
       * @type {Boolean}
       * @public
       */
      this.exists = false;
  
      /**
       * The data store that this resource should be persisted to.
       *
       * @type {Store}
       * @protected
       */
      this.store = store;
    }
  
    /**
     * Get the model's ID.
     *
     * @return {Integer}
     * @public
     * @final
     */
    id() {
      return this.data.id;
    }
  
    /**
     * Get one of the model's attributes.
     *
     * @param {String} attribute
     * @return {*}
     * @public
     * @final
     */
    attribute(attribute) {
      return this.data.attributes[attribute];
    }
  
    /**
     * Merge new data into this model locally.
     *
     * @param {Object} data A resource object to merge into this model
     * @public
     */
    pushData(data) {
      // Since most of the top-level items in a resource object are objects
      // (e.g. relationships, attributes), we'll need to check and perform the
      // merge at the second level if that's the case.
      for (const key in data) {
        if (typeof data[key] === 'object') {
          this.data[key] = this.data[key] || {};
  
          // For every item in a second-level object, we want to check if we've
          // been handed a Model instance. If so, we will convert it to a
          // relationship data object.
          for (const innerKey in data[key]) {
            if (data[key][innerKey] instanceof Model) {
              data[key][innerKey] = {data: Model.getIdentifier(data[key][innerKey])};
            }
            this.data[key][innerKey] = data[key][innerKey];
          }
        } else {
          this.data[key] = data[key];
        }
      }
  
      // Now that we've updated the data, we can say that the model is fresh.
      // This is an easy way to invalidate retained subtrees etc.
      this.freshness = new Date();
    }
  
    /**
     * Merge new attributes into this model locally.
     *
     * @param {Object} attributes The attributes to merge.
     * @public
     */
    pushAttributes(attributes) {
      this.pushData({attributes});
    }
  
    /**
     * Merge new attributes into this model, both locally and with persistence.
     *
     * @param {Object} attributes The attributes to save. If a 'relationships' key
     *     exists, it will be extracted and relationships will also be saved.
     * @param {Object} [options]
     * @return {Promise}
     * @public
     */
    save(attributes, options = {}) {
      const data = {
        type: this.data.type,
        id: this.data.id,
        attributes
      };
  
      // If a 'relationships' key exists, extract it from the attributes hash and
      // set it on the top-level data object instead. We will be sending this data
      // object to the API for persistence.
      if (attributes.relationships) {
        data.relationships = {};
  
        for (const key in attributes.relationships) {
          const model = attributes.relationships[key];
  
          data.relationships[key] = {
            data: model instanceof Array
              ? model.map(Model.getIdentifier)
              : Model.getIdentifier(model)
          };
        }
  
        delete attributes.relationships;
      }
  
      // Before we update the model's data, we should make a copy of the model's
      // old data so that we can revert back to it if something goes awry during
      // persistence.
      const oldData = this.copyData();
  
      this.pushData(data);
  
      return app.request(Object.assign({
        method: this.exists ? 'PATCH' : 'POST',
        url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
        data: {data}
      }, options)).then(
        // If everything went well, we'll make sure the store knows that this
        // model exists now (if it didn't already), and we'll push the data that
        // the API returned into the store.
        payload => {
          this.store.data[payload.data.type] = this.store.data[payload.data.type] || {};
          this.store.data[payload.data.type][payload.data.id] = this;
          return this.store.pushPayload(payload);
        },
  
        // If something went wrong, though... good thing we backed up our model's
        // old data! We'll revert to that and let others handle the error.
        response => {
          this.pushData(oldData);
          m.lazyRedraw();
          throw response;
        }
      );
    }
  
    /**
     * Send a request to delete the resource.
     *
     * @param {Object} data Data to send along with the DELETE request.
     * @param {Object} [options]
     * @return {Promise}
     * @public
     */
    delete(data, options = {}) {
      if (!this.exists) return m.deferred.resolve().promise;
  
      return app.request(Object.assign({
        method: 'DELETE',
        url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
        data
      }, options)).then(() => {
        this.exists = false;
        this.store.remove(this);
      });
    }
  
    /**
     * Construct a path to the API endpoint for this resource.
     *
     * @return {String}
     * @protected
     */
    apiEndpoint() {
      return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
    }
  
    copyData() {
      return JSON.parse(JSON.stringify(this.data));
    }
  
    /**
     * Generate a function which returns the value of the given attribute.
     *
     * @param {String} name
     * @param {function} [transform] A function to transform the attribute value
     * @return {*}
     * @public
     */
    static attribute(name, transform) {
      return function() {
        const value = this.data.attributes && this.data.attributes[name];
  
        return transform ? transform(value) : value;
      };
    }
  
    /**
     * Generate a function which returns the value of the given has-one
     * relationship.
     *
     * @param {String} name
     * @return {Model|Boolean|undefined} false if no information about the
     *     relationship exists; undefined if the relationship exists but the model
     *     has not been loaded; or the model if it has been loaded.
     * @public
     */
    static hasOne(name) {
      return function() {
        if (this.data.relationships) {
          const relationship = this.data.relationships[name];
  
          if (relationship) {
            return app.store.getById(relationship.data.type, relationship.data.id);
          }
        }
  
        return false;
      };
    }
  
    /**
     * Generate a function which returns the value of the given has-many
     * relationship.
     *
     * @param {String} name
     * @return {Array|Boolean} false if no information about the relationship
     *     exists; an array if it does, containing models if they have been
     *     loaded, and undefined for those that have not.
     * @public
     */
    static hasMany(name) {
      return function() {
        if (this.data.relationships) {
          const relationship = this.data.relationships[name];
  
          if (relationship) {
            return relationship.data.map(data => app.store.getById(data.type, data.id));
          }
        }
  
        return false;
      };
    }
  
    /**
     * Transform the given value into a Date object.
     *
     * @param {String} value
     * @return {Date|null}
     * @public
     */
    static transformDate(value) {
      return value ? new Date(value) : null;
    }
  
    /**
     * Get a resource identifier object for the given model.
     *
     * @param {Model} model
     * @return {Object}
     * @protected
     */
    static getIdentifier(model) {
      return {
        type: model.data.type,
        id: model.data.id
      };
    }
  }
  