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 docs/man
npm-debug.log npm-debug.log
.project .project
test/memory.json

View File

@ -17,6 +17,42 @@ function Connector(name, settings) {
*/ */
Connector.prototype.relational = false; 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 * Execute a command with given parameters
* @param {String} command The command such as SQL * @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) { Connector.prototype.disconnect = function disconnect(cb) {
// NO-OP // NO-OP
cb && process.nextTick(cb);
}; };
/** /**

View File

@ -2,6 +2,8 @@ var util = require('util');
var Connector = require('../connector'); var Connector = require('../connector');
var geo = require('../geo'); var geo = require('../geo');
var utils = require('../utils'); var utils = require('../utils');
var fs = require('fs');
var async = require('async');
/** /**
* Initialize the Oracle connector against the given data source * Initialize the Oracle connector against the given data source
@ -10,42 +12,111 @@ var utils = require('../utils');
* @param {Function} [callback] The callback function * @param {Function} [callback] The callback function
*/ */
exports.initialize = function initializeDataSource(dataSource, callback) { exports.initialize = function initializeDataSource(dataSource, callback) {
dataSource.connector = new Memory(); dataSource.connector = new Memory(null, dataSource.settings);
dataSource.connector.connect(callback); dataSource.connector.connect(callback);
}; };
exports.Memory = Memory; exports.Memory = Memory;
function Memory(m) { function Memory(m, settings) {
if (m) { if (m instanceof Memory) {
this.isTransaction = true; this.isTransaction = true;
this.cache = m.cache; this.cache = m.cache;
this.ids = m.ids; this.ids = m.ids;
this.constructor.super_.call(this, 'memory'); this.constructor.super_.call(this, 'memory', settings);
this._models = m._models; this._models = m._models;
} else { } else {
this.isTransaction = false; this.isTransaction = false;
this.cache = {}; this.cache = {};
this.ids = {}; this.ids = {};
this.constructor.super_.call(this, 'memory'); this.constructor.super_.call(this, 'memory', settings);
} }
} }
util.inherits(Memory, Connector); util.inherits(Memory, Connector);
Memory.prototype.getDefaultIdType = function() {
return Number;
};
Memory.prototype.getTypes = function() {
return ['db', 'nosql', 'memory'];
};
Memory.prototype.connect = function (callback) { Memory.prototype.connect = function (callback) {
if (this.isTransaction) { if (this.isTransaction) {
this.onTransactionExec = callback; 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 { } else {
process.nextTick(callback); 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) { Memory.prototype.define = function defineModel(definition) {
this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments)); this.constructor.super_.prototype.define.apply(this, [].slice.call(arguments));
var m = definition.model.modelName; var m = definition.model.modelName;
this.cache[m] = {}; if(!this.cache[m]) {
this.ids[m] = 1; this.cache[m] = {};
this.ids[m] = 1;
}
}; };
Memory.prototype.create = function create(model, data, callback) { 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); var idName = this.idName(model);
id = (props[idName] && props[idName].type && props[idName].type(id)) || id; id = (props[idName] && props[idName].type && props[idName].type(id)) || id;
this.setIdValue(model, data, id); this.setIdValue(model, data, id);
if(!this.cache[model]) {
this.cache[model] = {};
}
this.cache[model][id] = JSON.stringify(data); this.cache[model][id] = JSON.stringify(data);
process.nextTick(function () { this.saveToFile(id, callback);
callback(null, id);
});
}; };
Memory.prototype.updateOrCreate = function (model, data, 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) { Memory.prototype.save = function save(model, data, callback) {
this.cache[model][this.getIdValue(model, data)] = JSON.stringify(data); this.cache[model][this.getIdValue(model, data)] = JSON.stringify(data);
process.nextTick(function () { this.saveToFile(data, callback);
callback(null, data);
});
}; };
Memory.prototype.exists = function exists(model, id, 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) { Memory.prototype.destroy = function destroy(model, id, callback) {
delete this.cache[model][id]; delete this.cache[model][id];
process.nextTick(callback); this.saveToFile(null, callback);
}; };
Memory.prototype.fromDb = function (model, data) { Memory.prototype.fromDb = function (model, data) {
@ -273,7 +343,7 @@ Memory.prototype.destroyAll = function destroyAll(model, where, callback) {
if (!where) { if (!where) {
this.cache[model] = {}; this.cache[model] = {};
} }
process.nextTick(callback); this.saveToFile(null, callback);
}; };
Memory.prototype.count = function count(model, callback, where) { Memory.prototype.count = function count(model, callback, where) {

View File

@ -126,6 +126,7 @@ DataAccessObject.create = function (data, callback) {
function modelCreated() { function modelCreated() {
if (--wait === 0) { if (--wait === 0) {
callback(gotError ? errors : null, instances); 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 () { saveDone.call(obj, function () {
createDone.call(obj, function () { createDone.call(obj, function () {
callback(err, obj); callback(err, obj);
if(!err) Model.emit('changed', obj);
}); });
}); });
}, 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) { this.getDataSource().connector.updateOrCreate(Model.modelName, inst.toObject(true), function (err, data) {
var obj; var obj;
if (data) { if (data) {
inst._initProperties(data, false); inst._initProperties(data);
obj = inst; obj = inst;
} else { } else {
obj = null; obj = null;
@ -318,7 +320,7 @@ DataAccessObject.findById = function find(id, cb) {
setIdValue(this, data, id); setIdValue(this, data, id);
} }
obj = new this(); obj = new this();
obj._initProperties(data, false); obj._initProperties(data);
} }
cb(err, obj); cb(err, obj);
}.bind(this)); }.bind(this));
@ -540,14 +542,34 @@ DataAccessObject.find = function find(params, cb) {
data.forEach(function (d, i) { data.forEach(function (d, i) {
var obj = new constr(); var obj = new constr();
obj._initProperties(d, false, params.fields); obj._initProperties(d, {fields: params.fields});
if (params && params.include && params.collect) { if (params && params.include) {
data[i] = obj.__cachedRelations[params.collect]; if (params.collect) {
} else { // The collect property indicates that the query is to return the
data[i] = obj; // 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) { if (data && data.countBeforeLimit) {
data.countBeforeLimit = data.countBeforeLimit; data.countBeforeLimit = data.countBeforeLimit;
} }
@ -607,6 +629,7 @@ DataAccessObject.remove =
DataAccessObject.deleteAll = DataAccessObject.deleteAll =
DataAccessObject.destroyAll = function destroyAll(where, cb) { DataAccessObject.destroyAll = function destroyAll(where, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
if (!cb && 'function' === typeof where) { if (!cb && 'function' === typeof where) {
cb = where; cb = where;
@ -615,6 +638,7 @@ DataAccessObject.remove =
if (!where) { if (!where) {
this.getDataSource().connector.destroyAll(this.modelName, function (err, data) { this.getDataSource().connector.destroyAll(this.modelName, function (err, data) {
cb && cb(err, data); cb && cb(err, data);
if(!err) Model.emit('deletedAll');
}.bind(this)); }.bind(this));
} else { } else {
// Support an optional where object // Support an optional where object
@ -622,6 +646,7 @@ DataAccessObject.remove =
where = this._coerce(where); where = this._coerce(where);
this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) { this.getDataSource().connector.destroyAll(this.modelName, where, function (err, data) {
cb && cb(err, data); cb && cb(err, data);
if(!err) Model.emit('deletedAll', where);
}.bind(this)); }.bind(this));
} }
}; };
@ -635,11 +660,13 @@ DataAccessObject.removeById =
DataAccessObject.deleteById = DataAccessObject.deleteById =
DataAccessObject.destroyById = function deleteById(id, cb) { DataAccessObject.destroyById = function deleteById(id, cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this;
this.getDataSource().connector.destroy(this.modelName, id, function (err) { this.getDataSource().connector.destroy(this.modelName, id, function (err) {
if ('function' === typeof cb) { if ('function' === typeof cb) {
cb(err); cb(err);
} }
if(!err) Model.emit('deleted', id);
}.bind(this)); }.bind(this));
}; };
@ -684,6 +711,7 @@ setRemoting(DataAccessObject.count, {
*/ */
DataAccessObject.prototype.save = function (options, callback) { DataAccessObject.prototype.save = function (options, callback) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this.constructor;
if (typeof options == 'function') { if (typeof options == 'function') {
callback = options; callback = options;
@ -736,10 +764,13 @@ DataAccessObject.prototype.save = function (options, callback) {
if (err) { if (err) {
return callback(err, inst); return callback(err, inst);
} }
inst._initProperties(data, false); inst._initProperties(data);
updateDone.call(inst, function () { updateDone.call(inst, function () {
saveDone.call(inst, function () { saveDone.call(inst, function () {
callback(err, inst); callback(err, inst);
if(!err) {
Model.emit('changed', inst);
}
}); });
}); });
}); });
@ -769,15 +800,18 @@ DataAccessObject.prototype.remove =
DataAccessObject.prototype.delete = DataAccessObject.prototype.delete =
DataAccessObject.prototype.destroy = function (cb) { DataAccessObject.prototype.destroy = function (cb) {
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var Model = this.constructor;
var id = getIdValue(this.constructor, this);
this.trigger('destroy', function (destroyed) { 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) { if (err) {
return cb(err); return cb(err);
} }
destroyed(function () { destroyed(function () {
if (cb) cb(); if (cb) cb();
Model.emit('deleted', id);
}); });
}.bind(this)); }.bind(this));
}); });
@ -811,7 +845,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
if (stillConnecting(this.getDataSource(), this, arguments)) return; if (stillConnecting(this.getDataSource(), this, arguments)) return;
var inst = this; var inst = this;
var model = this.constructor.modelName; var Model = this.constructor
var model = Model.modelName;
if (typeof data === 'function') { if (typeof data === 'function') {
cb = data; cb = data;
@ -850,7 +885,8 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
} }
done.call(inst, function () { done.call(inst, function () {
saveDone.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 * Set up the data access functions from the data source
* @param modelClass * @param {Model} modelClass The model class
* @param settings * @param {Object} settings The settings object
*/ */
DataSource.prototype.setupDataAccess = function (modelClass, settings) { DataSource.prototype.setupDataAccess = function (modelClass, settings) {
if (this.connector && this.connector.define) { if (this.connector) {
// pass control to connector // Check if the id property should be generated
this.connector.define({ var idName = modelClass.definition.idName();
model: modelClass, var idProp = modelClass.definition.rawProperties[idName];
properties: modelClass.definition.properties, if(idProp && idProp.generated && this.connector.getDefaultIdType) {
settings: settings // 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 // add data access objects
@ -559,14 +570,55 @@ DataSource.prototype.mixin = function (ModelCtor) {
}); });
}; };
/**
* @see ModelBuilder.prototype.getModel
*/
DataSource.prototype.getModel = function (name, forceCreate) { DataSource.prototype.getModel = function (name, forceCreate) {
return this.modelBuilder.getModel(name, forceCreate); return this.modelBuilder.getModel(name, forceCreate);
}; };
/**
* @see ModelBuilder.prototype.getModelDefinition
*/
DataSource.prototype.getModelDefinition = function (name) { DataSource.prototype.getModelDefinition = function (name) {
return this.modelBuilder.getModelDefinition(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. * 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 * Include mixin for ./model.js
*/ */
@ -29,8 +33,8 @@ Inclusion.include = function (objects, include, cb) {
var self = this; var self = this;
if ( if (
(include.constructor.name == 'Array' && include.length == 0) || !include || (Array.isArray(include) && include.length === 0) ||
(include.constructor.name == 'Object' && Object.keys(include).length == 0) (isPlainObject(include) && Object.keys(include).length === 0)
) { ) {
cb(null, objects); cb(null, objects);
return; return;
@ -48,7 +52,7 @@ Inclusion.include = function (objects, include, cb) {
nbCallbacks++; nbCallbacks++;
callback(function () { callback(function () {
nbCallbacks--; nbCallbacks--;
if (nbCallbacks == 0) { if (nbCallbacks === 0) {
cb(null, objects); cb(null, objects);
} }
}); });
@ -61,7 +65,7 @@ Inclusion.include = function (objects, include, cb) {
if (typeof ij === 'string') { if (typeof ij === 'string') {
ij = [ij]; ij = [ij];
} }
if (ij.constructor.name === 'Object') { if (isPlainObject(ij)) {
var newIj = []; var newIj = [];
for (var key in ij) { for (var key in ij) {
var obj = {}; var obj = {};
@ -76,12 +80,13 @@ Inclusion.include = function (objects, include, cb) {
function processIncludeItem(objs, include, keyVals, objsByKeys) { function processIncludeItem(objs, include, keyVals, objsByKeys) {
var relations = self.relations; var relations = self.relations;
if (include.constructor.name === 'Object') { var relationName, subInclude;
var relationName = Object.keys(include)[0]; if (isPlainObject(include)) {
var subInclude = include[relationName]; relationName = Object.keys(include)[0];
subInclude = include[relationName];
} else { } else {
var relationName = include; relationName = include;
var subInclude = []; subInclude = [];
} }
var relation = relations[relationName]; var relation = relations[relationName];
@ -89,7 +94,7 @@ Inclusion.include = function (objects, include, cb) {
return function () { return function () {
cb(new Error('Relation "' + relationName + '" is not defined for ' cb(new Error('Relation "' + relationName + '" is not defined for '
+ self.modelName + ' model')); + self.modelName + ' model'));
} };
} }
var req = {'where': {}}; var req = {'where': {}};
@ -117,18 +122,17 @@ Inclusion.include = function (objects, include, cb) {
} }
} }
req['where'][relation.keyTo] = {inq: inValues}; req.where[relation.keyTo] = {inq: inValues};
req['include'] = subInclude; req.include = subInclude;
return function (cb) { return function (cb) {
relation.modelTo.find(req, function (err, objsIncluded) { relation.modelTo.find(req, function (err, objsIncluded) {
var objectsFrom, j;
for (var i = 0; i < objsIncluded.length; i++) { for (var i = 0; i < objsIncluded.length; i++) {
delete keysToBeProcessed[objsIncluded[i][relation.keyTo]]; delete keysToBeProcessed[objsIncluded[i][relation.keyTo]];
var objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]]; objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]];
for (var j = 0; j < objectsFrom.length; j++) { for (j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) { defineCachedRelations(objectsFrom[j]);
objectsFrom[j].__cachedRelations = {};
}
if (relation.multiple) { if (relation.multiple) {
if (!objectsFrom[j].__cachedRelations[relationName]) { if (!objectsFrom[j].__cachedRelations[relationName]) {
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 // No relation have been found for these keys
for (var key in keysToBeProcessed) { for (var key in keysToBeProcessed) {
var objectsFrom = objsByKeys[relation.keyFrom][key]; objectsFrom = objsByKeys[relation.keyFrom][key];
for (var j = 0; j < objectsFrom.length; j++) { for (j = 0; j < objectsFrom.length; j++) {
if (!objectsFrom[j].__cachedRelations) { defineCachedRelations(objectsFrom[j]);
objectsFrom[j].__cachedRelations = {};
}
objectsFrom[j].__cachedRelations[relationName] = objectsFrom[j].__cachedRelations[relationName] =
relation.multiple ? [] : null; relation.multiple ? [] : null;
} }
@ -158,5 +160,5 @@ Inclusion.include = function (objects, include, cb) {
return null; return null;
} }
} };

View File

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

View File

@ -27,8 +27,13 @@ var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
* @constructor * @constructor
* @param {Object} data - initial object data * @param {Object} data - initial object data
*/ */
function ModelBaseClass(data) { function ModelBaseClass(data, options) {
this._initProperties(data, true); 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 // 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; return data;
} }
/** /**
* Initialize properties * Initialize the model instance with a list of properties
* @param data * @param {Object} data The data object
* @param applySetters * @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 * @private
*/ */
ModelBaseClass.prototype._initProperties = function (data, applySetters) { ModelBaseClass.prototype._initProperties = function (data, options) {
var self = this; var self = this;
var ctor = this.constructor; var ctor = this.constructor;
var properties = ctor.definition.build(); var properties = ctor.definition.build();
data = data || {}; data = data || {};
options = options || {};
var applySetters = options.applySetters;
var strict = options.strict;
if(strict === undefined) {
strict = ctor.definition.settings.strict;
}
Object.defineProperty(this, '__cachedRelations', { Object.defineProperty(this, '__cachedRelations', {
writable: true, writable: true,
enumerable: false, enumerable: false,
@ -76,15 +91,32 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
value: {} 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) { 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]); this.__data[i] = this.__dataWas[i] = clone(data[i]);
} else if (i in ctor.relations) { } else if (i in ctor.relations) {
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo]; 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) { if (applySetters === true) {
for (var propertyName in data) { 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]; 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 // Set the unknown properties as properties to the object
if (strict === false) { if (strict === false) {
for (var propertyName in data) { for (var propertyName in data) {
if (!(propertyName in properties)) { if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) {
self[propertyName] = self.__data[propertyName] || data[propertyName]; self[propertyName] = self.__data[propertyName] || data[propertyName];
} }
} }
@ -117,7 +149,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
ctor.forEachProperty(function (propertyName) { ctor.forEachProperty(function (propertyName) {
if ('undefined' === typeof self.__data[propertyName]) { if (undefined === self.__data[propertyName]) {
self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName); self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName);
} else { } else {
self.__dataWas[propertyName] = self.__data[propertyName]; self.__dataWas[propertyName] = self.__data[propertyName];
@ -160,7 +192,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) {
} }
this.trigger('initialize'); this.trigger('initialize');
} };
/** /**
* @param {String} prop - property name * @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 * @param {Boolean} onlySchema - restrict properties to dataSource only,
* when onlySchema == true, only properties defined in dataSource returned, * default to false. When onlySchema is true, only properties defined in
* otherwise all enumerable properties returned * the schema are returned, otherwise all enumerable properties returned
* @returns {Object} - canonical object representation (no getters and setters) * @returns {Object} - canonical object representation (no getters and setters)
*/ */
ModelBaseClass.prototype.toObject = function (onlySchema) { ModelBaseClass.prototype.toObject = function (onlySchema) {
if(onlySchema === undefined) {
onlySchema = true;
}
var data = {}; var data = {};
var self = this; 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) { this.constructor.forEachProperty(function (propertyName) {
if (self[propertyName] instanceof List) { if (self[propertyName] instanceof List) {
data[propertyName] = self[propertyName].toObject(!schemaLess); data[propertyName] = self[propertyName].toObject(!schemaLess);
@ -223,10 +260,25 @@ ModelBaseClass.prototype.toObject = function (onlySchema) {
} }
}); });
var val = null;
if (schemaLess) { 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)) { 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) { if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess); data[propertyName] = val.toObject(!schemaLess);
} else { } else {
@ -238,13 +290,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema) {
return data; return data;
}; };
// ModelBaseClass.prototype.hasOwnProperty = function (prop) {
// return this.__data && this.__data.hasOwnProperty(prop) ||
// Object.getOwnPropertyNames(this).indexOf(prop) !== -1;
// };
ModelBaseClass.prototype.toJSON = function () { ModelBaseClass.prototype.toJSON = function () {
return this.toObject(); return this.toObject(false);
}; };
ModelBaseClass.prototype.fromObject = function (obj) { ModelBaseClass.prototype.fromObject = function (obj) {
@ -291,10 +338,15 @@ ModelBaseClass.mixin = function (anotherClass, options) {
ModelBaseClass.prototype.getDataSource = function () { ModelBaseClass.prototype.getDataSource = function () {
return this.__dataSource || this.constructor.dataSource; return this.__dataSource || this.constructor.dataSource;
} };
ModelBaseClass.getDataSource = function () { ModelBaseClass.getDataSource = function () {
return this.dataSource; return this.dataSource;
} };
ModelBaseClass.prototype.setStrict = function (strict) {
this.__strict = strict;
};
jutil.mixin(ModelBaseClass, Hookable); jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable); 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 * Declare hasMany relation
* *
@ -34,11 +46,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
anotherClass = params.model; anotherClass = params.model;
} else { } else {
var anotherClassName = i8n.singularize(anotherClass).toLowerCase(); var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
for (var name in this.dataSource.modelBuilder.models) { anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.dataSource.modelBuilder.models[name];
}
}
} }
} }
var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true); var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true);
@ -130,9 +138,13 @@ Relation.hasMany = function hasMany(anotherClass, params) {
function find(id, cb) { function find(id, cb) {
anotherClass.findById(id, function (err, inst) { anotherClass.findById(id, function (err, inst) {
if (err) return cb(err); if (err) {
if (!inst) return cb(new Error('Not found')); return cb(err);
if (inst[fk] && inst[fk].toString() == this[idName].toString()) { }
if (!inst) {
return cb(new Error('Not found'));
}
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
cb(null, inst); cb(null, inst);
} else { } else {
cb(new Error('Permission denied')); cb(new Error('Permission denied'));
@ -143,9 +155,13 @@ Relation.hasMany = function hasMany(anotherClass, params) {
function destroy(id, cb) { function destroy(id, cb) {
var self = this; var self = this;
anotherClass.findById(id, function (err, inst) { anotherClass.findById(id, function (err, inst) {
if (err) return cb(err); if (err) {
if (!inst) return cb(new Error('Not found')); return cb(err);
if (inst[fk] && inst[fk].toString() == self[idName].toString()) { }
if (!inst) {
return cb(new Error('Not found'));
}
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
inst.destroy(cb); inst.destroy(cb);
} else { } else {
cb(new Error('Permission denied')); cb(new Error('Permission denied'));
@ -186,11 +202,7 @@ Relation.belongsTo = function (anotherClass, params) {
anotherClass = params.model; anotherClass = params.model;
} else { } else {
var anotherClassName = anotherClass.toLowerCase(); var anotherClassName = anotherClass.toLowerCase();
for (var name in this.dataSource.modelBuilder.models) { anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
if (name.toLowerCase() === anotherClassName) {
anotherClass = this.dataSource.modelBuilder.models[name];
}
}
} }
} }
@ -207,16 +219,20 @@ Relation.belongsTo = function (anotherClass, params) {
}; };
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName); 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) { if (id === null) {
cb(null, null); cb(null, null);
return; return;
} }
anotherClass.findById(id, function (err, inst) { anotherClass.findById(id, function (err, inst) {
if (err) return cb(err); if (err) {
if (!inst) return cb(null, null); return cb(err);
}
if (!inst) {
return cb(null, null);
}
if (inst[idName] === this[fk]) { if (inst[idName] === this[fk]) {
cb(null, inst); cb(null, inst);
} else { } else {
@ -234,7 +250,7 @@ Relation.belongsTo = function (anotherClass, params) {
} }
var self = this; var self = this;
var cachedValue; var cachedValue;
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) { if (!refresh && this.__cachedRelations && (this.__cachedRelations[methodName] !== undefined)) {
cachedValue = this.__cachedRelations[methodName]; cachedValue = this.__cachedRelations[methodName];
} }
if (p instanceof ModelBaseClass) { // acts as setter if (p instanceof ModelBaseClass) { // acts as setter
@ -277,7 +293,7 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
if (params.model) { if (params.model) {
anotherClass = params.model; anotherClass = params.model;
} else { } else {
anotherClass = lookupModel(i8n.singularize(anotherClass)) || anotherClass = lookupModel(models, i8n.singularize(anotherClass).toLowerCase()) ||
anotherClass; anotherClass;
} }
if (typeof anotherClass === 'string') { if (typeof anotherClass === 'string') {
@ -288,7 +304,7 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
if (!params.through) { if (!params.through) {
var name1 = this.modelName + anotherClass.modelName; var name1 = this.modelName + anotherClass.modelName;
var name2 = anotherClass.modelName + this.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); this.dataSource.define(name1);
} }
params.through.belongsTo(this); params.through.belongsTo(this);
@ -296,13 +312,4 @@ Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params
this.hasMany(anotherClass, {as: params.as, through: params.through}); 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 * 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'); 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 self = this;
var params = mergeParams(actualCond, caller._scope); var params = mergeParams(actualCond, caller._scope);
return targetClass.find(params, function (err, data) { return targetClass.find(params, function (err, data) {
if (!err && saveOnCache) { if (!err && saveOnCache) {
if (!self.__cachedRelations) { defineCachedRelations(self);
self.__cachedRelations = {};
}
self.__cachedRelations[name] = data; self.__cachedRelations[name] = data;
} }
cb(err, data); cb(err, data);

View File

@ -19,6 +19,22 @@ util.inherits(BaseSQL, Connector);
*/ */
BaseSQL.prototype.relational = true; 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 () { BaseSQL.prototype.query = function () {
throw new Error('query method should be declared in connector'); throw new Error('query method should be declared in connector');
}; };

View File

@ -4,6 +4,8 @@ exports.selectFields = selectFields;
exports.removeUndefined = removeUndefined; exports.removeUndefined = removeUndefined;
exports.parseSettings = parseSettings; exports.parseSettings = parseSettings;
exports.mergeSettings = mergeSettings; exports.mergeSettings = mergeSettings;
exports.isPlainObject = isPlainObject;
exports.defineCachedRelations = defineCachedRelations;
var traverse = require('traverse'); var traverse = require('traverse');
@ -176,4 +178,29 @@ function mergeSettings(target, src) {
} }
return dst; 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", "name": "loopback-datasource-juggler",
"version": "1.2.13", "version": "1.3.0",
"description": "LoopBack DataSoure Juggler", "description": "LoopBack DataSoure Juggler",
"keywords": [ "keywords": [
"StrongLoop", "StrongLoop",
@ -27,10 +27,10 @@
"mocha": "~1.12.1" "mocha": "~1.12.1"
}, },
"dependencies": { "dependencies": {
"async": "~0.2.9", "async": "~0.2.10",
"inflection": "~1.2.6", "inflection": "~1.3.3",
"traverse": "~0.6.5", "traverse": "~0.6.6",
"qs": "~0.6.5" "qs": "~0.6.6"
}, },
"license": "MIT" "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) { function addHooks(name, done) {
var called = false, random = String(Math.floor(Math.random() * 1000)); var called = false, random = String(Math.floor(Math.random() * 1000));
User['before' + name] = function (next, data) { User['before' + name] = function (next, data) {

View File

@ -13,6 +13,12 @@ describe('include', function () {
passports.length.should.be.ok; passports.length.should.be.ok;
passports.forEach(function (p) { passports.forEach(function (p) {
p.__cachedRelations.should.have.property('owner'); 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; var owner = p.__cachedRelations.owner;
if (!p.ownerId) { if (!p.ownerId) {
should.not.exist(owner); should.not.exist(owner);
@ -31,6 +37,11 @@ describe('include', function () {
should.exist(users); should.exist(users);
users.length.should.be.ok; users.length.should.be.ok;
users.forEach(function (u) { 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.should.have.property('posts');
u.__cachedRelations.posts.forEach(function (p) { u.__cachedRelations.posts.forEach(function (p) {
p.userId.should.equal(u.id); p.userId.should.equal(u.id);
@ -47,6 +58,12 @@ describe('include', function () {
passports.length.should.be.ok; passports.length.should.be.ok;
passports.forEach(function (p) { passports.forEach(function (p) {
p.__cachedRelations.should.have.property('owner'); 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; var user = p.__cachedRelations.owner;
if (!p.ownerId) { if (!p.ownerId) {
should.not.exist(user); should.not.exist(user);
@ -97,6 +114,12 @@ describe('include', function () {
should.exist(users); should.exist(users);
users.length.should.be.ok; users.length.should.be.ok;
users.forEach(function (user) { 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('posts');
user.__cachedRelations.should.have.property('passports'); user.__cachedRelations.should.have.property('passports');
user.__cachedRelations.posts.forEach(function (p) { user.__cachedRelations.posts.forEach(function (p) {

View File

@ -46,11 +46,35 @@ describe('ModelBuilder define model', function () {
User.modelName.should.equal('User'); User.modelName.should.equal('User');
user.should.be.a('object'); user.should.be.a('object');
assert(user.name === 'Joe'); user.should.have.property('name', 'Joe');
assert(user.age === undefined); user.should.not.have.property('age');
assert(user.toObject().age === undefined); user.toObject().should.not.have.property('age');
assert(user.toObject(true).age === undefined); user.toObject(true).should.not.have.property('age');
assert(user.bio === undefined); 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); done(null, User);
}); });
@ -83,6 +107,31 @@ describe('ModelBuilder define model', function () {
done(null, User); 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) { it('should use false as the default value for strict', function (done) {
var modelBuilder = new ModelBuilder(); 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 () { 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 () { describe('DataSource constructor', function () {
// Mocked require // Mocked require
var loader = function (name) { 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);
});
});
});