diff --git a/lib/mongoose.js b/lib/mongoose.js new file mode 100644 index 00000000..8224c42a --- /dev/null +++ b/lib/mongoose.js @@ -0,0 +1,155 @@ +/** + * Module dependencies + */ +var mongoose = require('mongoose'); + +exports.initialize = function initializeSchema(schema, callback) { + schema.client = mongoose.connect(schema.settings.url); + schema.adapter = new MongooseAdapter(schema.client); +}; + +function MongooseAdapter(client) { + this._models = {}; + this.client = client; + this.cache = {}; +} + +MongooseAdapter.prototype.define = function (descr) { + var props = {}; + Object.keys(descr.properties).forEach(function (key) { + props[key] = descr.properties[key].type; + if (props[key].name === 'Text') props[key] = String; + }); + var schema = new mongoose.Schema(props); + this._models[descr.model.modelName] = mongoose.model(descr.model.modelName, schema); + this.cache[descr.model.modelName] = {}; +}; + +MongooseAdapter.prototype.setCache = function (model, instance) { + this.cache[model][instance.id] = instance; +}; + +MongooseAdapter.prototype.getCached = function (model, id, cb) { + if (this.cache[model][id]) { + cb(null, this.cache[model][id]); + } else { + this._models[model].findById(id, function (err, instance) { + if (err) return cb(err); + this.cache[model][id] = instance; + cb(null, instance); + }.bind(this)); + } +}; + +MongooseAdapter.prototype.create = function (model, data, callback) { + var m = new this._models[model](data); + m.save(function (err) { + callback(err, err ? null : m.id); + }); +}; + +MongooseAdapter.prototype.save = function (model, data, callback) { + this.getCached(model, data.id, function (err, inst) { + if (err) return callback(err); + merge(inst, data); + inst.save(callback); + }); +}; + +MongooseAdapter.prototype.exists = function (model, id, callback) { + delete this.cache[model][id]; + this.getCached(model, id, function (err, data) { + if (err) return callback(err); + callback(err, !!data); + }); +}; + +MongooseAdapter.prototype.find = function find(model, id, callback) { + delete this.cache[model][id]; + this.getCached(model, id, function (err, data) { + if (err) return callback(err); + callback(err, data); + }); +}; + +MongooseAdapter.prototype.destroy = function destroy(model, id, callback) { + this.getCached(model, id, function (err, data) { + if (err) return callback(err); + if (data) data.remove(callback); + else callback(null); + }); +}; + +MongooseAdapter.prototype.all = function all(model, filter, callback) { + this._models[model].find(typeof filter === 'function' ? {} : filter, function (err, data) { + if (err) return callback(err); + callback(null, data); + }); +}; + +function applyFilter(filter) { + if (typeof filter === 'function') { + return filter; + } + var keys = Object.keys(filter); + return function (obj) { + var pass = true; + keys.forEach(function (key) { + if (!test(filter[key], obj[key])) { + pass = false; + } + }); + return pass; + } + + function test(example, value) { + if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { + return value.match(example); + } + // not strict equality + return example == value; + } +} + +MongooseAdapter.prototype.destroyAll = function destroyAll(model, callback) { + var wait = 0; + this._models[model].find(function (err, data) { + if (err) return callback(err); + wait = data.length; + data.forEach(function (obj) { + obj.remove(done) + }); + }); + + var error = null; + function done(err) { + error = error || err; + if (--wait === 0) { + callback(error); + } + } + +}; + +MongooseAdapter.prototype.count = function count(model, callback) { + this._models[model].count(callback); +}; + +MongooseAdapter.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { + this.getCached(model, id, function (err, inst) { + if (err) { + return cb(err); + } else if (inst) { + merge(inst, data); + inst.save(cb); + } else cb(); + }); +}; + +function merge(base, update) { + Object.keys(update).forEach(function (key) { + base[key] = update[key]; + }); + return base; +} + diff --git a/lib/redis-adapter.js b/lib/redis.js similarity index 100% rename from lib/redis-adapter.js rename to lib/redis.js diff --git a/test/common_test.js b/test/common_test.js new file mode 100644 index 00000000..2a6f22d2 --- /dev/null +++ b/test/common_test.js @@ -0,0 +1,280 @@ +var Schema = require('../index').Schema; +var Text = Schema.Text; + +require('./spec_helper').init(exports); + +var schemas = { + /* + riak: {}, + sequelize: { + database: 'sequ-test', + username: 'root' + } + */ + neo4j: { url: 'http://localhost:7474/' }, + mongoose: { url: 'mongodb://localhost/test' }, + redis: {}, + memory: {} +}; + +Object.keys(schemas).forEach(function (schemaName) { + if (process.env.ONLY && process.env.ONLY !== schemaName) return; + context(schemaName, function () { + var schema = new Schema(schemaName, schemas[schemaName]); + testOrm(schema); + }); +}); + +function testOrm(schema) { + + var Post; + var start = Date.now(); + + it('should define class', function (test) { + + var User = schema.define('User', { + name: String, + bio: Text, + approved: Boolean, + joinedAt: Date, + age: Number + }); + + Post = schema.define('Post', { + title: { type: String, length: 255 }, + content: { type: Text }, + date: { type: Date, detault: Date.now }, + published: { type: Boolean, default: false } + }); + + var user = new User; + + test.ok(User instanceof Function); + + // class methods + test.ok(User.find instanceof Function); + test.ok(User.create instanceof Function); + + // instance methods + test.ok(user.save instanceof Function); + + schema.automigrate(function (err) { + if (err) { + console.log('Error while migrating'); + console.log(err); + } else { + test.done(); + } + }); + + }); + + it('should initialize object properly', function (test) { + var hw = 'Hello word', post = new Post({title: hw}); + test.equal(post.title, hw); + test.ok(!post.propertyChanged('title')); + post.title = 'Goodbye, Lenin'; + test.equal(post.title_was, hw); + test.ok(post.propertyChanged('title')); + // test.ok(post.isNewRecord()); + test.done(); + }); + + it('should be expoted to JSON', function (test) { + test.equal(JSON.stringify(new Post({id: 1, title: 'hello, json'})), + '{"id":1,"title":"hello, json","content":null,"date":null,"published":null}'); + test.done(); + }); + + it('should create object', function (test) { + Post.create(function (err, post) { + if (err) throw err; + test.ok(post.id); + test.ok(!post.title, 'Title is blank'); + test.ok(!post.date, 'Date is blank'); + Post.exists(post.id, function (err, exists) { + if (err) throw err; + test.ok(exists); + test.done(); + }); + }); + }); + + it('should save object', function (test) { + var title = 'Initial title', title2 = 'Hello world', + date = new Date; + + Post.create({ + title: title, + date: date + }, function (err, obj) { + test.ok(obj.id); + test.equals(obj.title, title); + // test.equals(obj.date, date); + obj.title = title2; + test.ok(obj.propertyChanged('title'), 'Title changed'); + obj.save(function (err, obj) { + test.equal(obj.title, title2); + test.ok(!obj.propertyChanged('title')); + test.done(); + }); + }); + }); + + it('should create object with initial data', function (test) { + var title = 'Initial title', + date = new Date; + + Post.create({ + title: title, + date: date + }, function (err, obj) { + test.ok(obj.id); + test.equals(obj.title, title); + test.equals(obj.date, date); + Post.find(obj.id, function () { + test.equal(obj.title, title); + test.equal(obj.date, date.toString()); + test.done(); + }); + }); + }); + + it('should not create new instances for the same object', function (test) { + var title = 'Initial title'; + Post.create({ title: title }, function (err, post) { + test.ok(post.id, 'Object should have id'); + test.equals(post.title, title); + Post.find(post.id, function (err, foundPost) { + if (err) throw err; + test.equal(post.title, title); + test.strictEqual(post, foundPost); + test.done(); + }); + }); + }); + + it('should not re-instantiate object on saving', function (test) { + var title = 'Initial title'; + var post = new Post({title: title}); + post.save(function (err, savedPost) { + test.strictEqual(post, savedPost); + test.done(); + }); + }); + + it('should destroy object', function (test) { + Post.create(function (err, post) { + Post.exists(post.id, function (err, exists) { + test.ok(exists, 'Object exists'); + post.destroy(function () { + Post.exists(post.id, function (err, exists) { + test.ok(!exists, 'Hey! ORM told me that object exists, but it looks like it doesn\'t. Something went wrong...'); + Post.find(post.id, function (err, obj) { + test.equal(obj, null, 'Param obj should be null'); + test.done(); + }); + }); + }); + }); + }); + }); + + it('should update single attribute', function (test) { + Post.create({title: 'title', content: 'content', published: true}, function (err, post) { + post.content = 'New content'; + post.updateAttribute('title', 'New title', function () { + test.equal(post.title, 'New title'); + test.ok(!post.propertyChanged('title')); + test.equal(post.content, 'New content'); + test.ok(post.propertyChanged('content')); + post.reload(function () { + test.equal(post.title, 'New title'); + test.ok(!post.propertyChanged('title')); + test.equal(post.content, 'content'); + test.ok(!post.propertyChanged('content')); + test.done(); + }); + }); + }); + }); + + var countOfposts; + it('should fetch collection', function (test) { + Post.all(function (err, posts) { + countOfposts = posts.length; + test.ok(countOfposts > 0); + test.ok(posts[0] instanceof Post); + test.done(); + }); + }); + + it('should fetch count of records in collection', function (test) { + Post.count(function (err, count) { + test.equal(countOfposts, count); + test.done(); + }); + }); + + it('should find filtered set of records', function (test) { + var wait = 3; + + // exact match with string + Post.all({title: 'New title'}, function (err, res) { + var pass = true; + res.forEach(function (r) { + if (r.title != 'New title') pass = false; + }); + test.ok(res.length > 0); + test.ok(pass, 'Exact match with string'); + done(); + }); + + // matching null + Post.all({date: null, title: null}, function (err, res) { + var pass = true; + res.forEach(function (r) { + if (r.date != null || r.title != null) pass = false; + }); + test.ok(res.length > 0); + test.ok(pass, 'Matching null'); + done(); + }); + + // matching regexp + Post.all({title: /hello/i}, function (err, res) { + var pass = true; + res.forEach(function (r) { + if (!r.title || !r.title.match(/hello/i)) pass = false; + }); + test.ok(res.length > 0); + test.ok(pass, 'Matching regexp'); + done(); + }); + + function done() { + if (--wait === 0) { + test.done(); + } + } + + }); + + it('should destroy all records', function (test) { + Post.destroyAll(function (err) { + Post.all(function (err, posts) { + test.equal(posts.length, 0); + Post.count(function (err, count) { + test.equal(count, 0); + test.done(); + process.nextTick(allTestsDone); + }); + }); + }); + }); + + function allTestsDone() { + console.log('Test done in %dms\n', Date.now() - start); + } + +} diff --git a/test/datamapper_test.js b/test/datamapper_test.js new file mode 100644 index 00000000..11047d34 --- /dev/null +++ b/test/datamapper_test.js @@ -0,0 +1,317 @@ +require('./spec_helper').init(exports); + +[ 'redis~' +, 'mysql' +, 'mongodb~' +, 'postgres~' +].forEach(function (driver) { + // context(driver, testCasesFor(driver)); +}); + +function testCasesFor (driver) { + return function () { + + function Post () { this.initialize.apply(this, Array.prototype.slice.call(arguments)); } + function Comment () { this.initialize.apply(this, Array.prototype.slice.call(arguments)); } + + var properties = {}; + + properties['post'] = { + title: { type: String, validate: /.{10,255}/ }, + content: { type: String }, + published: { type: Boolean, default: false }, + date: { type: Date, default: function () {return new Date} } + }; + + properties['comment'] = { + content: { type: String, validate: /./ }, + date: { type: Date }, + author: { type: String }, + approved: { type: Boolean } + }; + + var associations = {}; + associations['post'] = { + comments: {className: 'Comment', relationType: 'n', tableName: 'comment'} + }; + + associations['comment'] = { + post: {className: 'Post', relationType: '<', tableName: 'post'} + }; + + try { + var orm = require('../lib/datamapper/' + driver); + if (driver == 'mysql') { + orm.configure({ + host: 'webdesk.homelinux.org', + port: 3306, + database: 'test', + user: 'guest', + password: '' + }); + } + } catch (e) { + console.log(e.message); + return; + } + orm.debugMode = true; + orm.mixPersistMethods(Post, { + className: 'Post', + tableName: 'post', + properties: properties['post'], + associations: associations['post'] + }); + orm.mixPersistMethods(Comment, { + className: 'Comment', + tableName: 'comment', + properties: properties['comment'], + associations: associations['comment'], + scopes: { + approved: { conditions: { approved: true } }, + author: { block: function (author) { return {conditions: {author: author}}; } } + } + }); + + var HOW_MANY_RECORDS = 1; + + it('cleanup database', function (test) { + var wait = 0; + var time = new Date; + var len; + Post.allInstances(function (posts) { + if (posts.length === 0) test.done(); + len = posts.length; + posts.forEach(function (post) { + wait += 1; + post.destroy(done); + }); + }); + + function done () { + if (--wait === 0) { + test.done(); + console.log('Cleanup %d records completed in %d ms', len, new Date - time); + } + } + }); + + it('create a lot of data', function (test) { + var wait = HOW_MANY_RECORDS; + var time = new Date; + for (var i = wait; i > 0; i -= 1) { + Post.create({title: Math.random().toString(), content: arguments.callee.caller.toString(), date: new Date, published: false}, done); + } + + function done () { + if (--wait === 0) { + test.done(); + console.log('Creating %d records completed in %d ms', HOW_MANY_RECORDS, new Date - time); + } + } + }); + + it('should retrieve all data fast', function (test) { + var time = new Date; + Post.allInstances(function (posts) { + test.equal(posts.length, HOW_MANY_RECORDS); + console.log('Retrieving %d records completed in %d ms', HOW_MANY_RECORDS, new Date - time); + test.done(); + }); + }); + + it('should initialize object properly', function (test) { + var hw = 'Hello world', post = new Post({title: hw}); + test.equal(post.title, hw); + test.ok(!post.propertyChanged('title')); + post.title = 'Goodbye, Lenin'; + test.equal(post.title_was, hw); + test.ok(post.propertyChanged('title')); + test.ok(post.isNewRecord()); + test.done(); + }); + + it('should create object', function (test) { + Post.create(function () { + test.ok(this.id); + Post.exists(this.id, function (exists) { + test.ok(exists); + test.done(); + }); + }); + }); + + it('should save object', function (test) { + var title = 'Initial title', title2 = 'Hello world', + date = new Date; + + Post.create({ + title: title, + date: date + }, function () { + test.ok(this.id); + test.equals(this.title, title); + test.equals(this.date, date); + this.title = title2; + this.save(function () { + test.equal(this.title, title2); + test.ok(!this.propertyChanged('title')); + test.done(); + }); + }); + }); + + it('should create object with initial data', function (test) { + var title = 'Initial title', + date = new Date; + + Post.create({ + title: title, + date: date + }, function () { + test.ok(this.id); + test.equals(this.title, title); + test.equals(this.date, date); + Post.find(this.id, function () { + test.equal(this.title, title); + test.equal(this.date, date.toString()); + test.done(); + }); + }); + }); + + it('should not create new instances for the same object', function (test) { + var title = 'Initial title'; + Post.create({ title: title }, function () { + var post = this; + test.ok(this.id, 'Object should have id'); + test.equals(this.title, title); + Post.find(this.id, function () { + test.equal(this.title, title); + test.strictEqual(this, post); + test.done(); + }); + }); + }); + + it('should destroy object', function (test) { + Post.create(function () { + var post = this; + Post.exists(post.id, function (exists) { + test.ok(exists, 'Object exists'); + post.destroy(function () { + Post.exists(post.id, function (exists) { + test.ok(!exists, 'Object not exists'); + Post.find(post.id, function (err, obj) { + test.ok(err, 'Object not found'); + test.equal(obj, null, 'Param obj should be null'); + test.done(); + }); + }); + }); + }); + }); + }); + + it('should update single attribute', function (test) { + Post.create({title: 'title', content: 'content'}, function () { + this.content = 'New content'; + this.updateAttribute('title', 'New title', function () { + test.equal(this.title, 'New title'); + test.ok(!this.propertyChanged('title')); + test.equal(this.content, 'New content'); + test.ok(this.propertyChanged('content')); + this.reload(function () { + test.equal(this.title, 'New title'); + test.ok(!this.propertyChanged('title')); + test.equal(this.content, 'content'); + test.ok(!this.propertyChanged('content')); + test.done(); + }); + }); + }); + }); + + // NOTE: this test rely on previous + it('should fetch collection', function (test) { + Post.allInstances(function (posts) { + test.ok(posts.length > 0); + test.strictEqual(posts[0].constructor, Post); + test.done(); + }); + }); + + // NOTE: this test rely on previous + it('should fetch first, second, third and last elements of class', function (test) { + test.done(); return; + var queries = 4; + test.expect(queries); + function done () { if (--queries == 0) test.done(); } + + Post.first(function (post) { + test.strictEqual(post.constructor, Post); + done(); + }); + Post.second(function (post) { + test.strictEqual(post.constructor, Post); + done(); + }); + Post.third(function (post) { + test.strictEqual(post.constructor, Post); + done(); + }); + Post.last(function (post) { + test.strictEqual(post.constructor, Post); + done(); + }); + }); + + it('should load associated collection', function (test) { + test.done(); return; + Post.last(function (post) { + post.comments.approved.where('author = ?', 'me').load(); + }); + }); + + it('should find record and associated association', function (test) { + test.done(); return; + Post.last(function (post) { + Post.find(post.id, {include: 'comments'}, function () { + }); + }); + }); + + it('should fetch associated collection', function (test) { + test.done(); return; + Post.create(function () { + // load collection + this.comments(function () { + }); + // creating associated object + this.comments.create(function () { + }); + this.comments.build().save(); + // named scopes + this.comments.pending(function () { + }); + this.comments.approved(function () { + }); + }); + }); + + it('should validate object', function (test) { + test.done(); return; + var post = new Post; + test.ok(!post.isValid()); + post.save(function (id) { + test.ok(!id, 'Post should not be saved'); + }); + post.title = 'Title'; + test.ok(post.isValid()); + post.save(function (id) { + test.ok(id); + test.done(); + }); + }); + + } +}; diff --git a/test/spec_helper.js b/test/spec_helper.js new file mode 100644 index 00000000..b5ca20c1 --- /dev/null +++ b/test/spec_helper.js @@ -0,0 +1,70 @@ +try { + global.sinon = require('sinon'); +} catch (e) { + // ignore +} + +var group_name = false, EXT_EXP; +function it (should, test_case) { + check_external_exports(); + if (group_name) { + EXT_EXP[group_name][should] = test_case; + } else { + EXT_EXP[should] = test_case; + } +} + +global.it = it; + +function context(name, tests) { + check_external_exports(); + EXT_EXP[name] = {}; + group_name = name; + tests({ + before: function (f) { + it('setUp', f); + }, + after: function (f) { + it('tearDown', f); + } + }); + group_name = false; +} + +global.context = context; + +exports.init = function (external_exports) { + EXT_EXP = external_exports; + if (external_exports.done) { + external_exports.done(); + } +}; + +function check_external_exports() { + if (!EXT_EXP) throw new Error( + 'Before run this, please ensure that ' + + 'require("spec_helper").init(exports); called'); +} + +// add assertions + +var assert = require(require('module')._resolveFilename('nodeunit')[0].replace(/index\.js$/, 'lib/assert')); + +// Check response status code 200 OK +assert.status200 = function (response, message) { + if (response.statusCode !== 200) { + assert.fail(response.statusCode, 200, message || 'Status code is not 200', '===', assert.status200); + } +} + +// Check redirection +assert.redirect = function (response, path, message) { + if (response.statusCode !== 302) { + assert.fail(response.statusCode, 302, 'Status code is not 302', '===', assert.redirect); + } + var realPath = require('url').parse(response.headers.location).pathname; + if (realPath !== path) { + assert.fail(realPath, path, message || 'Wrong location', '===', assert.redirect); + } +} +