Refactor the CRUD operations to DataAccessObject

This commit is contained in:
Raymond Feng 2013-05-17 08:49:57 -07:00
parent 3d82fc10b9
commit 630b991d1d
10 changed files with 704 additions and 611 deletions

2
.gitignore vendored
View File

@ -3,7 +3,7 @@ doc
coverage.html
coverage
v8.log
.idea
.DS_Store
benchmark.js
analyse.r

38
examples/app.js Normal file
View File

@ -0,0 +1,38 @@
var DataSource = require('../../jugglingdb').Schema;
var dataSource = new DataSource();
// define models
var Post = dataSource.define('Post', {
title: { type: String, length: 255 },
content: { type: DataSource.Text },
date: { type: Date, default: function () { return new Date;} },
timestamp: { type: Number, default: Date.now },
published: { type: Boolean, default: false, index: true }
});
// simplier way to describe model
var User = dataSource.define('User', {
name: String,
bio: DataSource.Text,
approved: Boolean,
joinedAt: Date,
age: Number
});
var Group = dataSource.define('Group', {name: String});
// define any custom method
User.prototype.getNameAndAge = function () {
return this.name + ', ' + this.age;
};
var user = new User({name: 'Joe'});
console.log(user);
console.log(dataSource.models);
console.log(dataSource.definitions);
var user2 = User.create({name: 'Joe'});
console.log(user2);

566
lib/dao.js Normal file
View File

