407 lines
11 KiB
JavaScript
407 lines
11 KiB
JavaScript
/*!
|
|
* Module exports class Model
|
|
*/
|
|
module.exports = ModelBaseClass;
|
|
|
|
/*!
|
|
* Module dependencies
|
|
*/
|
|
|
|
var util = require('util');
|
|
var jutil = require('./jutil');
|
|
var List = require('./list');
|
|
var Hookable = require('./hooks');
|
|
var validations = require('./validations.js');
|
|
|
|
// Set up an object for quick lookup
|
|
var BASE_TYPES = {
|
|
'String': true,
|
|
'Boolean': true,
|
|
'Number': true,
|
|
'Date': true,
|
|
'Text': true,
|
|
'ObjectID': true
|
|
};
|
|
|
|
/**
|
|
* Model class: base class for all persistent objects.
|
|
*
|
|
* `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
|
|
*
|
|
* @class
|
|
* @param {Object} data Initial object data
|
|
*/
|
|
function ModelBaseClass(data, options) {
|
|
options = options || {};
|
|
if(!('applySetters' in options)) {
|
|
// Default to true
|
|
options.applySetters = true;
|
|
}
|
|
this._initProperties(data, options);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
var self = this;
|
|
var ctor = this.constructor;
|
|
|
|
if(data instanceof ctor) {
|
|
// Convert the data to be plain object to avoid pollutions
|
|
data = data.toObject(false);
|
|
}
|
|
var properties = ctor.definition.properties;
|
|
data = data || {};
|
|
|
|
options = options || {};
|
|
var applySetters = options.applySetters;
|
|
var strict = options.strict;
|
|
|
|
if(strict === undefined) {
|
|
strict = ctor.definition.settings.strict;
|
|
}
|
|
|
|
if (ctor.hideInternalProperties) {
|
|
// Object.defineProperty() is expensive. We only try to make the internal
|
|
// properties hidden (non-enumerable) if the model class has the
|
|
// `hideInternalProperties` set to true
|
|
Object.defineProperties(this, {
|
|
__cachedRelations: {
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: {}
|
|
},
|
|
|
|
__data: {
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: {}
|
|
},
|
|
|
|
// Instance level data source
|
|
__dataSource: {
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: options.dataSource
|
|
},
|
|
|
|
// Instance level strict mode
|
|
__strict: {
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: strict
|
|
}
|
|
});
|
|
} else {
|
|
this.__cachedRelations = {};
|
|
this.__data = {};
|
|
this.__dataSource = options.dataSource;
|
|
this.__strict = strict;
|
|
}
|
|
|
|
if (data.__cachedRelations) {
|
|
this.__cachedRelations = data.__cachedRelations;
|
|
}
|
|
|
|
var keys = Object.keys(data);
|
|
var size = keys.length;
|
|
var p, propVal;
|
|
for (var k = 0; k < size; k++) {
|
|
p = keys[k];
|
|
propVal = data[p];
|
|
if (typeof propVal === 'function') {
|
|
continue;
|
|
}
|
|
if (properties[p]) {
|
|
// Managed property
|
|
if (applySetters) {
|
|
self[p] = propVal;
|
|
} else {
|
|
self.__data[p] = propVal;
|
|
}
|
|
} else if (ctor.relations[p]) {
|
|
// Relation
|
|
if (ctor.relations[p].type === 'belongsTo' && propVal != null) {
|
|
// If the related model is populated
|
|
self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
|
|
}
|
|
self.__cachedRelations[p] = propVal;
|
|
} else {
|
|
// Un-managed property
|
|
if (strict === false) {
|
|
self[p] = self.__data[p] = propVal;
|
|
} else if (strict === 'throw') {
|
|
throw new Error('Unknown property: ' + p);
|
|
}
|
|
}
|
|
}
|
|
|
|
keys = Object.keys(properties);
|
|
size = keys.length;
|
|
|
|
for (k = 0; k < size; k++) {
|
|
p = keys[k];
|
|
propVal = self.__data[p];
|
|
|
|
// Set default values
|
|
if (propVal === undefined) {
|
|
var def = properties[p]['default'];
|
|
if (def !== undefined) {
|
|
if (typeof def === 'function') {
|
|
if (def === Date) {
|
|
// FIXME: We should coerce the value in general
|
|
// This is a work around to {default: Date}
|
|
// Date() will return a string instead of Date
|
|
def = new Date();
|
|
} else {
|
|
def = def();
|
|
}
|
|
}
|
|
// FIXME: We should coerce the value
|
|
// will implement it after we refactor the PropertyDefinition
|
|
self.__data[p] = def;
|
|
}
|
|
}
|
|
|
|
// Handle complex types (JSON/Object)
|
|
var type = properties[p].type;
|
|
if (! BASE_TYPES[type.name]) {
|
|
if (typeof self.__data[p] !== 'object' && self.__data[p]) {
|
|
try {
|
|
self.__data[p] = JSON.parse(self.__data[p] + '');
|
|
} catch (e) {
|
|
self.__data[p] = String(self.__data[p]);
|
|
}
|
|
}
|
|
if (type.name === 'Array' || Array.isArray(type)) {
|
|
if (!(self.__data[p] instanceof List)
|
|
&& self.__data[p] !== undefined
|
|
&& self.__data[p] !== null ) {
|
|
self.__data[p] = List(self.__data[p], type, self);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.trigger('initialize');
|
|
};
|
|
|
|
/**
|
|
* Define a property on the model.
|
|
* @param {String} prop Property name
|
|
* @param {Object} params Various property configuration
|
|
*/
|
|
ModelBaseClass.defineProperty = function (prop, params) {
|
|
this.dataSource.defineProperty(this.modelName, prop, params);
|
|
};
|
|
|
|
ModelBaseClass.getPropertyType = function (propName) {
|
|
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;
|
|
};
|
|
|
|
ModelBaseClass.prototype.getPropertyType = function (propName) {
|
|
return this.constructor.getPropertyType(propName);
|
|
};
|
|
|
|
/**
|
|
* Return string representation of class
|
|
* This overrides the default `toString()` method
|
|
*/
|
|
ModelBaseClass.toString = function () {
|
|
return '[Model ' + this.modelName + ']';
|
|
};
|
|
|
|
/**
|
|
* Convert model instance to a plain JSON object.
|
|
* Returns a canonical object representation (no getters and setters).
|
|
*
|
|
* @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.
|
|
*/
|
|
ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
|
|
if (onlySchema === undefined) {
|
|
onlySchema = true;
|
|
}
|
|
var data = {};
|
|
var self = this;
|
|
var Model = this.constructor;
|
|
|
|
// if it is already an Object
|
|
if (Model === Object) {
|
|
return self;
|
|
}
|
|
|
|
var strict = this.__strict;
|
|
var schemaLess = (strict === false) || !onlySchema;
|
|
|
|
var props = Model.definition.properties;
|
|
var keys = Object.keys(props);
|
|
var propertyName, val;
|
|
for (var i = 0; i < keys.length; i++) {
|
|
propertyName = keys[i];
|
|
val = self[propertyName];
|
|
|
|
// Exclude functions
|
|
if (typeof val === 'function') {
|
|
continue;
|
|
}
|
|
// Exclude hidden properties
|
|
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
|
continue;
|
|
}
|
|
|
|
if (val instanceof List) {
|
|
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
|
} else {
|
|
if (val !== undefined && val !== null && val.toObject) {
|
|
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
|
} else {
|
|
data[propertyName] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
keys = Object.keys(self);
|
|
var size = keys.length;
|
|
for (i = 0; i < size; i++) {
|
|
propertyName = keys[i];
|
|
if (props[propertyName]) {
|
|
continue;
|
|
}
|
|
if (propertyName.indexOf('__') === 0) {
|
|
continue;
|
|
}
|
|
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
|
continue;
|
|
}
|
|
val = self[propertyName];
|
|
if (val !== undefined && data[propertyName] === undefined) {
|
|
if (typeof val === 'function') {
|
|
continue;
|
|
}
|
|
if (val !== null && val.toObject) {
|
|
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
|
} else {
|
|
data[propertyName] = val;
|
|
}
|
|
}
|
|
}
|
|
// Now continue to check __data
|
|
keys = Object.keys(self.__data);
|
|
size = keys.length;
|
|
for (i = 0; i < size; i++) {
|
|
propertyName = keys[i];
|
|
if (propertyName.indexOf('__') === 0) {
|
|
continue;
|
|
}
|
|
if (data[propertyName] === undefined) {
|
|
if (removeHidden && Model.isHiddenProperty(propertyName)) {
|
|
continue;
|
|
}
|
|
var ownVal = self[propertyName];
|
|
// The ownVal can be a relation function
|
|
val = (ownVal !== undefined && (typeof ownVal !== 'function'))
|
|
? ownVal : self.__data[propertyName];
|
|
if (typeof val === 'function') {
|
|
continue;
|
|
}
|
|
|
|
if (val !== undefined && val !== null && val.toObject) {
|
|
data[propertyName] = val.toObject(!schemaLess, removeHidden);
|
|
} else {
|
|
data[propertyName] = val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
ModelBaseClass.isHiddenProperty = function (propertyName) {
|
|
var Model = this;
|
|
var settings = Model.definition && Model.definition.settings;
|
|
var hiddenProperties = settings && (settings.hiddenProperties || settings.hidden);
|
|
if (Array.isArray(hiddenProperties)) {
|
|
// Cache the hidden properties as an object for quick lookup
|
|
settings.hiddenProperties = {};
|
|
for (var i = 0; i < hiddenProperties.length; i++) {
|
|
settings.hiddenProperties[hiddenProperties[i]] = true;
|
|
}
|
|
hiddenProperties = settings.hiddenProperties;
|
|
}
|
|
if (hiddenProperties) {
|
|
return hiddenProperties[propertyName];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ModelBaseClass.prototype.toJSON = function () {
|
|
return this.toObject(false, true);
|
|
};
|
|
|
|
ModelBaseClass.prototype.fromObject = function (obj) {
|
|
for (var key in obj) {
|
|
this[key] = obj[key];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reset dirty attributes.
|
|
* This method does not perform any database operations; it just resets the object to its
|
|
* initial state.
|
|
*/
|
|
ModelBaseClass.prototype.reset = function () {
|
|
var obj = this;
|
|
for (var k in obj) {
|
|
if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
|
|
delete obj[k];
|
|
}
|
|
}
|
|
};
|
|
|
|
ModelBaseClass.prototype.inspect = function () {
|
|
return util.inspect(this.__data, false, 4, true);
|
|
};
|
|
|
|
ModelBaseClass.mixin = function (anotherClass, options) {
|
|
return jutil.mixin(this, anotherClass, options);
|
|
};
|
|
|
|
ModelBaseClass.prototype.getDataSource = function () {
|
|
return this.__dataSource || this.constructor.dataSource;
|
|
};
|
|
|
|
ModelBaseClass.getDataSource = function () {
|
|
return this.dataSource;
|
|
};
|
|
|
|
ModelBaseClass.prototype.setStrict = function (strict) {
|
|
this.__strict = strict;
|
|
};
|
|
|
|
jutil.mixin(ModelBaseClass, Hookable);
|
|
jutil.mixin(ModelBaseClass, validations.Validatable);
|