Skip to content

Commit

Permalink
[BUGFIX release] Refactor Model.reopen to use mixins
Browse files Browse the repository at this point in the history
This fixes bugs where users import Model form 'ember-data/model' and it was missing required methods
  • Loading branch information
bmac committed Jan 18, 2016
1 parent 9fefef6 commit ebd7e02
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 232 deletions.
6 changes: 2 additions & 4 deletions addon/-private/system/debug/debug-info.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Model from "ember-data/model";
import Ember from "ember";

Model.reopen({
export default Ember.Mixin.create({

/**
Provides info about the model for debugging purposes
Expand Down Expand Up @@ -63,5 +63,3 @@ Model.reopen({
};
}
});

export default Model;
226 changes: 226 additions & 0 deletions addon/-private/system/model/attr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import Ember from 'ember';
import { assert } from "ember-data/-private/debug";


var get = Ember.get;
var Map = Ember.Map;

/**
@module ember-data
*/

/**
@class Model
@namespace DS
*/

export const AttrClassMethodsMixin = Ember.Mixin.create({
/**
A map whose keys are the attributes of the model (properties
described by DS.attr) and whose values are the meta object for the
property.
Example
```app/models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
firstName: attr('string'),
lastName: attr('string'),
birthday: attr('date')
});
```
```javascript
import Ember from 'ember';
import Person from 'app/models/person';
var attributes = Ember.get(Person, 'attributes')
attributes.forEach(function(meta, name) {
console.log(name, meta);
});
// prints:
// firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"}
// lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"}
// birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"}
```
@property attributes
@static
@type {Ember.Map}
@readOnly
*/
attributes: Ember.computed(function() {
var map = Map.create();

this.eachComputedProperty((name, meta) => {
if (meta.isAttribute) {
assert("You may not set `id` as an attribute on your model. Please remove any lines that look like: `id: DS.attr('<type>')` from " + this.toString(), name !== 'id');

meta.name = name;
map.set(name, meta);
}
});

return map;
}).readOnly(),

/**
A map whose keys are the attributes of the model (properties
described by DS.attr) and whose values are type of transformation
applied to each attribute. This map does not include any
attributes that do not have an transformation type.
Example
```app/models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
firstName: attr(),
lastName: attr('string'),
birthday: attr('date')
});
```
```javascript
import Ember from 'ember';
import Person from 'app/models/person';
var transformedAttributes = Ember.get(Person, 'transformedAttributes')
transformedAttributes.forEach(function(field, type) {
console.log(field, type);
});
// prints:
// lastName string
// birthday date
```
@property transformedAttributes
@static
@type {Ember.Map}
@readOnly
*/
transformedAttributes: Ember.computed(function() {
var map = Map.create();

this.eachAttribute((key, meta) => {
if (meta.type) {
map.set(key, meta.type);
}
});

return map;
}).readOnly(),

/**
Iterates through the attributes of the model, calling the passed function on each
attribute.
The callback method you provide should have the following signature (all
parameters are optional):
```javascript
function(name, meta);
```
- `name` the name of the current property in the iteration
- `meta` the meta object for the attribute property in the iteration
Note that in addition to a callback, you can also pass an optional target
object that will be set as `this` on the context.
Example
```javascript
import DS from 'ember-data';
var Person = DS.Model.extend({
firstName: attr('string'),
lastName: attr('string'),
birthday: attr('date')
});
Person.eachAttribute(function(name, meta) {
console.log(name, meta);
});
// prints:
// firstName {type: "string", isAttribute: true, options: Object, parentType: function, name: "firstName"}
// lastName {type: "string", isAttribute: true, options: Object, parentType: function, name: "lastName"}
// birthday {type: "date", isAttribute: true, options: Object, parentType: function, name: "birthday"}
```
@method eachAttribute
@param {Function} callback The callback to execute
@param {Object} [binding] the value to which the callback's `this` should be bound
@static
*/
eachAttribute(callback, binding) {
get(this, 'attributes').forEach((meta, name) => {
callback.call(binding, name, meta);
});
},

/**
Iterates through the transformedAttributes of the model, calling
the passed function on each attribute. Note the callback will not be
called for any attributes that do not have an transformation type.
The callback method you provide should have the following signature (all
parameters are optional):
```javascript
function(name, type);
```
- `name` the name of the current property in the iteration
- `type` a string containing the name of the type of transformed
applied to the attribute
Note that in addition to a callback, you can also pass an optional target
object that will be set as `this` on the context.
Example
```javascript
import DS from 'ember-data';
var Person = DS.Model.extend({
firstName: attr(),
lastName: attr('string'),
birthday: attr('date')
});
Person.eachTransformedAttribute(function(name, type) {
console.log(name, type);
});
// prints:
// lastName string
// birthday date
```
@method eachTransformedAttribute
@param {Function} callback The callback to execute
@param {Object} [binding] the value to which the callback's `this` should be bound
@static
*/
eachTransformedAttribute(callback, binding) {
get(this, 'transformedAttributes').forEach((type, name) => {
callback.call(binding, name, type);
});
}
});


export const AttrInstanceMethodsMixin = Ember.Mixin.create({
eachAttribute(callback, binding) {
this.constructor.eachAttribute(callback, binding);
}
});
16 changes: 15 additions & 1 deletion addon/-private/system/model/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { assert, deprecate } from "ember-data/-private/debug";
import { PromiseObject } from "ember-data/-private/system/promise-proxies";
import Errors from "ember-data/-private/system/model/errors";
import isEnabled from 'ember-data/-private/features';
import DebuggerInfoMixin from 'ember-data/-private/system/debug/debug-info';
import { BelongsToMixin } from 'ember-data/-private/system/relationships/belongs-to';
import { HasManyMixin } from 'ember-data/-private/system/relationships/has-many';
import { DidDefinePropertyMixin, RelationshipsClassMethodsMixin, RelationshipsInstanceMethodsMixin } from 'ember-data/-private/system/relationships/ext';
import { AttrClassMethodsMixin, AttrInstanceMethodsMixin } from 'ember-data/-private/system/model/attr';

/**
@module ember-data
Expand Down Expand Up @@ -972,4 +977,13 @@ if (isEnabled("ds-references")) {

}

export default Model;
Model.reopenClass(RelationshipsClassMethodsMixin);
Model.reopenClass(AttrClassMethodsMixin);

export default Model.extend(
DebuggerInfoMixin,
BelongsToMixin,
DidDefinePropertyMixin,
RelationshipsInstanceMethodsMixin,
HasManyMixin,
AttrInstanceMethodsMixin);
3 changes: 1 addition & 2 deletions addon/-private/system/relationships/belongs-to.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Ember from 'ember';
import { assert, warn } from "ember-data/-private/debug";
import Model from 'ember-data/model';
import normalizeModelName from "ember-data/-private/system/normalize-model-name";

/**
Expand Down Expand Up @@ -138,7 +137,7 @@ export default function belongsTo(modelName, options) {
These observers observe all `belongsTo` relationships on the record. See
`relationships/ext` to see how these observers get their dependencies.
*/
Model.reopen({
export const BelongsToMixin = Ember.Mixin.create({
notifyBelongsToChanged(key) {
this.notifyPropertyChange(key);
}
Expand Down
7 changes: 3 additions & 4 deletions addon/-private/system/relationships/ext.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
typeForRelationshipMeta,
relationshipFromMeta
} from "ember-data/-private/system/relationship-meta";
import Model from "ember-data/model";
import EmptyObject from "ember-data/-private/system/empty-object";

var get = Ember.get;
Expand Down Expand Up @@ -98,7 +97,7 @@ var relationshipsByNameDescriptor = Ember.computed(function() {
@class Model
@namespace DS
*/
Model.reopen({
export const DidDefinePropertyMixin = Ember.Mixin.create({

/**
This Ember.js hook allows an object to be notified when a property
Expand Down Expand Up @@ -157,7 +156,7 @@ Model.reopen({
extensively.
*/

Model.reopenClass({
export const RelationshipsClassMethodsMixin = Ember.Mixin.create({

/**
For a given relationship name, returns the model type of the relationship.
Expand Down Expand Up @@ -599,7 +598,7 @@ Model.reopenClass({

});

Model.reopen({
export const RelationshipsInstanceMethodsMixin = Ember.Mixin.create({
/**
Given a callback, iterates over each of the relationships in the model,
invoking the callback with the name of each relationship and its relationship
Expand Down
2 changes: 1 addition & 1 deletion addon/-private/system/relationships/has-many.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export default function hasMany(type, options) {
}).meta(meta);
}

Model.reopen({
export const HasManyMixin = Ember.Mixin.create({
notifyHasManyAdded(key) {
//We need to notifyPropertyChange in the adding case because we need to make sure
//we fetch the newly added record in case it is unloaded
Expand Down
Loading

0 comments on commit ebd7e02

Please sign in to comment.