@ -0,0 +1,566 @@
/**
* Module exports class Model
*/
module.exports = DataAccessObject;
/**
* Module dependencies
*/
var util = require('util');
var validations = require('./validations.js');
var ValidationError = validations.ValidationError;
var List = require('./list.js');
require('./hooks.js');
require('./relations.js');
require('./include.js');
/**
* DAO class - base class for all persist objects
* provides **common API** to access any database adapter.
* This class describes only abstract behavior layer, refer to `lib/adapters/*.js`
* to learn more about specific adapter implementations
*
* `DataAccessObject` mixes `Validatable` and `Hookable` classes methods
*
* @constructor
* @param {Object} data - initial object data
*/
function DataAccessObject() {
}
DataAccessObject._forDB = function (data) {
var res = {};
Object.keys(data).forEach(function (propName) {
if (this.whatTypeName(propName) === 'JSON' || data[propName] instanceof Array) {
res[propName] = JSON.stringify(data[propName]);
} else {
res[propName] = data[propName];
}
}.bind(this));
return res;
};
/**
* Create new instance of Model class, saved in database
*
* @param data [optional]
* @param callback(err, obj)
* callback called with arguments:
*
* - err (null or Error)
* - instance (null or Model)
*/
DataAccessObject.create = function (data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
var Model = this;
var modelName = Model.modelName;
if (typeof data === 'function') {
callback = data;
data = {};
}
if (typeof callback !== 'function') {
callback = function () {};
}
if (!data) {
data = {};
}
if (data instanceof Array) {
var instances = [];
var errors = Array(data.length);
var gotError = false;
var wait = data.length;
if (wait === 0) callback(null, []);
var instances = [];
for (var i = 0; i < data.length; i += 1) {
(function(d, i) {
instances.push(Model.create(d, function(err, inst) {
if (err) {
errors[i] = err;
gotError = true;
}
modelCreated();
}));
})(data[i], i);
}
return instances;
function modelCreated() {
if (--wait === 0) {
callback(gotError ? errors : null, instances);
}
}
}
var obj;
// if we come from save
if (data instanceof Model && !data.id) {
obj = data;
} else {
obj = new Model(data);
}
data = obj.toObject(true);
// validation required
obj.isValid(function(valid) {
if (valid) {
create();
} else {
callback(new ValidationError(obj), obj);
}
}, data);
function create() {
obj.trigger('create', function(createDone) {
obj.trigger('save', function(saveDone) {
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
if (id) {
obj.__data.id = id;
obj.__dataWas.id = id;
defineReadonlyProp(obj, 'id', id);
}
if (rev) {
obj._rev = rev
}
if (err) {
return callback(err, obj);
}
saveDone.call(obj, function () {
createDone.call(obj, function () {
callback(err, obj);
});
});
}, obj);
}, obj);
}, obj);
}
return obj;
};
function stillConnecting(schema, obj, args) {
if (schema.connected) return false;
if (schema.connecting) return true;
var method = args.callee;
schema.once('connected', function () {
method.apply(obj, [].slice.call(args));
});
schema.connect();
return true;
};
/**
* Update or insert
*/
DataAccessObject.upsert = DataAccessObject.updateOrCreate = function upsert(data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
var Model = this;
if (!data.id) return this.create(data, callback);
if (this.schema.adapter.updateOrCreate) {
var inst = new Model(data);
this.schema.adapter.updateOrCreate(Model.modelName, inst.toObject(true), function (err, data) {
var obj;
if (data) {
inst._initProperties(data);
obj = inst;
} else {
obj = null;
}
callback(err, obj);
});
} else {
this.find(data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
inst.updateAttributes(data, callback);
} else {
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};
/**
* Find one record, same as `all`, limited by 1 and return object, not collection,
* if not found, create using data provided as second argument
*
* @param {Object} query - search conditions: {where: {test: 'me'}}.
* @param {Object} data - object to create.
* @param {Function} cb - callback called with (err, instance)
*/
DataAccessObject.findOrCreate = function findOrCreate(query, data, callback) {
if (typeof query === 'undefined') {
query = {where: {}};
}
if (typeof data === 'function' || typeof data === 'undefined') {
callback = data;
data = query && query.where;
}
if (typeof callback === 'undefined') {
callback = function () {};
}
var t = this;
this.findOne(query, function (err, record) {
if (err) return callback(err);
if (record) return callback(null, record);
t.create(data, callback);
});
};
/**
* Check whether object exitst in database
*
* @param {id} id - identifier of object (primary key value)
* @param {Function} cb - callbacl called with (err, exists: Bool)
*/
DataAccessObject.exists = function exists(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (id) {
this.schema.adapter.exists(this.modelName, id, cb);
} else {
cb(new Error('Model::exists requires positive id argument'));
}
};
/**
* Find object by id
*
* @param {id} id - primary key value
* @param {Function} cb - callback called with (err, instance)
*/
DataAccessObject.find = function find(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
this.schema.adapter.find(this.modelName, id, function (err, data) {
var obj = null;
if (data) {
if (!data.id) {
data.id = id;
}
obj = new this();
obj._initProperties(data, false);
}
cb(err, obj);
}.bind(this));
};
/**
* Find all instances of Model, matched by query
* make sure you have marked as `index: true` fields for filter or sort
*
* @param {Object} params (optional)
*
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
* - include: String, Object or Array. See DataAccessObject.include documentation.
* - order: String
* - limit: Number
* - skip: Number
*
* @param {Function} callback (required) called with arguments:
*
* - err (null or Error)
* - Array of instances
*/
DataAccessObject.all = function all(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (arguments.length === 1) {
cb = params;
params = null;
}
var constr = this;
this.schema.adapter.all(this.modelName, params, function (err, data) {
if (data && data.forEach) {
data.forEach(function (d, i) {
var obj = new constr;
obj._initProperties(d, false);
if (params && params.include && params.collect) {
data[i] = obj.__cachedRelations[params.collect];
} else {
data[i] = obj;
}
});
if (data && data.countBeforeLimit) {
data.countBeforeLimit = data.countBeforeLimit;
}
cb(err, data);
}
else
cb(err, []);
});
};
/**
* Find one record, same as `all`, limited by 1 and return object, not collection
*
* @param {Object} params - search conditions: {where: {test: 'me'}}
* @param {Function} cb - callback called with (err, instance)
*/
DataAccessObject.findOne = function findOne(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (typeof params === 'function') {
cb = params;
params = {};
}
params.limit = 1;
this.all(params, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err, null);
cb(err, collection[0]);
});
};
/**
* Destroy all records
* @param {Function} cb - callback called with (err)
*/
DataAccessObject.destroyAll = function destroyAll(cb) {
if (stillConnecting(this.schema, this, arguments)) return;
this.schema.adapter.destroyAll(this.modelName, function (err) {
if ('function' === typeof cb) {
cb(err);
}
}.bind(this));
};
/**
* Return count of matched records
*
* @param {Object} where - search conditions (optional)
* @param {Function} cb - callback, called with (err, count)
*/
DataAccessObject.count = function (where, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (typeof where === 'function') {
cb = where;
where = null;
}
this.schema.adapter.count(this.modelName, cb, where);
};
/**
* Save instance. When instance haven't id, create method called instead.
* Triggers: validate, save, update | create
* @param options {validate: true, throws: false} [optional]
* @param callback(err, obj)
*/
DataAccessObject.prototype.save = function (options, callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
if (typeof options == 'function') {
callback = options;
options = {};
}
callback = callback || function () {};
options = options || {};
if (!('validate' in options)) {
options.validate = true;
}
if (!('throws' in options)) {
options.throws = false;
}
var inst = this;
var data = inst.toObject(true);
var Model = this.constructor;
var modelName = Model.modelName;
if (!this.id) {
return Model.create(this, callback);
}
// validate first
if (!options.validate) {
return save();
}
inst.isValid(function (valid) {
if (valid) {
save();
} else {
var err = new ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
}
});
// then save
function save() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (updateDone) {
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
if (err) {
return callback(err, inst);
}
inst._initProperties(data, false);
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
});
});
});
}, data);
}, data);
}
};
DataAccessObject.prototype.isNewRecord = function () {
return !this.id;
};
/**
* Return adapter of current record
* @private
*/
DataAccessObject.prototype._adapter = function () {
return this.schema.adapter;
};
/**
* Delete object from persistence
*
* @triggers `destroy` hook (async) before and after destroying object
*/
DataAccessObject.prototype.destroy = function (cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, this.id, function (err) {
if (err) {
return cb(err);
}
destroyed(function () {
if(cb) cb();
});
}.bind(this));
});
};
/**
* Update single attribute
*
* equals to `updateAttributes({name: value}, cb)
*
* @param {String} name - name of property
* @param {Mixed} value - value of property
* @param {Function} callback - callback called with (err, instance)
*/
DataAccessObject.prototype.updateAttribute = function updateAttribute(name, value, callback) {
var data = {};
data[name] = value;
this.updateAttributes(data, callback);
};
/**
* Update set of attributes
*
* this method performs validation before updating
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data - data to update
* @param {Function} callback - callback called with (err, instance)
*/
DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
var inst = this;
var model = this.constructor.modelName;
if (typeof data === 'function') {
cb = data;
data = null;
}
if (!data) {
data = {};
}
// update instance's properties
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
});
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
cb(new ValidationError(inst), inst);
}
} else {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
});
inst._adapter().updateAttributes(model, inst.id, inst.constructor._forDB(data), function (err) {
if (!err) {
// update _was attrs
Object.keys(data).forEach(function (key) {
inst.__dataWas[key] = inst.__data[key];
});
}
done.call(inst, function () {
saveDone.call(inst, function () {
cb(err, inst);
});
});
});
}, data);
}, data);
}
}, data);
};
/**
* Reload object from persistence
*
* @requires `id` member of `object` to be able to call `find`
* @param {Function} callback - called with (err, instance) arguments
*/
DataAccessObject.prototype.reload = function reload(callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
this.constructor.find(this.id, callback);
};
/**
* Define readonly property on object
*
* @param {Object} obj
* @param {String} key
* @param {Mixed} value
*/
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
enumerable: true,
configurable: true,
value: value
});
}

