Merge branch 'release/1.3.0' into production

This commit is contained in:
Raymond Feng 2014-02-11 14:34:20 -08:00
commit c653483568
18 changed files with 794 additions and 150 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ docs/html
docs/man
npm-debug.log
.project
test/memory.json

View File

@ -17,6 +17,42 @@ function Connector(name, settings) {
*/
Connector.prototype.relational = false;
/**
* Get types associated with the connector
* @returns {String[]} The types for the connector
*/
Connector.prototype.getTypes = function() {
return ['db', 'nosql'];
};
/**
* Get the default data type for ID
* @returns {Function} The default type for ID
*/
Connector.prototype.getDefaultIdType = function() {
return String;
};
/**
* Get the metadata for the connector
* @returns {Object} The metadata object
* @property {String} type The type for the backend
* @property {Function} defaultIdType The default id type
* @property {Boolean} [isRelational] If the connector represents a relational database
* @property {Object} schemaForSettings The schema for settings object
*/
Connector.prototype.getMedadata = function () {
if (!this._metadata) {
this._metadata = {
types: this.getTypes(),
defaultIdType: this.getDefaultIdType(),
isRelational: this.isRelational || (this.getTypes().indexOf('rdbms') !== -1),
schemaForSettings: {}
};
}
return this._metadata;
};
/**
* Execute a command with given parameters
* @param {String} command The command such as SQL
@ -98,6 +134,7 @@ Connector.prototype.defineProperty = function (model, propertyName, propertyDefi
*/
Connector.prototype.disconnect = function disconnect(cb) {
// NO-OP
cb && process.nextTick(cb);
};
/**

View File

@ -2,6 +2,8 @@ var util = require('util');
var Connector = require('../connector');
var geo = require('../geo');
var utils = require('../utils');
var fs = require('fs');
var async = require('async');
/**
* Initialize the Oracle connector against the given data source
@ -10,42 +12,111 @@ var utils = require('../utils');
* @param {Function} [callback] The callback function
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new Memory();
dataSource.connector = new Memory(null, dataSource.settings);
dataSource.connector.connect(callback);
};
exports.Memory = Memory;
function Memory(m) {
if (m) {
function Memory(m, settings) {
if (m instanceof Memory) {
this.isTransaction = true;
this.cache = m.cache;
this.ids = m.ids;
this.constructor.super_.call(this, 'memory');
this.constructor.super_.call(this, 'memory', settings);
this._models = m._models;
} else {
this.isTransaction = false;
this.cache = {};
this.ids = {};
this.constructor.super_.call(this, 'memory');
this.constructor.super_.call(this, 'memory', settings);
}
}
util.inherits(Memory, Connector);
Memory.prototype.getDefaultIdType = function() {
return Number;
};
Memory.prototype.getTypes = function() {
return ['db', 'nosql', 'memory'];
};
Memory.prototype.connect = function (callback) {
if (this.isTransaction) {
this.onTransactionExec = callback;
} else {
this.loadFromFile(callback);
}
};
Memory.prototype.loadFromFile = function(callback) {
var self = this;
if (self.settings.file) {
fs.readFile(self.settings.file, {encoding: 'utf8', flag: 'r'}, function (err, data) {
if (err && err.code !== 'ENOENT') {
callback && callback(err);
} else {
if (data) {
data = JSON.parse(data.toString());
self.ids = data.ids || {};
self.cache = data.models || {};
} else {
if(!self.cache) {
self.ids = {};
self.cache = {};
}
}
callback && callback();
}
});
} else {
process.nextTick(callback);
}
};
/*!
* Flush the cache into the json file if necessary
* @param {Function} callback
*/
Memory.prototype.saveToFile = function (result, callback) {
var self = this;
if (this.settings.file) {
if(!self.writeQueue) {
// Create a queue for writes
self.writeQueue = async.queue(function (task, cb) {
// Flush out the models/ids
var data = JSON.stringify({
ids: self.ids,
models: self.cache
}, null, ' ');
fs.writeFile(self.settings.file, data, function (err) {
cb(err);
task.callback && task.callback(err, task.data);
});
}, 1);
}
// Enqueue the write
self.writeQueue.push({
data: result,
callback: callback
});
} else {
process.nextTick(function () {
callback && callback(null, result);
});
}
};
Memory.prototype.define = function defineModel(definition) {
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
var m = definition.model.modelName;
this.cache[m] = {};
this.ids[m] = 1;
if(!this.cache[m]) {
this.cache[m] = {};
this.ids[m] = 1;
}
};
Memory.prototype.create = function create(model, data, callback) {
@ -68,10 +139,11 @@ Memory.prototype.create = function create(model, data, callback) {
var idName = this.idName(model);
id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id);
if(!this.cache[model]) {
this.cache[model] = {};
}
this.cache[model][id] = JSON.stringify(data);
process.nextTick(function () {
callback(null, id);
});
this.saveToFile(id, callback);
};
Memory.prototype.updateOrCreate = function (model, data, callback) {
@ -90,9 +162,7 @@ Memory.prototype.updateOrCreate = function (model, data, callback) {
Memory.prototype.save = function save(model, data, callback) {
this.cache[model][this.getIdValue(model, data)] = JSON.stringify(data);
process.nextTick(function () {
callback(null, data);
});
this.saveToFile(data, callback);
};
Memory.prototype.exists = function exists(model, id, callback) {
@ -110,7 +180,7 @@ Memory.prototype.find = function find(model, id, callback) {
Memory.prototype.destroy = function destroy(model, id, callback) {
delete this.cache[model][id];
process.nextTick(callback);
this.saveToFile(null, callback);
};
Memory.prototype.fromDb = function (model, data) {
@ -273,7 +343,7 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
if (!where) {
this.cache[model] = {};
}
process.nextTick(callback);
this.saveToFile(null, callback);
};
Memory.prototype.count = function count(model, callback, where) {

View File

@ -126,6 +126,7 @@ DataAccessObject.create = function (data, callback) {
function modelCreated() {
if (--wait === 0) {
callback(gotError ? errors : null, instances);
if(!gotError) instances.forEach(Model.emit.bind('changed'));
}
}
}
@ -168,6 +169,7 @@ DataAccessObject.create = function (data, callback) {
saveDone.call(obj, function () {
createDone.call(obj, function () {
callback(err, obj);
if(!err) Model.emit('changed', obj);
});
});
}, obj);
@ -221,7 +223,7 @@ DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data
this.getDataSource().connector.updateOrCreate(Model.modelName, inst.toObject(true), function (err, data) {
var obj;
if (data) {
inst._initProperties(data, false);
inst._initProperties(data);
obj = inst;
} else {
obj = null;
@ -318,7 +320,7 @@ DataAccessObject.findById = function find(id, cb) {
setIdValue(this, data, id);
}
obj = new this();
obj._initProperties(data, false);
obj._initProperties(data);
}
cb(err, obj);
}.bind(this));
@ -540,14 +542,34 @@ DataAccessObject.find = function find(params, cb) {
data.forEach(function (d, i) {
var obj = new constr();
obj._initProperties(d, false, params.fields);
obj._initProperties(d, {fields: params.fields});
if (params && params.include && params.collect) {
data[i] = obj.__cachedRelations[params.collect];
} else {
data[i] = obj;
if (params && params.include) {
if (params.collect) {
// The collect property indicates that the query is to return the
// standlone items for a related model, not as child of the parent object
// For example, article.tags
obj = obj.__cachedRelations[params.collect];
} else {
// This handles the case to return parent items including the related
// models. For example, Article.find({include: 'tags'}, ...);
// Try to normalize the include
var includes = params.include || [];
if (typeof includes === 'string') {
includes = [includes];
} else if (typeof includes === 'object') {
includes = Object.keys(includes);
}
includes.forEach(function (inc) {
// Promote the included model as a direct property
obj.__data[inc] = obj.__cachedRelations[inc];
});
delete obj.__data.__cachedRelations;
}
}
data[i] = obj;
});
if (data && data.countBeforeLimit) {
data.countBeforeLimit = data.countBeforeLimit;
}
@ -607,6 +629,7 @@ DataAccessObject.remove =
DataAccessObject.deleteAll =
DataAccessObject.destroyAll = function destroyAll(where, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
if (!cb && 'function' === typeof where) {
cb = where;
@ -615,6 +638,7 @@ DataAccessObject.remove =
if (!where) {
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
cb && cb(err, data);
if(!err) Model.emit('deletedAll');
}.bind(this));
} else {
// Support an optional where object
@ -622,6 +646,7 @@ DataAccessObject.remove =
where = this._coerce(where);
this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) {
cb && cb(err, data);
if(!err) Model.emit('deletedAll', where);
}.bind(this));
}
};
@ -635,11 +660,13 @@ DataAccessObject.removeById =
DataAccessObject.deleteById =
DataAccessObject.destroyById = function deleteById(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
this.getDataSource().connector.destroy(this.modelName, id, function (err) {
if ('function' === typeof cb) {
cb(err);
}
if(!err) Model.emit('deleted', id);
}.bind(this));
};
@ -684,6 +711,7 @@ setRemoting(DataAccessObject.count, {
*/
DataAccessObject.prototype.save = function (options, callback) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this.constructor;
if (typeof options == 'function') {
callback = options;
@ -736,10 +764,13 @@ DataAccessObject.prototype.save = function (options, callback) {
if (err) {
return callback(err, inst);
}
inst._initProperties(data, false);
inst._initProperties(data);
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
if(!err) {
Model.emit('changed', inst);
}
});
});
});
@ -769,15 +800,18 @@ DataAccessObject.prototype.remove =
DataAccessObject.prototype.delete =
DataAccessObject.prototype.destroy = function (cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this.constructor;
var id = getIdValue(this.constructor, this);
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, getIdValue(this.constructor, this), function (err) {
this._adapter().destroy(this.constructor.modelName, id, function (err) {
if (err) {
return cb(err);
}
destroyed(function () {
if (cb) cb();
Model.emit('deleted', id);
});
}.bind(this));
});
@ -811,7 +845,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
if (stillConnecting(this.getDataSource(), this, arguments)) return;
var inst = this;
var model = this.constructor.modelName;
var Model = this.constructor
var model = Model.modelName;
if (typeof data === 'function') {
cb = data;
@ -850,7 +885,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
}
done.call(inst, function () {
saveDone.call(inst, function () {
cb(err, inst);
if(cb) cb(err, inst);
if(!err) Model.emit('changed', inst);
});
});
});

