/**
# Overview
This package provides the `DataAdapter` which the [Ember Inspector](https://github.com/emberjs/ember-inspector)
uses to subscribe and retrieve information for the `data` tab in the inspector.
This package adds roughly .6 KB when minified and compressed to your application in production; however,
you can opt out of shipping this addon in production via options in `ember-cli-build.js`
```js
let app = new EmberApp(defaults, {
emberData: {
includeDataAdapterInProduction: false
}
});
```
When using `ember-data` as a dependency of your app, the default is to ship the inspector support to production.
When not using `ember-data` as a dependency but instead using EmberData via declaring specific `@ember-data/<package>`
dependencies the default is to not ship to production.
@module @ember-data/debug
@main @ember-data/debug
@public
*/
import { addObserver, removeObserver } from '@ember/object/observers';
import { inject as service } from '@ember/service';
import { A } from '@ember/array';
import DataAdapter from '@ember/debug/data-adapter';
import { capitalize, underscore } from '@ember/string';
import { assert } from '@ember/debug';
import { get } from '@ember/object';
import { typesMapFor } from './setup';
/**
Implements `@ember/debug/data-adapter` with for EmberData
integration with the ember-inspector.
@class InspectorDataAdapter
@extends DataAdapter
@private
*/
export default DataAdapter.extend({
store: service('store'),
/**
Specifies how records can be filtered based on the state of the record
Records returned will need to have a `filterValues`
property with a key for every name in the returned array
@method getFilters
@private
@return {Array} List of objects defining filters
The object should have a `name` and `desc` property
*/
getFilters() {
return [
{ name: 'isNew', desc: 'New' },
{ name: 'isModified', desc: 'Modified' },
{ name: 'isClean', desc: 'Clean' },
];
},
_nameToClass(type) {
return get(this, 'store').modelFor(type);
},
/**
Fetch the model types and observe them for changes.
Maintains the list of model types without needing the Model package for detection.
@method watchModelTypes
@public
@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) {
const store = get(this, 'store');
const __createRecordData = store._createRecordData;
const _releaseMethods = [];
const discoveredTypes = typesMapFor(store);
// Add any models that were added during initialization of the app, before the inspector was opened
discoveredTypes.forEach((_, type) => {
this.watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, _releaseMethods);
});
// Overwrite _createRecordData so newly added models will get added to the list
store._createRecordData = identifier => {
this.watchTypeIfUnseen(store, discoveredTypes, identifier.type, typesAdded, typesUpdated, _releaseMethods);
return __createRecordData.call(store, identifier);
};
let release = () => {
_releaseMethods.forEach(fn => fn());
store._createRecordData = __createRecordData;
// reset the list so the models can be added if the inspector is re-opened
// the entries are set to false instead of removed, since the models still exist in the app
// we just need the inspector to become aware of them
discoveredTypes.forEach((value, key) => {
discoveredTypes.set(key, false);
});
this.releaseMethods.removeObject(release);
};
this.releaseMethods.pushObject(release);
return release;
},
/**
* Loop over the discovered types and use the callbacks from watchModelTypes to notify
* the consumer of this adapter about the mdoels.
*
* @param {store} store
* @internal
* @param {Map} discoveredTypes
* @param {String} type
* @param {Function} typesAdded
* @param {Function} typesUpdated
* @param {Array} releaseMethods
*/
watchTypeIfUnseen(store, discoveredTypes, type, typesAdded, typesUpdated, releaseMethods) {
if (discoveredTypes.get(type) !== true) {
let klass = store.modelFor(type);
let wrapped = this.wrapModelType(klass, type);
releaseMethods.push(this.observeModelType(type, typesUpdated));
typesAdded([wrapped]);
discoveredTypes.set(type, true);
}
},
/**
Creates a human readable string used for column headers
@method columnNameToDesc
@private
@param {String} name The attribute name
@return {String} Human readable string based on the attribute name
*/
columnNameToDesc(name) {
return capitalize(
underscore(name)
.replace(/_/g, ' ')
.trim()
);
},
/**
Get the columns for a given model type
@method columnsForType
@private
@param {Model} typeClass
@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(typeClass) {
let columns = [
{
name: 'id',
desc: 'Id',
},
];
let count = 0;
let self = this;
get(typeClass, 'attributes').forEach((meta, name) => {
if (count++ > self.attributeLimit) {
return false;
}
let desc = this.columnNameToDesc(name);
columns.push({ name: name, desc: desc });
});
return columns;
},
/**
Fetches all loaded records for a given type
@method getRecords
@internal
@param {Model} modelClass of the record
@param {String} modelName of the record
@return {Array} An array of Model records
This array will be observed for changes,
so it should update when new records are added/removed
*/
getRecords(modelClass, modelName) {
if (arguments.length < 2) {
// Legacy Ember.js < 1.13 support
let containerKey = modelClass._debugContainerKey;
if (containerKey) {
let match = containerKey.match(/model:(.*)/);
if (match !== null) {
modelName = match[1];
}
}
}
assert('Cannot find model name. Please upgrade to Ember.js >= 1.13 for Ember Inspector support', !!modelName);
return this.get('store').peekAll(modelName);
},
/**
Gets the values for each column
This is the attribute values for a given record
@method getRecordColumnValues
@private
@param {Model} record to get values from
@return {Object} Keys should match column names defined by the model type
*/
getRecordColumnValues(record) {
let count = 0;
let columnValues = { id: get(record, 'id') };
record.eachAttribute(key => {
if (count++ > this.attributeLimit) {
return false;
}
columnValues[key] = get(record, key);
});
return columnValues;
},
/**
Returns keywords to match when searching records
@method getRecordKeywords
@private
@param {Model} record
@return {Array} Relevant keywords for search based on the record's attribute values
*/
getRecordKeywords(record) {
let keywords = [];
let keys = A(['id']);
record.eachAttribute(key => keys.push(key));
keys.forEach(key => keywords.push(get(record, key)));
return keywords;
},
/**
Returns the values of filters defined by `getFilters`
These reflect the state of the record
@method getRecordFilterValues
@private
@param {Model} record
@return {Object} The record state filter values
*/
getRecordFilterValues(record) {
return {
isNew: record.get('isNew'),
isModified: record.get('hasDirtyAttributes') && !record.get('isNew'),
isClean: !record.get('hasDirtyAttributes'),
};
},
/**
Returns a color that represents the record's state
Possible colors: black, blue, green
@method getRecordColor
@private
@param {Model} record
@return {String} The record color
*/
getRecordColor(record) {
let color = 'black';
if (record.get('isNew')) {
color = 'green';
} else if (record.get('hasDirtyAttributes')) {
color = 'blue';
}
return color;
},
/**
Observes all relevant properties and re-sends the wrapped record
when a change occurs
@method observeRecord
@internal
@param {Model} record
@param {Function} recordUpdated Callback used to notify changes
@return {Function} The function to call to remove all observers
*/
observeRecord(record, recordUpdated) {
let releaseMethods = A();
let keysToObserve = A(['id', 'isNew', 'hasDirtyAttributes']);
record.eachAttribute(key => keysToObserve.push(key));
let adapter = this;
keysToObserve.forEach(function(key) {
let handler = function() {
recordUpdated(adapter.wrapRecord(record));
};
addObserver(record, key, handler);
releaseMethods.push(function() {
removeObserver(record, key, handler);
});
});
let release = function() {
releaseMethods.forEach(fn => fn());
};
return release;
},
});