loopback-datasource-juggler/lib/model.js

392 lines
11 KiB
JavaScript
Raw Normal View History

2014-03-12 23:28:46 +00:00
/*!
2013-04-11 23:23:34 +00:00
* Module exports class Model
*/
module.exports = ModelBaseClass;
2013-04-11 23:23:34 +00:00
2014-03-12 23:28:46 +00:00
/*!
2012-03-27 14:22:24 +00:00
* Module dependencies
2011-10-10 13:22:51 +00:00
*/
2014-01-24 17:09:53 +00:00
2011-10-10 13:22:51 +00:00
var util = require('util');
var traverse = require('traverse');
2013-05-28 05:20:30 +00:00
var jutil = require('./jutil');
var List = require('./list');
var Hookable = require('./hooks');
2013-06-05 21:33:52 +00:00
var validations = require('./validations.js');
2011-10-10 13:22:51 +00:00
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text', 'ObjectID'];
2011-10-10 13:22:51 +00:00
2011-10-08 17:11:26 +00:00
/**
2014-03-12 23:28:46 +00:00
* Model class: base class for all persistent objects.
2012-03-27 14:22:24 +00:00
*
* `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
2012-03-27 14:22:24 +00:00
*
2014-03-12 23:28:46 +00:00
* @class
* @param {Object} data Initial object data
2011-10-08 17:11:26 +00:00
*/
function ModelBaseClass(data, options) {
options = options || {};
if(!('applySetters' in options)) {
// Default to true
options.applySetters = true;
}
this._initProperties(data, options);
2012-04-19 15:01:40 +00:00
}
// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing
function clone(data) {
2014-01-24 17:09:53 +00:00
/*
if(!(data instanceof ModelBaseClass)) {
if(data && (Array.isArray(data) || 'object' === typeof data)) {
return traverse(data).clone();
}
}
*/
return data;
}
/**
* Initialize the model instance with a list of properties
* @param {Object} data The data object
* @param {Object} options An object to control the instantiation
* @property {Boolean} applySetters Controls if the setters will be applied
* @property {Boolean} strict Set the instance level strict mode
* @private
*/
ModelBaseClass.prototype._initProperties = function (data, options) {
2014-01-24 17:09:53 +00:00
var self = this;
var ctor = this.constructor;
if(data instanceof ctor) {
// Convert the data to be plain object to avoid polutions
data = data.toObject(false);
}
2014-01-24 17:09:53 +00:00
var properties = ctor.definition.build();
data = data || {};
options = options || {};
var applySetters = options.applySetters;
var strict = options.strict;
if(strict === undefined) {
strict = ctor.definition.settings.strict;
}
2014-01-24 17:09:53 +00:00
Object.defineProperty(this, '__cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
Object.defineProperty(this, '__data', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
Object.defineProperty(this, '__dataWas', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
/**
* Instance level data source
*/
Object.defineProperty(this, '__dataSource', {
writable: true,
enumerable: false,
configurable: true,
value: options.dataSource
});
/**
* Instance level strict mode
*/
Object.defineProperty(this, '__strict', {
writable: true,
enumerable: false,
configurable: true,
value: strict
});
if (data.__cachedRelations) {
this.__cachedRelations = data.__cachedRelations;
2014-01-24 17:09:53 +00:00
}
for (var i in data) {
if (i in properties && typeof data[i] !== 'function') {
2014-01-24 17:09:53 +00:00
this.__data[i] = this.__dataWas[i] = clone(data[i]);
} else if (i in ctor.relations) {
2014-03-13 23:43:38 +00:00
if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) {
// If the related model is populated
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
}
2014-01-24 17:09:53 +00:00
this.__cachedRelations[i] = data[i];
} else {
if (strict === false) {
this.__data[i] = this.__dataWas[i] = clone(data[i]);
} else if (strict === 'throw') {
throw new Error('Unknown property: ' + i);
}
}
2014-01-24 17:09:53 +00:00
}
var propertyName;
2014-01-24 17:09:53 +00:00
if (applySetters === true) {
for (propertyName in data) {
if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) {
2014-01-24 17:09:53 +00:00
self[propertyName] = self.__data[propertyName] || data[propertyName];
}
}
2014-01-24 17:09:53 +00:00
}
// Set the unknown properties as properties to the object
if (strict === false) {
for (propertyName in data) {
if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) {
2014-01-24 17:09:53 +00:00
self[propertyName] = self.__data[propertyName] || data[propertyName];
}
}
2014-01-24 17:09:53 +00:00
}
2014-01-24 17:09:53 +00:00
ctor.forEachProperty(function (propertyName) {
if (undefined === self.__data[propertyName]) {
2014-01-24 17:09:53 +00:00
self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName);
} else {
self.__dataWas[propertyName] = self.__data[propertyName];
}
2014-01-24 17:09:53 +00:00
});
2014-01-24 17:09:53 +00:00
ctor.forEachProperty(function (propertyName) {
2012-05-29 11:16:24 +00:00
2014-01-24 17:09:53 +00:00
var type = properties[propertyName].type;
2011-10-11 19:51:32 +00:00
2014-01-24 17:09:53 +00:00
if (BASE_TYPES.indexOf(type.name) === -1) {
if (typeof self.__data[propertyName] !== 'object' && self.__data[propertyName]) {
try {
self.__data[propertyName] = JSON.parse(self.__data[propertyName] + '');
} catch (e) {
self.__data[propertyName] = String(self.__data[propertyName]);
2011-10-11 19:51:32 +00:00
}
2014-01-24 17:09:53 +00:00
}
if (type.name === 'Array' || Array.isArray(type)) {
if (!(self.__data[propertyName] instanceof List)
&& self.__data[propertyName] !== undefined
&& self.__data[propertyName] !== null ) {
self.__data[propertyName] = List(self.__data[propertyName], type, self);
2014-01-24 17:09:53 +00:00
}
}
2011-10-11 19:51:32 +00:00
}
2014-01-24 17:09:53 +00:00
});
function getDefault(propertyName) {
var def = properties[propertyName]['default'];
if (def !== undefined) {
if (typeof def === 'function') {
return def();
} else {
return def;
}
} else {
return undefined;
}
}
this.trigger('initialize');
};
2011-10-08 17:11:26 +00:00
2012-03-27 14:22:24 +00:00
/**
2014-03-12 23:28:46 +00:00
* Define a property on the model.
* @param {String} prop Property name
* @param {Object} params Various property configuration
2012-03-27 14:22:24 +00:00
*/
ModelBaseClass.defineProperty = function (prop, params) {
2014-01-24 17:09:53 +00:00
this.dataSource.defineProperty(this.modelName, prop, params);
2011-12-09 15:23:29 +00:00
};
ModelBaseClass.getPropertyType = function (propName) {
2014-01-24 17:09:53 +00:00
var prop = this.definition.properties[propName];
if (!prop) {
// The property is not part of the definition
return null;
}
if (!prop.type) {
throw new Error('Type not defined for property ' + this.modelName + '.' + propName);
// return null;
}
return prop.type.name;
2012-05-29 11:16:24 +00:00
};
ModelBaseClass.prototype.getPropertyType = function (propName) {
2014-01-24 17:09:53 +00:00
return this.constructor.getPropertyType(propName);
2012-01-18 15:20:05 +00:00
};
2012-03-27 14:22:24 +00:00
/**
* Return string representation of class
2014-03-12 23:28:46 +00:00
* This overrides the default `toString()` method
2012-03-27 14:22:24 +00:00
*/
ModelBaseClass.toString = function () {
2014-01-24 17:09:53 +00:00
return '[Model ' + this.modelName + ']';
2012-07-13 13:53:22 +00:00
};
2011-10-08 17:11:26 +00:00
2012-03-27 14:22:24 +00:00
/**
2014-03-12 23:28:46 +00:00
* Convert model instance to a plain JSON object.
* Returns a canonical object representation (no getters and setters).
2012-03-27 14:22:24 +00:00
*
2014-03-12 23:28:46 +00:00
* @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties.
2012-03-27 14:22:24 +00:00
*/
2014-04-11 18:39:57 +00:00
ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
if(onlySchema === undefined) {
onlySchema = true;
}
2014-01-24 17:09:53 +00:00
var data = {};
var self = this;
2014-04-11 18:39:57 +00:00
var Model = this.constructor;
2014-01-24 17:09:53 +00:00
2014-05-05 14:23:12 +00:00
// if it is already an Object
if(Model === Object) return self;
var strict = this.__strict;
2014-02-04 04:52:01 +00:00
var schemaLess = (strict === false) || !onlySchema;
2014-01-24 17:09:53 +00:00
this.constructor.forEachProperty(function (propertyName) {
if (removeHidden && Model.isHiddenProperty(propertyName)) {
return;
}
if (typeof self[propertyName] === 'function') {
return;
}
2014-05-14 20:33:46 +00:00
2014-01-24 17:09:53 +00:00
if (self[propertyName] instanceof List) {
2014-04-11 18:39:57 +00:00
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
2014-01-24 17:09:53 +00:00
} else if (self.__data.hasOwnProperty(propertyName)) {
if (self[propertyName] !== undefined && self[propertyName] !== null && self[propertyName].toObject) {
2014-04-11 18:39:57 +00:00
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
2014-01-24 17:09:53 +00:00
} else {
data[propertyName] = self[propertyName];
}
} else {
data[propertyName] = null;
}
});
var val = null;
2014-01-24 17:09:53 +00:00
if (schemaLess) {
// Find its own properties which can be set via myModel.myProperty = 'myValue'.
// If the property is not declared in the model definition, no setter will be
// triggered to add it to __data
for (var propertyName in self) {
if(removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
if(self.hasOwnProperty(propertyName) && (!data.hasOwnProperty(propertyName))) {
val = self[propertyName];
if (typeof val === 'function') {
continue;
}
if (val !== undefined && val !== null && val.toObject) {
2014-04-11 18:39:57 +00:00
data[propertyName] = val.toObject(!schemaLess, removeHidden);
} else {
data[propertyName] = val;
}
}
}
// Now continue to check __data
for (propertyName in self.__data) {
2014-01-24 17:09:53 +00:00
if (!data.hasOwnProperty(propertyName)) {
if(removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName];
if (typeof val === 'function') {
continue;
}
2014-01-24 17:09:53 +00:00
if (val !== undefined && val !== null && val.toObject) {
2014-04-11 18:39:57 +00:00
data[propertyName] = val.toObject(!schemaLess, removeHidden);
2012-09-10 15:57:21 +00:00
} else {
2014-01-24 17:09:53 +00:00
data[propertyName] = val;
}
2014-01-24 17:09:53 +00:00
}
}
2014-01-24 17:09:53 +00:00
}
2014-04-11 18:39:57 +00:00
2014-01-24 17:09:53 +00:00
return data;
2011-10-08 17:11:26 +00:00
};
2014-04-11 18:39:57 +00:00
ModelBaseClass.isHiddenProperty = function(propertyName) {
var Model = this;
var settings = Model.definition && Model.definition.settings;
var hiddenProperties = settings && settings.hidden;
if(hiddenProperties) {
return ~hiddenProperties.indexOf(propertyName);
} else {
return false;
}
}
ModelBaseClass.prototype.toJSON = function () {
2014-04-11 18:39:57 +00:00
return this.toObject(false, true);
2012-09-10 15:57:21 +00:00
};
ModelBaseClass.prototype.fromObject = function (obj) {
2014-01-24 17:09:53 +00:00
for (var key in obj) {
this[key] = obj[key];
}
};
2011-10-08 17:11:26 +00:00
/**
* Checks is property changed based on current property and initial value
2012-03-27 14:22:24 +00:00
*
2014-03-12 23:28:46 +00:00
* @param {String} propertyName Property name
2011-10-08 17:11:26 +00:00
* @return Boolean
*/
ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName) {
2014-01-24 17:09:53 +00:00
return this.__data[propertyName] !== this.__dataWas[propertyName];
2011-10-08 17:11:26 +00:00
};
2012-03-27 14:22:24 +00:00
/**
2014-03-12 23:28:46 +00:00
* Reset dirty attributes.
* This method does not perform any database operations; it just resets the object to its
* initial state.
2012-03-27 14:22:24 +00:00
*/
ModelBaseClass.prototype.reset = function () {
2014-01-24 17:09:53 +00:00
var obj = this;
for (var k in obj) {
if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
delete obj[k];
}
if (obj.propertyChanged(k)) {
obj[k] = obj[k + '$was'];
}
2014-01-24 17:09:53 +00:00
}
2011-11-28 16:31:01 +00:00
};
ModelBaseClass.prototype.inspect = function () {
2014-01-24 17:09:53 +00:00
return util.inspect(this.__data, false, 4, true);
2012-10-15 23:15:29 +00:00
};
2014-01-24 17:09:53 +00:00
ModelBaseClass.mixin = function (anotherClass, options) {
return jutil.mixin(this, anotherClass, options);
};
2013-05-28 20:50:59 +00:00
2013-10-31 17:51:33 +00:00
ModelBaseClass.prototype.getDataSource = function () {
return this.__dataSource || this.constructor.dataSource;
};
2013-10-31 17:51:33 +00:00
ModelBaseClass.getDataSource = function () {
return this.dataSource;
};
ModelBaseClass.prototype.setStrict = function (strict) {
this.__strict = strict;
};
2013-10-31 17:51:33 +00:00
2013-05-28 05:20:30 +00:00
jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable);