From 630b991d1de50a199f426534a8f9887645c7b1d6 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 17 May 2013 08:49:57 -0700 Subject: [PATCH] Refactor the CRUD operations to DataAccessObject --- .gitignore | 2 +- examples/app.js | 38 +++ lib/dao.js | 566 ++++++++++++++++++++++++++++++++++++++++++++ lib/hooks.js | 7 +- lib/include.js | 4 +- lib/model.js | 569 ++------------------------------------------- lib/relations.js | 2 +- lib/schema.js | 123 +++++----- lib/scope.js | 2 +- lib/validations.js | 2 +- 10 files changed, 704 insertions(+), 611 deletions(-) create mode 100644 examples/app.js create mode 100644 lib/dao.js diff --git a/.gitignore b/.gitignore index bd50fea5..8dd1e071 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ doc coverage.html coverage v8.log - +.idea .DS_Store benchmark.js analyse.r diff --git a/examples/app.js b/examples/app.js new file mode 100644 index 00000000..c416773f --- /dev/null +++ b/examples/app.js @@ -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); + + + diff --git a/lib/dao.js b/lib/dao.js new file mode 100644 index 00000000..ea1e84fa --- /dev/null +++ b/lib/dao.js @@ -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 + }); +} diff --git a/lib/hooks.js b/lib/hooks.js index 80994f4c..1f7de668 100644 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -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; diff --git a/lib/include.js b/lib/include.js index f0bb05cd..26f2fe3a 100644 --- a/lib/include.js +++ b/lib/include.js @@ -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 ( diff --git a/lib/model.js b/lib/model.js index f1297a61..7e48fa64 100644 --- a/lib/model.js +++ b/lib/model.js @@ -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')); } + diff --git a/lib/relations.js b/lib/relations.js index 3b5e62ba..8daba2d7 100644 --- a/lib/relations.js +++ b/lib/relations.js @@ -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) { diff --git a/lib/schema.js b/lib/schema.js index b719dc57..3026a41f 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -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; diff --git a/lib/scope.js b/lib/scope.js index d2639ed3..273d26eb 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -6,7 +6,7 @@ exports.defineScope = defineScope; /** * Scope mixin for ./model.js */ -var Model = require('./model.js'); +var Model = require('./dao.js'); /** * Define scope diff --git a/lib/validations.js b/lib/validations.js index 380bc6bd..15b48c3a 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -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.