API Docs for: v2.11.0-beta.8
Show:

File: packages/ember-extension-support/lib/data_adapter.js

import { getOwner } from 'ember-utils';
import { get, run } from 'ember-metal';
import {
  String as StringUtils,
  Namespace,
  Object as EmberObject,
  A as emberA,
  addArrayObserver,
  removeArrayObserver,
  objectAt
} from 'ember-runtime';
import { Application } from 'ember-application';

/**
@module ember
@submodule ember-extension-support
*/

/**
  The `DataAdapter` helps a data persistence library
  interface with tools that debug Ember such
  as the [Ember Extension](https://github.com/tildeio/ember-extension)
  for Chrome and Firefox.

  This class will be extended by a persistence library
  which will override some of the methods with
  library-specific code.

  The methods likely to be overridden are:

  * `getFilters`
  * `detect`
  * `columnsForType`
  * `getRecords`
  * `getRecordColumnValues`
  * `getRecordKeywords`
  * `getRecordFilterValues`
  * `getRecordColor`
  * `observeRecord`

  The adapter will need to be registered
  in the application's container as `dataAdapter:main`.

  Example:

  ```javascript
  Application.initializer({
    name: "data-adapter",

    initialize: function(application) {
      application.register('data-adapter:main', DS.DataAdapter);
    }
  });
  ```

  @class DataAdapter
  @namespace Ember
  @extends EmberObject
  @public
*/
export default EmberObject.extend({
  init() {
    this._super(...arguments);
    this.releaseMethods = emberA();
  },

  /**
    The container-debug-adapter which is used
    to list all models.

    @property containerDebugAdapter
    @default undefined
    @since 1.5.0
    @public
  **/
  containerDebugAdapter: undefined,

  /**
    The number of attributes to send
    as columns. (Enough to make the record
    identifiable).

    @private
    @property attributeLimit
    @default 3
    @since 1.3.0
  */
  attributeLimit: 3,

  /**
     Ember Data > v1.0.0-beta.18
     requires string model names to be passed
     around instead of the actual factories.

     This is a stamp for the Ember Inspector
     to differentiate between the versions
     to be able to support older versions too.

     @public
     @property acceptsModelName
   */
  acceptsModelName: true,

  /**
    Stores all methods that clear observers.
    These methods will be called on destruction.

    @private
    @property releaseMethods
    @since 1.3.0
  */
  releaseMethods: emberA(),

  /**
    Specifies how records can be filtered.
    Records returned will need to have a `filterValues`
    property with a key for every name in the returned array.

    @public
    @method getFilters
    @return {Array} List of objects defining filters.
     The object should have a `name` and `desc` property.
  */
  getFilters() {
    return emberA();
  },

  /**
    Fetch the model types and observe them for changes.

    @public
    @method watchModelTypes

    @param {Function} typesAdded Callback to call to add types.
    Takes an array of objects containing wrapped types (returned from `wrapModelType`).

    @param {Function} typesUpdated Callback to call when a type has changed.
    Takes an array of objects containing wrapped types.

    @return {Function} Method to call to remove all observers
  */
  watchModelTypes(typesAdded, typesUpdated) {
    let modelTypes = this.getModelTypes();
    let releaseMethods = emberA();
    let typesToSend;

    typesToSend = modelTypes.map(type => {
      let klass = type.klass;
      let wrapped = this.wrapModelType(klass, type.name);
      releaseMethods.push(this.observeModelType(type.name, typesUpdated));
      return wrapped;
    });

    typesAdded(typesToSend);

    let release = () => {
      releaseMethods.forEach((fn) => fn());
      this.releaseMethods.removeObject(release);
    };
    this.releaseMethods.pushObject(release);
    return release;
  },

  _nameToClass(type) {
    if (typeof type === 'string') {
      type = getOwner(this)._lookupFactory(`model:${type}`);
    }
    return type;
  },

  /**
    Fetch the records of a given type and observe them for changes.

    @public
    @method watchRecords

    @param {String} modelName The model name.

    @param {Function} recordsAdded Callback to call to add records.
    Takes an array of objects containing wrapped records.
    The object should have the following properties:
      columnValues: {Object} The key and value of a table cell.
      object: {Object} The actual record object.

    @param {Function} recordsUpdated Callback to call when a record has changed.
    Takes an array of objects containing wrapped records.

    @param {Function} recordsRemoved Callback to call when a record has removed.
    Takes the following parameters:
      index: The array index where the records were removed.
      count: The number of records removed.

    @return {Function} Method to call to remove all observers.
  */
  watchRecords(modelName, recordsAdded, recordsUpdated, recordsRemoved) {
    let releaseMethods = emberA();
    let klass = this._nameToClass(modelName);
    let records = this.getRecords(klass, modelName);
    let release;

    function recordUpdated(updatedRecord) {
      recordsUpdated([updatedRecord]);
    }

    let recordsToSend = records.map((record) => {
      releaseMethods.push(this.observeRecord(record, recordUpdated));
      return this.wrapRecord(record);
    });

    let contentDidChange = (array, idx, removedCount, addedCount) => {
      for (let i = idx; i < idx + addedCount; i++) {
        let record = objectAt(array, i);
        let wrapped = this.wrapRecord(record);
        releaseMethods.push(this.observeRecord(record, recordUpdated));
        recordsAdded([wrapped]);
      }

      if (removedCount) {
        recordsRemoved(idx, removedCount);
      }
    };

    let observer = { didChange: contentDidChange, willChange() { return this; } };
    addArrayObserver(records, this, observer);

    release = () => {
      releaseMethods.forEach(fn => fn());
      removeArrayObserver(records, this, observer);
      this.releaseMethods.removeObject(release);
    };

    recordsAdded(recordsToSend);

    this.releaseMethods.pushObject(release);
    return release;
  },

  /**
    Clear all observers before destruction
    @private
    @method willDestroy
  */
  willDestroy() {
    this._super(...arguments);
    this.releaseMethods.forEach(fn => fn());
  },

  /**
    Detect whether a class is a model.

    Test that against the model class
    of your persistence library.

    @private
    @method detect
    @param {Class} klass The class to test.
    @return boolean Whether the class is a model class or not.
  */
  detect(klass) {
    return false;
  },

  /**
    Get the columns for a given model type.

    @private
    @method columnsForType
    @param {Class} type The model type.
    @return {Array} An array of columns of the following format:
     name: {String} The name of the column.
     desc: {String} Humanized description (what would show in a table column name).
  */
  columnsForType(type) {
    return emberA();
  },

  /**
    Adds observers to a model type class.

    @private
    @method observeModelType
    @param {String} modelName The model type name.
    @param {Function} typesUpdated Called when a type is modified.
    @return {Function} The function to call to remove observers.
  */

  observeModelType(modelName, typesUpdated) {
    let klass = this._nameToClass(modelName);
    let records = this.getRecords(klass, modelName);

    function onChange() {
      typesUpdated([this.wrapModelType(klass, modelName)]);
    }

    let observer = {
      didChange() {
        run.scheduleOnce('actions', this, onChange);
      },
      willChange() { return this; }
    };

    addArrayObserver(records, this, observer);

    let release = () => removeArrayObserver(records, this, observer);

    return release;
  },


  /**
    Wraps a given model type and observes changes to it.

    @private
    @method wrapModelType
    @param {Class} klass A model class.
    @param {String} modelName Name of the class.
    @return {Object} Contains the wrapped type and the function to remove observers
    Format:
      type: {Object} The wrapped type.
        The wrapped type has the following format:
          name: {String} The name of the type.
          count: {Integer} The number of records available.
          columns: {Columns} An array of columns to describe the record.
          object: {Class} The actual Model type class.
      release: {Function} The function to remove observers.
  */
  wrapModelType(klass, name) {
    let records = this.getRecords(klass, name);
    let typeToSend;

    typeToSend = {
      name,
      count: get(records, 'length'),
      columns: this.columnsForType(klass),
      object: klass
    };

    return typeToSend;
  },


  /**
    Fetches all models defined in the application.

    @private
    @method getModelTypes
    @return {Array} Array of model types.
  */
  getModelTypes() {
    let containerDebugAdapter = this.get('containerDebugAdapter');
    let types;

    if (containerDebugAdapter.canCatalogEntriesByType('model')) {
      types = containerDebugAdapter.catalogEntriesByType('model');
    } else {
      types = this._getObjectsOnNamespaces();
    }

    // New adapters return strings instead of classes.
    types = emberA(types).map((name) => {
      return {
        klass: this._nameToClass(name),
        name: name
      };
    });
    types = emberA(types).filter(type => this.detect(type.klass));

    return emberA(types);
  },

  /**
    Loops over all namespaces and all objects
    attached to them.

    @private
    @method _getObjectsOnNamespaces
    @return {Array} Array of model type strings.
  */
  _getObjectsOnNamespaces() {
    let namespaces = emberA(Namespace.NAMESPACES);
    let types = emberA();

    namespaces.forEach(namespace => {
      for (let key in namespace) {
        if (!namespace.hasOwnProperty(key)) { continue; }
        // Even though we will filter again in `getModelTypes`,
        // we should not call `lookupFactory` on non-models
        // (especially when `EmberENV.MODEL_FACTORY_INJECTIONS` is `true`)
        if (!this.detect(namespace[key])) { continue; }
        let name = StringUtils.dasherize(key);
        if (!(namespace instanceof Application) && namespace.toString()) {
          name = `${namespace}/${name}`;
        }
        types.push(name);
      }
    });
    return types;
  },

  /**
    Fetches all loaded records for a given type.

    @private
    @method getRecords
    @return {Array} An array of records.
     This array will be observed for changes,
     so it should update when new records are added/removed.
  */
  getRecords(type) {
    return emberA();
  },

  /**
    Wraps a record and observers changes to it.

    @private
    @method wrapRecord
    @param {Object} record The record instance.
    @return {Object} The wrapped record. Format:
    columnValues: {Array}
    searchKeywords: {Array}
  */
  wrapRecord(record) {
    let recordToSend = { object: record };

    recordToSend.columnValues = this.getRecordColumnValues(record);
    recordToSend.searchKeywords = this.getRecordKeywords(record);
    recordToSend.filterValues = this.getRecordFilterValues(record);
    recordToSend.color = this.getRecordColor(record);

    return recordToSend;
  },

  /**
    Gets the values for each column.

    @private
    @method getRecordColumnValues
    @return {Object} Keys should match column names defined
    by the model type.
  */
  getRecordColumnValues(record) {
    return {};
  },

  /**
    Returns keywords to match when searching records.

    @private
    @method getRecordKeywords
    @return {Array} Relevant keywords for search.
  */
  getRecordKeywords(record) {
    return emberA();
  },

  /**
    Returns the values of filters defined by `getFilters`.

    @private
    @method getRecordFilterValues
    @param {Object} record The record instance.
    @return {Object} The filter values.
  */
  getRecordFilterValues(record) {
    return {};
  },

  /**
    Each record can have a color that represents its state.

    @private
    @method getRecordColor
    @param {Object} record The record instance
    @return {String} The records color.
      Possible options: black, red, blue, green.
  */
  getRecordColor(record) {
    return null;
  },

  /**
    Observes all relevant properties and re-sends the wrapped record
    when a change occurs.

    @private
    @method observerRecord
    @param {Object} record The record instance.
    @param {Function} recordUpdated The callback to call when a record is updated.
    @return {Function} The function to call to remove all observers.
  */
  observeRecord(record, recordUpdated) {
    return function() {};
  }
});