View File

@ -424,17 +424,28 @@ DataSource.prototype.defineRelations = function (modelClass, relations) {
/*!
* Set up the data access functions from the data source
* @param modelClass
* @param settings
* @param {Model} modelClass The model class
* @param {Object} settings The settings object
*/
DataSource.prototype.setupDataAccess = function (modelClass, settings) {
if (this.connector && this.connector.define) {
// pass control to connector
this.connector.define({
model: modelClass,
properties: modelClass.definition.properties,
settings: settings
});
if (this.connector) {
// Check if the id property should be generated
var idName = modelClass.definition.idName();
var idProp = modelClass.definition.rawProperties[idName];
if(idProp && idProp.generated && this.connector.getDefaultIdType) {
// Set the default id type from connector's ability
var idType = this.connector.getDefaultIdType() || String;
idProp.type = idType;
modelClass.definition.properties[idName].type = idType;
}
if (this.connector.define) {
// pass control to connector
this.connector.define({
model: modelClass,
properties: modelClass.definition.properties,
settings: settings
});
}
}
// add data access objects
@ -559,14 +570,55 @@ DataSource.prototype.mixin = function (ModelCtor) {
});
};
/**
* @see ModelBuilder.prototype.getModel
*/
DataSource.prototype.getModel = function (name, forceCreate) {
return this.modelBuilder.getModel(name, forceCreate);
};
/**
* @see ModelBuilder.prototype.getModelDefinition
*/
DataSource.prototype.getModelDefinition = function (name) {
return this.modelBuilder.getModelDefinition(name);
};
/**
* Get the data source types
* @returns {String[]} The data source type, such as ['db', 'nosql', 'mongodb'],
* ['rest'], or ['db', 'rdbms', 'mysql']
*/
DataSource.prototype.getTypes = function () {
var types = this.connector && this.connector.getTypes() || [];
if (typeof types === 'string') {
types = types.split(/[\s,\/]+/);
}
return types;
};
/**
* Check the data source supports the given types
* @param String|String[]) types A type name or an array of type names
* @return {Boolean} true if all types are supported by the data source
*/
DataSource.prototype.supportTypes = function (types) {
var supportedTypes = this.getTypes();
if (Array.isArray(types)) {
// Check each of the types
for (var i = 0; i < types.length; i++) {
if (supportedTypes.indexOf(types[i]) === -1) {
// Not supported
return false;
}
}
return true;
} else {
// The types is a string
return supportedTypes.indexOf(types) !== -1;
}
};
/**
* Attach an existing model to a data source.
*

View File

@ -1,3 +1,7 @@
var utils = require('./utils');
var isPlainObject = utils.isPlainObject;
var defineCachedRelations = utils.defineCachedRelations;
/**
* Include mixin for ./model.js
*/
@ -29,8 +33,8 @@ Inclusion.include = function (objects, include, cb) {
var self = this;
if (
(include.constructor.name == 'Array' && include.length == 0) ||
(include.constructor.name == 'Object' && Object.keys(include).length == 0)
!include || (Array.isArray(include) && include.length === 0) ||
(isPlainObject(include) && Object.keys(include).length === 0)
) {
cb(null, objects);
return;
@ -48,7 +52,7 @@ Inclusion.include = function (objects, include, cb) {
nbCallbacks++;
callback(function () {
nbCallbacks--;
if (nbCallbacks == 0) {
if (nbCallbacks === 0) {
cb(null, objects);
}
});
@ -61,7 +65,7 @@ Inclusion.include = function (objects, include, cb) {
if (typeof ij === 'string') {
ij = [ij];
}
if (ij.constructor.name === 'Object') {
if (isPlainObject(ij)) {
var newIj = [];
for (var key in ij) {
var obj = {};
@ -76,12 +80,13 @@ Inclusion.include = function (objects, include, cb) {
function processIncludeItem(objs, include, keyVals, objsByKeys) {
var relations = self.relations;
if (include.constructor.name === 'Object') {
var relationName = Object.keys(include)[0];
var subInclude = include[relationName];
var relationName, subInclude;
if (isPlainObject(include)) {
relationName = Object.keys(include)[0];
subInclude = include[relationName];
} else {
var relationName = include;
var subInclude = [];
relationName = include;
subInclude = [];
}
var relation = relations[relationName];
@ -89,7 +94,7 @@ Inclusion.include = function (objects, include, cb) {
return function () {
cb(new Error('Relation "' + relationName + '" is not defined for '
+ self.modelName + ' model'));
}
};
}
var req = {'where': {}};
@ -117,18 +122,17 @@ Inclusion.include = function (objects, include, cb) {
}
}
req['where'][relation.keyTo] = {inq: inValues};
req['include'] = subInclude;
req.where[relation.keyTo] = {inq: inValues};
req.include = subInclude;
return function (cb) {
relation.modelTo.find(req, function (err, objsIncluded) {
var objectsFrom, j;
for (var i = 0; i < objsIncluded.length; i++) {
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
for (j = 0; j < objectsFrom.length; j++) {
defineCachedRelations(objectsFrom[j]);
if (relation.multiple) {
if (!objectsFrom[j].__cachedRelations[relationName]) {
objectsFrom[j].__cachedRelations[relationName] = [];
@ -142,11 +146,9 @@ Inclusion.include = function (objects, include, cb) {
// No relation have been found for these keys
for (var key in keysToBeProcessed) {
var objectsFrom = objsByKeys[relation.keyFrom][key];
for (var j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) {
objectsFrom[j].__cachedRelations = {};
}
objectsFrom = objsByKeys[relation.keyFrom][key];
for (j = 0; j < objectsFrom.length; j++) {
defineCachedRelations(objectsFrom[j]);
objectsFrom[j].__cachedRelations[relationName] =
relation.multiple ? [] : null;
}
@ -158,5 +160,5 @@ Inclusion.include = function (objects, include, cb) {
return null;
}
}
};

View File

@ -71,6 +71,11 @@ ModelBuilder.prototype.getModel = function (name, forceCreate) {
return model;
};
/**
* Get the model definition by name
* @param {String} name The model name
* @returns {ModelDefinition} The model definition
*/
ModelBuilder.prototype.getModelDefinition = function (name) {
return this.definitions[name];
};
@ -151,17 +156,14 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
// TODO: [rfeng] We need to decide what names to use for built-in models such as User.
if (!ModelClass || !ModelClass.settings.unresolved) {
// every class can receive hash of data as optional param
ModelClass = function ModelConstructor(data, dataSource) {
ModelClass = function ModelConstructor(data, options) {
if (!(this instanceof ModelConstructor)) {
return new ModelConstructor(data, dataSource);
return new ModelConstructor(data, options);
}
if (ModelClass.settings.unresolved) {
throw new Error('Model ' + ModelClass.modelName + ' is not defined.');
}
ModelBaseClass.apply(this, arguments);
if (dataSource) {
hiddenProperty(this, '__dataSource', dataSource);
}
};
// mix in EventEmitter (don't inherit from)
var events = new EventEmitter();
@ -338,16 +340,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
if (!DataType) {
throw new Error('Invalid type for property ' + propertyName);
}
if (Array.isArray(DataType) || DataType === Array) {
DataType = List;
} else if (DataType.name === 'Date') {
var OrigDate = Date;
DataType = function Date(arg) {
return new OrigDate(arg);
};
} else if (typeof DataType === 'string') {
DataType = modelBuilder.resolveType(DataType);
}
if (prop.required) {
var requiredOptions = typeof prop.required === 'object' ? prop.required : undefined;
@ -363,6 +355,17 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
}
},
set: function (value) {
var DataType = ModelClass.definition.properties[propertyName].type;
if (Array.isArray(DataType) || DataType === Array) {
DataType = List;
} else if (DataType.name === 'Date') {
var OrigDate = Date;
DataType = function Date(arg) {
return new OrigDate(arg);
};
} else if (typeof DataType === 'string') {
DataType = modelBuilder.resolveType(DataType);
}
if (ModelClass.setter[propertyName]) {
ModelClass.setter[propertyName].call(this, value); // Try setter first
} else {

View File

@ -27,8 +27,13 @@ var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
* @constructor
* @param {Object} data - initial object data
*/
function ModelBaseClass(data) {
this._initProperties(data, true);
function ModelBaseClass(data, options) {
options = options || {};
if(!('applySetters' in options)) {
// Default to true
options.applySetters = true;
}
this._initProperties(data, options);
}
// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing
@ -42,19 +47,29 @@ function clone(data) {
*/
return data;
}
/**
* Initialize properties
* @param data
* @param applySetters
* 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, applySetters) {
ModelBaseClass.prototype._initProperties = function (data, options) {
var self = this;
var ctor = this.constructor;
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;
}
Object.defineProperty(this, '__cachedRelations', {
writable: true,
enumerable: false,
@ -76,15 +91,32 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
value: {}
});
if (data['__cachedRelations']) {
this.__cachedRelations = data['__cachedRelations'];
/**
* 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;
}
// Check if the strict option is set to false for the model
var strict = ctor.definition.settings.strict;
for (var i in data) {
if (i in properties) {
if (i in properties && typeof data[i] !== 'function') {
this.__data[i] = this.__dataWas[i] = clone(data[i]);
} else if (i in ctor.relations) {
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
@ -100,7 +132,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
if (applySetters === true) {
for (var propertyName in data) {
if ((propertyName in properties) || (propertyName in ctor.relations)) {
if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) {
self[propertyName] = self.__data[propertyName] || data[propertyName];
}
}
@ -109,7 +141,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
// Set the unknown properties as properties to the object
if (strict === false) {
for (var propertyName in data) {
if (!(propertyName in properties)) {
if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) {
self[propertyName] = self.__data[propertyName] || data[propertyName];
}
}
@ -117,7 +149,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
ctor.forEachProperty(function (propertyName) {
if ('undefined' === typeof self.__data[propertyName]) {
if (undefined === self.__data[propertyName]) {
self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName);
} else {
self.__dataWas[propertyName] = self.__data[propertyName];
@ -160,7 +192,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
}
this.trigger('initialize');
}
};
/**
* @param {String} prop - property name
@ -197,18 +229,23 @@ ModelBaseClass.toString = function () {
};
/**
* Convert instance to Object
* Convert model instance to a plain JSON object
*
* @param {Boolean} onlySchema - restrict properties to dataSource only, default false
* when onlySchema == true, only properties defined in dataSource returned,
* otherwise all enumerable properties returned
* @param {Boolean} onlySchema - restrict properties to dataSource only,
* default to false. When onlySchema is true, only properties defined in
* the schema are returned, otherwise all enumerable properties returned
* @returns {Object} - canonical object representation (no getters and setters)
*/
ModelBaseClass.prototype.toObject = function (onlySchema) {
if(onlySchema === undefined) {
onlySchema = true;
}
var data = {};
var self = this;
var schemaLess = this.constructor.definition.settings.strict === false || !onlySchema;
var strict = this.__strict;
var schemaLess = (strict === false) || !onlySchema;
this.constructor.forEachProperty(function (propertyName) {
if (self[propertyName] instanceof List) {
data[propertyName] = self[propertyName].toObject(!schemaLess);
@ -223,10 +260,25 @@ ModelBaseClass.prototype.toObject = function (onlySchema) {
}
});
var val = null;
if (schemaLess) {
for (var propertyName in self.__data) {
// 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(self.hasOwnProperty(propertyName) && (!data.hasOwnProperty(propertyName))) {
val = self[propertyName];
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess);
} else {
data[propertyName] = val;
}
}
}
// Now continue to check __data
for (propertyName in self.__data) {
if (!data.hasOwnProperty(propertyName)) {
var val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName];
val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName];
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess);
} else {
@ -238,13 +290,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema) {
return data;
};
// ModelBaseClass.prototype.hasOwnProperty = function (prop) {
// return this.__data && this.__data.hasOwnProperty(prop) ||
// Object.getOwnPropertyNames(this).indexOf(prop) !== -1;
// };
ModelBaseClass.prototype.toJSON = function () {
return this.toObject();
return this.toObject(false);
};
ModelBaseClass.prototype.fromObject = function (obj) {
@ -291,10 +338,15 @@ ModelBaseClass.mixin = function (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);

View File

@ -18,6 +18,18 @@ Relation.relationNameFor = function relationNameFor(foreignKey) {
}
};
function lookupModel(models, modelName) {
if(models[modelName]) {
return models[modelName];
}
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
/**
* Declare hasMany relation
*
@ -34,11 +46,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
anotherClass = params.model;
} else {
var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
for (var name in this.dataSource.modelBuilder.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.dataSource.modelBuilder.models[name];
}
}
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
}
}
var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true);
@ -130,9 +138,13 @@ Relation.hasMany = function hasMany(anotherClass, params) {
function find(id, cb) {
anotherClass.findById(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(new Error('Not found'));
if (inst[fk] && inst[fk].toString() == this[idName].toString()) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(new Error('Not found'));
}
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
cb(null, inst);
} else {
cb(new Error('Permission denied'));
@ -143,9 +155,13 @@ Relation.hasMany = function hasMany(anotherClass, params) {
function destroy(id, cb) {
var self = this;
anotherClass.findById(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(new Error('Not found'));
if (inst[fk] && inst[fk].toString() == self[idName].toString()) {
if (err) {
return cb(err);
}
if (!inst) {
return cb(new Error('Not found'));
}
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
inst.destroy(cb);
} else {
cb(new Error('Permission denied'));
@ -186,11 +202,7 @@ Relation.belongsTo = function (anotherClass, params) {
anotherClass = params.model;
} else {
var anotherClassName = anotherClass.toLowerCase();
for (var name in this.dataSource.modelBuilder.models) {
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.dataSource.modelBuilder.models[name];
}
}
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
}
}
@ -207,16 +219,20 @@ Relation.belongsTo = function (anotherClass, params) {
};
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName);
this.prototype['__finders__'] = this.prototype['__finders__'] || {};
this.prototype.__finders__ = this.prototype.__finders__ || {};
this.prototype['__finders__'][methodName] = function (id, cb) {
this.prototype.__finders__[methodName] = function (id, cb) {
if (id === null) {
cb(null, null);
return;
}
anotherClass.findById(id, function (err, inst) {
if (err) return cb(err);
if (!inst) return cb(null, null);
if (err) {
return cb(err);
}
if (!inst) {
return cb(null, null);
}
if (inst[idName] === this[fk]) {
cb(null, inst);
} else {
@ -234,7 +250,7 @@ Relation.belongsTo = function (anotherClass, params) {
}
var self = this;
var cachedValue;
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
if (!refresh && this.__cachedRelations && (this.__cachedRelations[methodName] !== undefined)) {
cachedValue = this.__cachedRelations[methodName];
}
if (p instanceof ModelBaseClass) { // acts as setter
@ -277,7 +293,7 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
if (params.model) {
anotherClass = params.model;
} else {
anotherClass = lookupModel(i8n.singularize(anotherClass)) ||
anotherClass = lookupModel(models, i8n.singularize(anotherClass).toLowerCase()) ||
anotherClass;
}
if (typeof anotherClass === 'string') {
@ -288,7 +304,7 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
if (!params.through) {
var name1 = this.modelName + anotherClass.modelName;
var name2 = anotherClass.modelName + this.modelName;
params.through = lookupModel(name1) || lookupModel(name2) ||
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
this.dataSource.define(name1);
}
params.through.belongsTo(this);
@ -296,13 +312,4 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
this.hasMany(anotherClass, {as: params.as, through: params.through});
function lookupModel(modelName) {
var lookupClassName = modelName.toLowerCase();
for (var name in models) {
if (name.toLowerCase() === lookupClassName) {
return models[name];
}
}
}
};

View File

@ -1,3 +1,5 @@
var utils = require('./utils');
var defineCachedRelations = utils.defineCachedRelations;
/**
* Module exports
*/
@ -54,14 +56,12 @@ function defineScope(cls, targetClass, name, params, methods) {
throw new Error('Method can be only called with one or two arguments');
}
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
if (!this.__cachedRelations || (this.__cachedRelations[name] === undefined) || actualRefresh) {
var self = this;
var params = mergeParams(actualCond, caller._scope);
return targetClass.find(params, function (err, data) {
if (!err && saveOnCache) {
if (!self.__cachedRelations) {
self.__cachedRelations = {};
}
defineCachedRelations(self);
self.__cachedRelations[name] = data;
}
cb(err, data);

View File

@ -19,6 +19,22 @@ util.inherits(BaseSQL, Connector);
*/
BaseSQL.prototype.relational = true;
/**
* Get types associated with the connector
* @returns {String[]} The types for the connector
*/
BaseSQL.prototype.getTypes = function() {
return ['db', 'rdbms', 'sql'];
};
/*!
* Get the default data type for ID
* @returns {Function}
*/
BaseSQL.prototype.getDefaultIdType = function() {
return Number;
};
BaseSQL.prototype.query = function () {
throw new Error('query method should be declared in connector');
};

View File

@ -4,6 +4,8 @@ exports.selectFields = selectFields;
exports.removeUndefined = removeUndefined;
exports.parseSettings = parseSettings;
exports.mergeSettings = mergeSettings;
exports.isPlainObject = isPlainObject;
exports.defineCachedRelations = defineCachedRelations;
var traverse = require('traverse');
@ -176,4 +178,29 @@ function mergeSettings(target, src) {
}
return dst;
}
/**
* Define an non-enumerable __cachedRelations property
* @param {Object} obj The obj to receive the __cachedRelations
*/
function defineCachedRelations(obj) {
if (!obj.__cachedRelations) {
Object.defineProperty(obj, '__cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
}
}
/**
* Check if the argument is plain object
* @param {*) obj The obj value
* @returns {boolean}
*/
function isPlainObject(obj) {
return (typeof obj === 'object') && (obj !== null)
&& (obj.constructor === Object);
}

View File

@ -1,6 +1,6 @@
{
"name": "loopback-datasource-juggler",
"version": "1.2.13",
"version": "1.3.0",
"description": "LoopBack DataSoure Juggler",
"keywords": [
"StrongLoop",
@ -27,10 +27,10 @@
"mocha": "~1.12.1"
},
"dependencies": {
"async": "~0.2.9",
"inflection": "~1.2.6",
"traverse": "~0.6.5",
"qs": "~0.6.5"
"async": "~0.2.10",
"inflection": "~1.3.3",
"traverse": "~0.6.6",
"qs": "~0.6.6"
},
"license": "MIT"
}

81
test/events.js Normal file
View File

@ -0,0 +1,81 @@
var should = require('./init.js');
describe('events', function() {
beforeEach(function(done) {
var test = this;
this.db = getSchema();
this.TestModel = this.db.define('TestModel');
this.db.automigrate(function(err) {
if(err) return done(err);
test.TestModel.create(function(err, inst) {
if(err) return done(err);
test.inst = inst;
done();
});
});
this.shouldEmitEvent = function(eventName, listener, done) {
var timeout = setTimeout(function() {
done(new Error('did not emit ' + eventName));
}, 100);
this.TestModel.on(eventName, function() {
clearTimeout(timeout);
listener.apply(this, arguments);
done();
});
}
});
describe('changed', function() {
it('should be emitted after save', function(done) {
var model = new this.TestModel({name: 'foobar'});
this.shouldEmitEvent('changed', assertValidChangedArgs, done);
model.save();
});
it('should be emitted after upsert', function(done) {
this.shouldEmitEvent('changed', assertValidChangedArgs, done);
this.TestModel.upsert({name: 'batbaz'});
});
it('should be emitted after create', function(done) {
this.shouldEmitEvent('changed', assertValidChangedArgs, done);
this.TestModel.create({name: '...'});
});
it('should be emitted after updateAttributes', function(done) {
var test = this;
this.TestModel.create({name: 'bazzy'}, function(err, model) {
// prevent getting the changed event from "create"
process.nextTick(function() {
test.shouldEmitEvent('changed', assertValidChangedArgs, done);
model.updateAttributes({name: 'foo'});
});
});
});
});
describe('deleted', function() {
it('should be emitted after destroy', function(done) {
this.shouldEmitEvent('deleted', assertValidDeletedArgs, done);
this.inst.destroy();
});
it('should be emitted after deleteById', function(done) {
this.shouldEmitEvent('deleted', assertValidDeletedArgs, done);
this.TestModel.deleteById(this.inst.id);
});
});
describe('deletedAll', function() {
it('should be emitted after destroyAll', function(done) {
this.shouldEmitEvent('deletedAll', function(where) {
where.name.should.equal('foo');
}, done);
this.TestModel.destroyAll({name: 'foo'});
});
});
});
function assertValidChangedArgs(obj) {
obj.should.have.property('id');
}
function assertValidDeletedArgs(id) {
id.should.be.ok;
}

View File

@ -402,6 +402,8 @@ describe('hooks', function () {
});
});
function addHooks(name, done) {
var called = false, random = String(Math.floor(Math.random() * 1000));
User['before' + name] = function (next, data) {

View File

@ -13,6 +13,12 @@ describe('include', function () {
passports.length.should.be.ok;
passports.forEach(function (p) {
p.__cachedRelations.should.have.property('owner');
// The relation should be promoted as the 'owner' property
p.should.have.property('owner');
// The __cachedRelations should be removed from json output
p.toJSON().should.not.have.property('__cachedRelations');
var owner = p.__cachedRelations.owner;
if (!p.ownerId) {
should.not.exist(owner);
@ -31,6 +37,11 @@ describe('include', function () {
should.exist(users);
users.length.should.be.ok;
users.forEach(function (u) {
// The relation should be promoted as the 'owner' property
u.should.have.property('posts');
// The __cachedRelations should be removed from json output
u.toJSON().should.not.have.property('__cachedRelations');
u.__cachedRelations.should.have.property('posts');
u.__cachedRelations.posts.forEach(function (p) {
p.userId.should.equal(u.id);
@ -47,6 +58,12 @@ describe('include', function () {
passports.length.should.be.ok;
passports.forEach(function (p) {
p.__cachedRelations.should.have.property('owner');
// The relation should be promoted as the 'owner' property
p.should.have.property('owner');
// The __cachedRelations should be removed from json output
p.toJSON().should.not.have.property('__cachedRelations');
var user = p.__cachedRelations.owner;
if (!p.ownerId) {
should.not.exist(user);
@ -97,6 +114,12 @@ describe('include', function () {
should.exist(users);
users.length.should.be.ok;
users.forEach(function (user) {
// The relation should be promoted as the 'owner' property
user.should.have.property('posts');
user.should.have.property('passports');
// The __cachedRelations should be removed from json output
user.toJSON().should.not.have.property('__cachedRelations');
user.__cachedRelations.should.have.property('posts');
user.__cachedRelations.should.have.property('passports');
user.__cachedRelations.posts.forEach(function (p) {

View File

@ -46,11 +46,35 @@ describe('ModelBuilder define model', function () {
User.modelName.should.equal('User');
user.should.be.a('object');
assert(user.name === 'Joe');
assert(user.age === undefined);
assert(user.toObject().age === undefined);
assert(user.toObject(true).age === undefined);
assert(user.bio === undefined);
user.should.have.property('name', 'Joe');
user.should.not.have.property('age');
user.toObject().should.not.have.property('age');
user.toObject(true).should.not.have.property('age');
user.should.not.have.property('bio');
done(null, User);
});
it('should ignore non-predefined properties in strict mode', function (done) {
var modelBuilder = new ModelBuilder();
var User = modelBuilder.define('User', {name: String, bio: String}, {strict: true});
var user = new User({name: 'Joe'});
user.age = 10;
user.bio = 'me';
user.should.have.property('name', 'Joe');
user.should.have.property('bio', 'me');
// Non predefined property age should be ignored in strict mode if schemaOnly parameter is not false
user.toObject().should.not.have.property('age');
user.toObject(true).should.not.have.property('age');
user.toObject(false).should.have.property('age', 10);
// Predefined property bio should be kept in strict mode
user.toObject().should.have.property('bio', 'me');
user.toObject(true).should.have.property('bio', 'me');
user.toObject(false).should.have.property('bio', 'me');
done(null, User);
});
@ -83,6 +107,31 @@ describe('ModelBuilder define model', function () {
done(null, User);
});
it('should take non-predefined properties in non-strict mode', function (done) {
var modelBuilder = new ModelBuilder();
var User = modelBuilder.define('User', {name: String, bio: String}, {strict: false});
var user = new User({name: 'Joe'});
user.age = 10;
user.bio = 'me';
user.should.have.property('name', 'Joe');
user.should.have.property('bio', 'me');
// Non predefined property age should be kept in non-strict mode
user.toObject().should.have.property('age', 10);
user.toObject(true).should.have.property('age', 10);
user.toObject(false).should.have.property('age', 10);
// Predefined property bio should be kept
user.toObject().should.have.property('bio', 'me');
user.toObject(true).should.have.property('bio', 'me');
user.toObject(false).should.have.property('bio', 'me');
done(null, User);
});
it('should use false as the default value for strict', function (done) {
var modelBuilder = new ModelBuilder();
@ -416,6 +465,61 @@ describe('DataSource define model', function () {
});
});
it('supports instance level strict mode', function () {
var ds = new DataSource('memory');
var User = ds.define('User', {name: String, bio: String}, {strict: true});
var user = new User({name: 'Joe', age: 20}, {strict: false});
user.should.have.property('__strict', false);
user.should.be.a('object');
user.should.have.property('name', 'Joe');
user.should.have.property('age', 20);
user.toObject().should.have.property('age', 20);
user.toObject(true).should.have.property('age', 20);
user.setStrict(true);
user.toObject().should.not.have.property('age');
user.toObject(true).should.not.have.property('age');
user.toObject(false).should.have.property('age', 20);
});
it('injects id by default', function (done) {
var ds = new ModelBuilder();
var User = ds.define('User', {});
assert.deepEqual(User.definition.properties.id,
{type: Number, id: 1, generated: true});
done();
});
it('disables idInjection if the value is false', function (done) {
var ds = new ModelBuilder();
var User1 = ds.define('User', {}, {idInjection: false});
assert(!User1.definition.properties.id);
done();
});
it('updates generated id type by the connector', function (done) {
var builder = new ModelBuilder();
var User = builder.define('User', {id: {type: String, generated: true, id: true}});
assert.deepEqual(User.definition.properties.id,
{type: String, id: 1, generated: true});
var ds = new DataSource('memory');// define models
User.attachTo(ds);
assert.deepEqual(User.definition.properties.id,
{type: Number, id: 1, generated: true});
done();
});
});
describe('Load models with base', function () {
@ -445,6 +549,42 @@ describe('Load models with base', function () {
});
});
describe('DataSource connector types', function() {
it('should return an array of types', function() {
var ds = new DataSource('memory');
var types = ds.getTypes();
assert.deepEqual(types, ['db', 'nosql', 'memory']);
});
it('should test supported types by string', function() {
var ds = new DataSource('memory');
var result = ds.supportTypes('db');
assert(result);
});
it('should test supported types by array', function() {
var ds = new DataSource('memory');
var result = ds.supportTypes(['db', 'memory']);
assert(result);
});
it('should test unsupported types by string', function() {
var ds = new DataSource('memory');
var result = ds.supportTypes('rdbms');
assert(!result);
});
it('should test unsupported types by array', function() {
var ds = new DataSource('memory');
var result = ds.supportTypes(['rdbms', 'memory']);
assert(!result);
result = ds.supportTypes(['rdbms']);
assert(!result);
});
});
describe('DataSource constructor', function () {
// Mocked require
var loader = function (name) {

95
test/memory.test.js Normal file
View File

@ -0,0 +1,95 @@
var jdb = require('../');
var DataSource = jdb.DataSource;
var path = require('path');
var fs = require('fs');
var assert = require('assert');
var async = require('async');
describe('Memory connector', function () {
var file = path.join(__dirname, 'memory.json');
function readModels(done) {
fs.readFile(file, function (err, data) {
var json = JSON.parse(data.toString());
assert(json.models);
assert(json.ids.User);
done(err, json);
});
}
before(function (done) {
fs.unlink(file, function (err) {
if (!err || err.code === 'ENOENT') {
done();
}
});
});
it('should save to a json file', function (done) {
var ds = new DataSource({
connector: 'memory',
file: file
});
var User = ds.createModel('User', {
name: String,
bio: String,
approved: Boolean,
joinedAt: Date,
age: Number
});
var count = 0;
var ids = [];
async.eachSeries(['John1', 'John2', 'John3'], function (item, cb) {
User.create({name: item}, function (err, result) {
ids.push(result.id);
count++;
readModels(function (err, json) {
assert.equal(Object.keys(json.models.User).length, count);
cb(err);
});
});
}, function (err, results) {
// Now try to delete one
User.deleteById(ids[0], function (err) {
readModels(function (err, json) {
assert.equal(Object.keys(json.models.User).length, 2);
User.upsert({id: ids[1], name: 'John'}, function(err, result) {
readModels(function (err, json) {
assert.equal(Object.keys(json.models.User).length, 2);
var user = JSON.parse(json.models.User[ids[1]]);
assert.equal(user.name, 'John');
done();
});
});
});
});
});
});
// The saved memory.json from previous test should be loaded
it('should load from the json file', function (done) {
var ds = new DataSource({
connector: 'memory',
file: file
});
var User = ds.createModel('User', {
name: String,
bio: String,
approved: Boolean,
joinedAt: Date,
age: Number
});
User.find(function (err, users) {
// There should be 2 records
assert.equal(users.length, 2);
done(err);
});
});
});