View File

@ -6,7 +6,7 @@ exports.Hookable = Hookable;
/**
* Hooks mixins for ./model.js
*/
var Hookable = require('./model.js');
var Hookable = require('./dao.js');
/**
* List of hooks available
@ -23,10 +23,11 @@ Hookable.afterUpdate = null;
Hookable.beforeDestroy = null;
Hookable.afterDestroy = null;
// TODO: Evaluate https://github.com/bnoguchi/hooks-js/
Hookable.prototype.trigger = function trigger(actionName, work, data) {
var capitalizedName = capitalize(actionName);
var beforeHook = this.constructor["before" + capitalizedName];
var afterHook = this.constructor["after" + capitalizedName];
var beforeHook = this.constructor["before" + capitalizedName] || this.constructor["pre" + capitalizedName];
var afterHook = this.constructor["after" + capitalizedName] || this.constructor["post" + capitalizedName];
if (actionName === 'validate') {
beforeHook = beforeHook || this.constructor.beforeValidation;
afterHook = afterHook || this.constructor.afterValidation;

View File

@ -1,7 +1,7 @@
/**
* Include mixin for ./model.js
*/
var AbstractClass = require('./model.js');
var DataAccessObject = require('./dao.js');
/**
* Allows you to load relations of several objects and optimize numbers of requests.
@ -22,7 +22,7 @@ var AbstractClass = require('./model.js');
* - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ...
*
*/
AbstractClass.include = function (objects, include, cb) {
DataAccessObject.include = function (objects, include, cb) {
var self = this;
if (

View File

@ -1,18 +1,14 @@
/**
* Module exports class Model
*/
module.exports = AbstractClass;
module.exports = ModelBaseClass;
/**
* Module dependencies
*/
var util = require('util');
var validations = require('./validations.js');
var ValidationError = validations.ValidationError;
var List = require('./list.js');
require('./hooks.js');
require('./relations.js');
require('./include.js');
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
@ -22,16 +18,16 @@ var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text'];
* This class describes only abstract behavior layer, refer to `lib/adapters/*.js`
* to learn more about specific adapter implementations
*
* `AbstractClass` mixes `Validatable` and `Hookable` classes methods
* `ModelBaseClass` mixes `Validatable` and `Hookable` classes methods
*
* @constructor
* @param {Object} data - initial object data
*/
function AbstractClass(data) {
function ModelBaseClass(data) {
this._initProperties(data, true);
}
AbstractClass.prototype._initProperties = function (data, applySetters) {
ModelBaseClass.prototype._initProperties = function (data, applySetters) {
var self = this;
var ctor = this.constructor;
var ds = ctor.schema.definitions[ctor.modelName];
@ -127,11 +123,11 @@ AbstractClass.prototype._initProperties = function (data, applySetters) {
* @param {String} prop - property name
* @param {Object} params - various property configuration
*/
AbstractClass.defineProperty = function (prop, params) {
ModelBaseClass.defineProperty = function (prop, params) {
this.schema.defineProperty(this.modelName, prop, params);
};
AbstractClass.whatTypeName = function (propName) {
ModelBaseClass.whatTypeName = function (propName) {
var prop = this.schema.definitions[this.modelName].properties[propName];
if (!prop || !prop.type) {
throw new Error('Undefined type for ' + this.modelName + ':' + propName);
@ -139,426 +135,19 @@ AbstractClass.whatTypeName = function (propName) {
return prop.type.name;
};
AbstractClass._forDB = function (data) {
var res = {};
Object.keys(data).forEach(function (propName) {
if (this.whatTypeName(propName) === 'JSON' || data[propName] instanceof Array) {
res[propName] = JSON.stringify(data[propName]);
} else {
res[propName] = data[propName];
}
}.bind(this));
return res;
};
AbstractClass.prototype.whatTypeName = function (propName) {
ModelBaseClass.prototype.whatTypeName = function (propName) {
return this.constructor.whatTypeName(propName);
};
/**
* Create new instance of Model class, saved in database
*
* @param data [optional]
* @param callback(err, obj)
* callback called with arguments:
*
* - err (null or Error)
* - instance (null or Model)
*/
AbstractClass.create = function (data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
var Model = this;
var modelName = Model.modelName;
if (typeof data === 'function') {
callback = data;
data = {};
}
if (typeof callback !== 'function') {
callback = function () {};
}
if (!data) {
data = {};
}
if (data instanceof Array) {
var instances = [];
var errors = Array(data.length);
var gotError = false;
var wait = data.length;
if (wait === 0) callback(null, []);
var instances = [];
for (var i = 0; i < data.length; i += 1) {
(function(d, i) {
instances.push(Model.create(d, function(err, inst) {
if (err) {
errors[i] = err;
gotError = true;
}
modelCreated();
}));
})(data[i], i);
}
return instances;
function modelCreated() {
if (--wait === 0) {
callback(gotError ? errors : null, instances);
}
}
}
var obj;
// if we come from save
if (data instanceof Model && !data.id) {
obj = data;
} else {
obj = new Model(data);
}
data = obj.toObject(true);
// validation required
obj.isValid(function(valid) {
if (valid) {
create();
} else {
callback(new ValidationError(obj), obj);
}
}, data);
function create() {
obj.trigger('create', function(createDone) {
obj.trigger('save', function(saveDone) {
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
if (id) {
obj.__data.id = id;
obj.__dataWas.id = id;
defineReadonlyProp(obj, 'id', id);
}
if (rev) {
obj._rev = rev
}
if (err) {
return callback(err, obj);
}
saveDone.call(obj, function () {
createDone.call(obj, function () {
callback(err, obj);
});
});
}, obj);
}, obj);
}, obj);
}
return obj;
};
function stillConnecting(schema, obj, args) {
if (schema.connected) return false;
if (schema.connecting) return true;
var method = args.callee;
schema.once('connected', function () {
method.apply(obj, [].slice.call(args));
});
schema.connect();
return true;
};
/**
* Update or insert
*/
AbstractClass.upsert = AbstractClass.updateOrCreate = function upsert(data, callback) {
if (stillConnecting(this.schema, this, arguments)) return;
var Model = this;
if (!data.id) return this.create(data, callback);
if (this.schema.adapter.updateOrCreate) {
var inst = new Model(data);
this.schema.adapter.updateOrCreate(Model.modelName, inst.toObject(true), function (err, data) {
var obj;
if (data) {
inst._initProperties(data);
obj = inst;
} else {
obj = null;
}
callback(err, obj);
});
} else {
this.find(data.id, function (err, inst) {
if (err) return callback(err);
if (inst) {
inst.updateAttributes(data, callback);
} else {
var obj = new Model(data);
obj.save(data, callback);
}
});
}
};
/**
* Find one record, same as `all`, limited by 1 and return object, not collection,
* if not found, create using data provided as second argument
*
* @param {Object} query - search conditions: {where: {test: 'me'}}.
* @param {Object} data - object to create.
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.findOrCreate = function findOrCreate(query, data, callback) {
if (typeof query === 'undefined') {
query = {where: {}};
}
if (typeof data === 'function' || typeof data === 'undefined') {
callback = data;
data = query && query.where;
}
if (typeof callback === 'undefined') {
callback = function () {};
}
var t = this;
this.findOne(query, function (err, record) {
if (err) return callback(err);
if (record) return callback(null, record);
t.create(data, callback);
});
};
/**
* Check whether object exitst in database
*
* @param {id} id - identifier of object (primary key value)
* @param {Function} cb - callbacl called with (err, exists: Bool)
*/
AbstractClass.exists = function exists(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (id) {
this.schema.adapter.exists(this.modelName, id, cb);
} else {
cb(new Error('Model::exists requires positive id argument'));
}
};
/**
* Find object by id
*
* @param {id} id - primary key value
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.find = function find(id, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
this.schema.adapter.find(this.modelName, id, function (err, data) {
var obj = null;
if (data) {
if (!data.id) {
data.id = id;
}
obj = new this();
obj._initProperties(data, false);
}
cb(err, obj);
}.bind(this));
};
/**
* Find all instances of Model, matched by query
* make sure you have marked as `index: true` fields for filter or sort
*
* @param {Object} params (optional)
*
* - where: Object `{ key: val, key2: {gt: 'val2'}}`
* - include: String, Object or Array. See AbstractClass.include documentation.
* - order: String
* - limit: Number
* - skip: Number
*
* @param {Function} callback (required) called with arguments:
*
* - err (null or Error)
* - Array of instances
*/
AbstractClass.all = function all(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (arguments.length === 1) {
cb = params;
params = null;
}
var constr = this;
this.schema.adapter.all(this.modelName, params, function (err, data) {
if (data && data.forEach) {
data.forEach(function (d, i) {
var obj = new constr;
obj._initProperties(d, false);
if (params && params.include && params.collect) {
data[i] = obj.__cachedRelations[params.collect];
} else {
data[i] = obj;
}
});
if (data && data.countBeforeLimit) {
data.countBeforeLimit = data.countBeforeLimit;
}
cb(err, data);
}
else
cb(err, []);
});
};
/**
* Find one record, same as `all`, limited by 1 and return object, not collection
*
* @param {Object} params - search conditions: {where: {test: 'me'}}
* @param {Function} cb - callback called with (err, instance)
*/
AbstractClass.findOne = function findOne(params, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (typeof params === 'function') {
cb = params;
params = {};
}
params.limit = 1;
this.all(params, function (err, collection) {
if (err || !collection || !collection.length > 0) return cb(err, null);
cb(err, collection[0]);
});
};
/**
* Destroy all records
* @param {Function} cb - callback called with (err)
*/
AbstractClass.destroyAll = function destroyAll(cb) {
if (stillConnecting(this.schema, this, arguments)) return;
this.schema.adapter.destroyAll(this.modelName, function (err) {
if ('function' === typeof cb) {
cb(err);
}
}.bind(this));
};
/**
* Return count of matched records
*
* @param {Object} where - search conditions (optional)
* @param {Function} cb - callback, called with (err, count)
*/
AbstractClass.count = function (where, cb) {
if (stillConnecting(this.schema, this, arguments)) return;
if (typeof where === 'function') {
cb = where;
where = null;
}
this.schema.adapter.count(this.modelName, cb, where);
};
/**
* Return string representation of class
*
* @override default toString method
*/
AbstractClass.toString = function () {
ModelBaseClass.toString = function () {
return '[Model ' + this.modelName + ']';
};
/**
* Save instance. When instance haven't id, create method called instead.
* Triggers: validate, save, update | create
* @param options {validate: true, throws: false} [optional]
* @param callback(err, obj)
*/
AbstractClass.prototype.save = function (options, callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
if (typeof options == 'function') {
callback = options;
options = {};
}
callback = callback || function () {};
options = options || {};
if (!('validate' in options)) {
options.validate = true;
}
if (!('throws' in options)) {
options.throws = false;
}
var inst = this;
var data = inst.toObject(true);
var Model = this.constructor;
var modelName = Model.modelName;
if (!this.id) {
return Model.create(this, callback);
}
// validate first
if (!options.validate) {
return save();
}
inst.isValid(function (valid) {
if (valid) {
save();
} else {
var err = new ValidationError(inst);
// throws option is dangerous for async usage
if (options.throws) {
throw err;
}
callback(err, inst);
}
});
// then save
function save() {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (updateDone) {
inst._adapter().save(modelName, inst.constructor._forDB(data), function (err) {
if (err) {
return callback(err, inst);
}
inst._initProperties(data, false);
updateDone.call(inst, function () {
saveDone.call(inst, function () {
callback(err, inst);
});
});
});
}, data);
}, data);
}
};
AbstractClass.prototype.isNewRecord = function () {
return !this.id;
};
/**
* Return adapter of current record
* @private
*/
AbstractClass.prototype._adapter = function () {
return this.schema.adapter;
};
/**
* Convert instance to Object
*
@ -567,7 +156,7 @@ AbstractClass.prototype._adapter = function () {
* otherwise all enumerable properties returned
* @returns {Object} - canonical object representation (no getters and setters)
*/
AbstractClass.prototype.toObject = function (onlySchema) {
ModelBaseClass.prototype.toObject = function (onlySchema) {
var data = {};
var ds = this.constructor.schema.definitions[this.constructor.modelName];
var properties = ds.properties;
@ -594,113 +183,16 @@ AbstractClass.prototype.toObject = function (onlySchema) {
return data;
};
// AbstractClass.prototype.hasOwnProperty = function (prop) {
// ModelBaseClass.prototype.hasOwnProperty = function (prop) {
// return this.__data && this.__data.hasOwnProperty(prop) ||
// Object.getOwnPropertyNames(this).indexOf(prop) !== -1;
// };
AbstractClass.prototype.toJSON = function () {
ModelBaseClass.prototype.toJSON = function () {
return this.toObject();
};
/**
* Delete object from persistence
*
* @triggers `destroy` hook (async) before and after destroying object
*/
AbstractClass.prototype.destroy = function (cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
this.trigger('destroy', function (destroyed) {
this._adapter().destroy(this.constructor.modelName, this.id, function (err) {
if (err) {
return cb(err);
}
destroyed(function () {
if(cb) cb();
});
}.bind(this));
});
};
/**
* Update single attribute
*
* equals to `updateAttributes({name: value}, cb)
*
* @param {String} name - name of property
* @param {Mixed} value - value of property
* @param {Function} callback - callback called with (err, instance)
*/
AbstractClass.prototype.updateAttribute = function updateAttribute(name, value, callback) {
var data = {};
data[name] = value;
this.updateAttributes(data, callback);
};
/**
* Update set of attributes
*
* this method performs validation before updating
*
* @trigger `validation`, `save` and `update` hooks
* @param {Object} data - data to update
* @param {Function} callback - callback called with (err, instance)
*/
AbstractClass.prototype.updateAttributes = function updateAttributes(data, cb) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
var inst = this;
var model = this.constructor.modelName;
if (typeof data === 'function') {
cb = data;
data = null;
}
if (!data) {
data = {};
}
// update instance's properties
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
});
inst.isValid(function (valid) {
if (!valid) {
if (cb) {
cb(new ValidationError(inst), inst);
}
} else {
inst.trigger('save', function (saveDone) {
inst.trigger('update', function (done) {
Object.keys(data).forEach(function (key) {
inst[key] = data[key];
});
inst._adapter().updateAttributes(model, inst.id, inst.constructor._forDB(data), function (err) {
if (!err) {
// update _was attrs
Object.keys(data).forEach(function (key) {
inst.__dataWas[key] = inst.__data[key];
});
}
done.call(inst, function () {
saveDone.call(inst, function () {
cb(err, inst);
});
});
});
}, data);
}, data);
}
}, data);
};
AbstractClass.prototype.fromObject = function (obj) {
ModelBaseClass.prototype.fromObject = function (obj) {
Object.keys(obj).forEach(function (key) {
this[key] = obj[key];
}.bind(this));
@ -712,29 +204,17 @@ AbstractClass.prototype.fromObject = function (obj) {
* @param {String} attr - property name
* @return Boolean
*/
AbstractClass.prototype.propertyChanged = function propertyChanged(attr) {
ModelBaseClass.prototype.propertyChanged = function propertyChanged(attr) {
return this.__data[attr] !== this.__dataWas[attr];
};
/**
* Reload object from persistence
*
* @requires `id` member of `object` to be able to call `find`
* @param {Function} callback - called with (err, instance) arguments
*/
AbstractClass.prototype.reload = function reload(callback) {
if (stillConnecting(this.constructor.schema, this, arguments)) return;
this.constructor.find(this.id, callback);
};
/**
* Reset dirty attributes
*
* this method does not perform any database operation it just reset object to it's
* initial state
*/
AbstractClass.prototype.reset = function () {
ModelBaseClass.prototype.reset = function () {
var obj = this;
Object.keys(obj).forEach(function (k) {
if (k !== 'id' && !obj.constructor.schema.definitions[obj.constructor.modelName].properties[k]) {
@ -746,7 +226,7 @@ AbstractClass.prototype.reset = function () {
});
};
AbstractClass.prototype.inspect = function () {
ModelBaseClass.prototype.inspect = function () {
return util.inspect(this.__data, false, 4, true);
};
@ -761,18 +241,7 @@ function isdef(s) {
return s !== undef;
}
/**
* Define readonly property on object
*
* @param {Object} obj
* @param {String} key
* @param {Mixed} value
*/
function defineReadonlyProp(obj, key, value) {
Object.defineProperty(obj, key, {
writable: false,
enumerable: true,
configurable: true,
value: value
});
ModelBaseClass.prototype.dataSource = function(name, settings) {
require('./jutil').inherits(this.constructor, require('./dao'));
}

View File

@ -7,7 +7,7 @@ var defineScope = require('./scope.js').defineScope;
/**
* Relations mixins for ./model.js
*/
var Model = require('./model.js');
var Model = require('./dao.js');
Model.relationNameFor = function relationNameFor(foreignKey) {
for (var rel in this.relations) {

View File

@ -1,7 +1,8 @@
/**
* Module dependencies
*/
var AbstractClass = require('./model.js');
var ModelBaseClass = require('./model.js');
var DataAccessObject = require('./dao.js');
var List = require('./list.js');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
@ -14,7 +15,7 @@ var existsSync = fs.existsSync || path.existsSync;
* Export public API
*/
exports.Schema = Schema;
// exports.AbstractClass = AbstractClass;
// exports.ModelBaseClass = ModelBaseClass;
/**
* Helpers
@ -60,7 +61,17 @@ Schema.registerType(Schema.JSON);
* ```
*/
function Schema(name, settings) {
// create blank models pool
this.models = {};
this.definitions = {};
this.dataSource(name, settings);
};
util.inherits(Schema, EventEmitter);
Schema.prototype.dataSource = function(name, settings) {
var schema = this;
// just save everything we get
this.name = name;
this.settings = settings;
@ -69,55 +80,55 @@ function Schema(name, settings) {
this.connected = false;
this.connecting = false;
// create blank models pool
this.models = {};
this.definitions = {};
// and initialize schema using adapter
// this is only one initialization entry point of adapter
// this module should define `adapter` member of `this` (schema)
var adapter;
if (typeof name === 'object') {
adapter = name;
this.name = adapter.name;
} else if (name.match(/^\//)) {
// try absolute path
adapter = require(name);
} else if (existsSync(__dirname + '/adapters/' + name + '.js')) {
// try built-in adapter
adapter = require('./adapters/' + name);
} else {
// try foreign adapter
try {
adapter = require('jugglingdb-' + name);
} catch (e) {
return console.log('\nWARNING: JugglingDB adapter "' + name + '" is not installed,\nso your models would not work, to fix run:\n\n npm install jugglingdb-' + name, '\n');
if (name) {
// and initialize schema using adapter
// this is only one initialization entry point of adapter
// this module should define `adapter` member of `this` (schema)
var adapter;
if (typeof name === 'object') {
adapter = name;
this.name = adapter.name;
} else if (name.match(/^\//)) {
// try absolute path
adapter = require(name);
} else if (existsSync(__dirname + '/adapters/' + name + '.js')) {
// try built-in adapter
adapter = require('./adapters/' + name);
} else {
// try foreign adapter
try {
adapter = require('jugglingdb-' + name);
} catch (e) {
return console.log('\nWARNING: JugglingDB adapter "' + name + '" is not installed,\nso your models would not work, to fix run:\n\n npm install jugglingdb-' + name, '\n');
}
}
}
adapter.initialize(this, function () {
if (adapter) {
adapter.initialize(this, function () {
// we have an adaper now?
if (!this.adapter) {
throw new Error('Adapter is not defined correctly: it should create `adapter` member of schema');
}
// we have an adaper now?
if (!this.adapter) {
throw new Error('Adapter is not defined correctly: it should create `adapter` member of schema');
}
this.adapter.log = function (query, start) {
schema.log(query, start);
};
this.adapter.logger = function (query) {
var t1 = Date.now();
var log = this.log;
return function (q) {
log(q || query, t1);
this.adapter.log = function (query, start) {
schema.log(query, start);
};
};
this.connected = true;
this.emit('connected');
this.adapter.logger = function (query) {
var t1 = Date.now();
var log = this.log;
return function (q) {
log(q || query, t1);
};
};
}.bind(this));
this.connected = true;
this.emit('connected');
}.bind(this));
}
schema.connect = function(cb) {
var schema = this;
@ -139,9 +150,7 @@ function Schema(name, settings) {
}
}
};
};
util.inherits(Schema, EventEmitter);
}
/**
* Define class
@ -190,7 +199,7 @@ Schema.prototype.define = function defineClass(className, properties, settings)
if (!(this instanceof ModelConstructor)) {
return new ModelConstructor(data);
}
AbstractClass.call(this, data);
ModelBaseClass.call(this, data);
hiddenProperty(this, 'schema', schema || this.constructor.schema);
};
@ -198,12 +207,20 @@ Schema.prototype.define = function defineClass(className, properties, settings)
hiddenProperty(NewClass, 'modelName', className);
hiddenProperty(NewClass, 'relations', {});
// inherit AbstractClass methods
for (var i in AbstractClass) {
NewClass[i] = AbstractClass[i];
// inherit ModelBaseClass methods
for (var i in ModelBaseClass) {
NewClass[i] = ModelBaseClass[i];
}
for (var j in AbstractClass.prototype) {
NewClass.prototype[j] = AbstractClass.prototype[j];
for (var j in ModelBaseClass.prototype) {
NewClass.prototype[j] = ModelBaseClass.prototype[j];
}
// inherit DataAccessObject methods
for (var m in DataAccessObject) {
NewClass[m] = DataAccessObject[m];
}
for (var n in DataAccessObject.prototype) {
NewClass.prototype[n] = DataAccessObject.prototype[n];
}
NewClass.getter = {};
@ -218,12 +235,14 @@ Schema.prototype.define = function defineClass(className, properties, settings)
settings: settings
};
if(this.adapter) {
// pass control to adapter
this.adapter.define({
model: NewClass,
properties: properties,
settings: settings
});
}
NewClass.prototype.__defineGetter__('id', function () {
return this.__data.id;

View File

@ -6,7 +6,7 @@ exports.defineScope = defineScope;
/**
* Scope mixin for ./model.js
*/
var Model = require('./model.js');
var Model = require('./dao.js');
/**
* Define scope

View File

@ -18,7 +18,7 @@ exports.ValidationError = ValidationError;
* In more complicated cases it can be {Hash} of messages (for each case):
* `User.validatesLengthOf('password', { min: 6, max: 20, message: {min: 'too short', max: 'too long'}});`
*/
var Validatable = require('./model.js');
var Validatable = require('./dao.js');
/**
* Validate presence. This validation fails when validated field is blank.