// This test written in mocha+should.js var should = require('./init.js'); var assert = require('assert'); var async = require('async'); var jdb = require('../'); var ModelBuilder = jdb.ModelBuilder; var DataSource = jdb.DataSource; describe('ModelBuilder define model', function () { it('should be able to define plain models', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, age: Number }); // define any custom method User.prototype.getNameAndAge = function () { return this.name + ', ' + this.age; }; modelBuilder.models.should.be.a('object').and.have.property('User', User); modelBuilder.definitions.should.be.a('object').and.have.property('User'); var user = new User({name: 'Joe', age: 20, xyz: false}); User.modelName.should.equal('User'); user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.have.property('xyz', false); user.should.not.have.property('bio'); done(null, User); }); it('should not take unknown properties in strict mode', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {name: String, bio: String}, {strict: true}); var user = new User({name: 'Joe', age: 20}); User.modelName.should.equal('User'); user.should.be.a('object'); user.should.have.property('name', 'Joe'); user.should.not.have.property('age'); user.toObject().should.not.have.property('age'); user.toObject(true).should.not.have.property('age'); user.should.not.have.property('bio'); done(null, User); }); it('should ignore non-predefined properties in strict mode', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {name: String, bio: String}, {strict: true}); var user = new User({name: 'Joe'}); user.age = 10; user.bio = 'me'; user.should.have.property('name', 'Joe'); user.should.have.property('bio', 'me'); // Non predefined property age should be ignored in strict mode if schemaOnly parameter is not false user.toObject().should.not.have.property('age'); user.toObject(true).should.not.have.property('age'); user.toObject(false).should.have.property('age', 10); // Predefined property bio should be kept in strict mode user.toObject().should.have.property('bio', 'me'); user.toObject(true).should.have.property('bio', 'me'); user.toObject(false).should.have.property('bio', 'me'); done(null, User); }); it('should throw when unknown properties are used if strict=throw', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {name: String, bio: String}, {strict: 'throw'}); try { var user = new User({name: 'Joe', age: 20}); assert(false, 'The code should have thrown an error'); } catch (e) { assert(true, 'The code is expected to throw an error'); } done(null, User); }); it('should be able to define open models', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {}, {strict: false}); var user = new User({name: 'Joe', age: 20}); User.modelName.should.equal('User'); user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); done(null, User); }); it('should take non-predefined properties in non-strict mode', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {name: String, bio: String}, {strict: false}); var user = new User({name: 'Joe'}); user.age = 10; user.bio = 'me'; user.should.have.property('name', 'Joe'); user.should.have.property('bio', 'me'); // Non predefined property age should be kept in non-strict mode user.toObject().should.have.property('age', 10); user.toObject(true).should.have.property('age', 10); user.toObject(false).should.have.property('age', 10); // Predefined property bio should be kept user.toObject().should.have.property('bio', 'me'); user.toObject(true).should.have.property('bio', 'me'); user.toObject(false).should.have.property('bio', 'me'); done(null, User); }); it('should use false as the default value for strict', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {}); var user = new User({name: 'Joe', age: 20}); User.modelName.should.equal('User'); user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); done(null, User); }); it('should be able to define nesting models', function (done) { var modelBuilder = new ModelBuilder(); // simplier way to describe model var User = modelBuilder.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, age: Number, address: { street: String, city: String, state: String, zipCode: String, country: String }, emails: [ { label: String, email: String } ], friends: [String] }); // define any custom method User.prototype.getNameAndAge = function () { return this.name + ', ' + this.age; }; modelBuilder.models.should.be.a('object').and.have.property('User', User); modelBuilder.definitions.should.be.a('object').and.have.property('User'); var user = new User({ name: 'Joe', age: 20, address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}, emails: [ {label: 'work', email: 'xyz@sample.com'} ], friends: ['Mary', 'John'] }); User.modelName.should.equal('User'); user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); user.should.have.property('address'); user.address.should.have.property('city', 'San Jose'); user.address.should.have.property('state', 'CA'); user = user.toObject(); user.emails.should.have.property('length', 1); user.emails[0].should.have.property('label', 'work'); user.emails[0].should.have.property('email', 'xyz@sample.com'); user.friends.should.have.property('length', 2); assert.equal(user.friends[0], 'Mary'); assert.equal(user.friends[1], 'John'); done(null, User); }); it('should be able to reference models by name before they are defined', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', {name: String, address: 'Address'}); var user; try { user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}}); assert(false, 'An exception should have been thrown'); } catch (e) { // Ignore } var Address = modelBuilder.define('Address', { street: String, city: String, state: String, zipCode: String, country: String }); user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}}); User.modelName.should.equal('User'); User.definition.properties.address.should.have.property('type', Address); user.should.be.a('object'); assert(user.name === 'Joe'); user.address.should.have.property('city', 'San Jose'); user.address.should.have.property('state', 'CA'); done(null, User); }); }); describe('DataSource ping', function() { var ds = new DataSource('memory'); ds.settings.connectionTimeout = 50; // ms ds.connector.connect = function(cb) { // Mock up the long delay setTimeout(cb, 100); }; ds.connector.ping = function(cb) { cb(new Error('bad connection 2')); } it('should report connection errors during ping', function(done) { ds.ping(function(err) { (!!err).should.be.true; err.message.should.be.eql('bad connection 2'); done(); }); }); it('should cancel invocation after timeout', function(done) { ds.connected = false; // Force connect var Post = ds.define('Post', { title: { type: String, length: 255 } }); Post.create(function(err) { (!!err).should.be.true; err.message.should.be.eql('Timeout in connecting after 50 ms'); done(); }); }); }); describe('DataSource define model', function () { it('should be able to define plain models', function () { var ds = new DataSource('memory'); // define models var Post = ds.define('Post', { title: { type: String, length: 255 }, content: { type: ModelBuilder.Text }, date: { type: Date, default: function () { return new Date(); } }, timestamp: { type: Number, default: Date.now }, published: { type: Boolean, default: false, index: true } }); // simpler way to describe model var User = ds.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: {type: Date, default: Date}, age: Number }); var Group = ds.define('Group', {group: String}); User.mixin(Group); // define any custom method User.prototype.getNameAndAge = function () { return this.name + ', ' + this.age; }; var user = new User({name: 'Joe', group: 'G1'}); assert.equal(user.name, 'Joe'); assert.equal(user.group, 'G1'); assert(user.joinedAt instanceof Date); // setup relationships User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}); Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); User.hasAndBelongsToMany('groups'); var user2 = new User({name: 'Smith'}); user2.save(function (err) { var post = user2.posts.build({title: 'Hello world'}); post.save(function (err, data) { // console.log(err ? err : data); }); }); Post.findOne({where: {published: false}, order: 'date DESC'}, function (err, data) { // console.log(data); }); User.create({name: 'Jeff'}, function (err, data) { if (err) { console.log(err); return; } var post = data.posts.build({title: 'My Post'}); }); User.create({name: 'Ray'}, function (err, data) { // console.log(data); }); var Article = ds.define('Article', {title: String}); var Tag = ds.define('Tag', {name: String}); Article.hasAndBelongsToMany('tags'); Article.create(function (e, article) { article.tags.create({name: 'popular'}, function (err, data) { Article.findOne(function (e, article) { article.tags(function (e, tags) { // console.log(tags); }); }); }); }); // should be able to attach a data source to an existing model var modelBuilder = new ModelBuilder(); var Color = modelBuilder.define('Color', { name: String }); Color.should.not.have.property('create'); // attach ds.attach(Color); Color.should.have.property('create'); Color.create({name: 'red'}); Color.create({name: 'green'}); Color.create({name: 'blue'}); Color.all(function (err, colors) { colors.should.have.lengthOf(3); }); }); it('should emit events during attach', function() { var ds = new DataSource('memory'); var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String }); var seq = 0; var dataAccessConfigured = -1; var dataSourceAttached = -1; User.on('dataAccessConfigured', function (model) { dataAccessConfigured = seq++; assert(User.create); assert(User.hasMany); }); User.on('dataSourceAttached', function (model) { assert(User.dataSource instanceof DataSource); dataSourceAttached = seq++; }); ds.attach(User); assert.equal(dataAccessConfigured, 0); assert.equal(dataSourceAttached, 1); }); it('should not take unknown properties in strict mode', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, bio: String}, {strict: true}); User.create({name: 'Joe', age: 20}, function (err, user) { User.modelName.should.equal('User'); user.should.be.a('object'); assert(user.name === 'Joe'); assert(user.age === undefined); assert(user.toObject().age === undefined); assert(user.toObject(true).age === undefined); assert(user.bio === undefined); done(null, User); }); }); it('should throw when unknown properties are used if strict=throw', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, bio: String}, {strict: 'throw'}); try { var user = new User({name: 'Joe', age: 20}); assert(false, 'The code should have thrown an error'); } catch (e) { assert(true, 'The code is expected to throw an error'); } done(null, User); }); it('should be able to define open models', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {}, {strict: false}); User.modelName.should.equal('User'); User.create({name: 'Joe', age: 20}, function (err, user) { user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); User.findById(user.id, function (err, user) { user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); done(null, User); }); }); }); it('should use false as the default value for strict', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {}); User.create({name: 'Joe', age: 20}, function (err, user) { User.modelName.should.equal('User'); user.should.be.a('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.not.have.property('bio'); done(null, User); }); }); it('should use true as the default value for strict for relational DBs', function (done) { var ds = new DataSource('memory'); ds.connector.relational = true; // HACK var User = ds.define('User', {name: String, bio: String}, {strict: true}); var user = new User({name: 'Joe', age: 20}); User.modelName.should.equal('User'); user.should.be.a('object'); assert(user.name === 'Joe'); assert(user.age === undefined); assert(user.toObject().age === undefined); assert(user.toObject(true).age === undefined); assert(user.bio === undefined); done(null, User); }); it('should throw when unknown properties are used if strict=false for relational DBs', function (done) { var ds = new DataSource('memory'); ds.connector.relational = true; // HACK var User = ds.define('User', {name: String, bio: String}, {strict: 'throw'}); try { var user = new User({name: 'Joe', age: 20}); assert(false, 'The code should have thrown an error'); } catch (e) { assert(true, 'The code is expected to throw an error'); } done(null, User); }); it('should change the property value for save if strict=false', function (done) { var ds = new DataSource('memory');// define models var Post = ds.define('Post'); Post.create({price: 900}, function (err, post) { assert.equal(post.price, 900); post.price = 1000; post.save(function (err, result) { assert.equal(1000, result.price); done(err, result); }); }); }); it('supports instance level strict mode', function () { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, bio: String}, {strict: true}); var user = new User({name: 'Joe', age: 20}, {strict: false}); user.should.have.property('__strict', false); user.should.be.a('object'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.toObject().should.have.property('age', 20); user.toObject(true).should.have.property('age', 20); user.setStrict(true); user.toObject().should.not.have.property('age'); user.toObject(true).should.not.have.property('age'); user.toObject(false).should.have.property('age', 20); }); it('should update the instance with unknown properties', function (done) { var ds = new DataSource('memory');// define models Post = ds.define('Post', { title: { type: String, length: 255, index: true }, content: { type: String } }); Post.create({title: 'a', content: 'AAA'}, function (err, post) { post.updateAttributes({title: 'b', xyz: 'xyz'}, function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); p.xyz.should.be.equal('xyz'); Post.findById(post.id, function (err, p) { p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); p.xyz.should.be.equal('xyz'); p.title.should.be.equal('b'); done(); }); }); }); }); it('injects id by default', function (done) { var ds = new ModelBuilder(); var User = ds.define('User', {}); assert.deepEqual(User.definition.properties.id, {type: Number, id: 1, generated: true}); done(); }); it('disables idInjection if the value is false', function (done) { var ds = new ModelBuilder(); var User1 = ds.define('User', {}, {idInjection: false}); assert(!User1.definition.properties.id); done(); }); it('updates generated id type by the connector', function (done) { var builder = new ModelBuilder(); var User = builder.define('User', {id: {type: String, generated: true, id: true}}); assert.deepEqual(User.definition.properties.id, {type: String, id: 1, generated: true}); var ds = new DataSource('memory');// define models User.attachTo(ds); assert.deepEqual(User.definition.properties.id, {type: Number, id: 1, generated: true}); done(); }); it('should allow an explicit remoting path', function () { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, bio: String}, { http: { path: 'accounts' } }); User.http.path.should.equal('/accounts'); }); }); describe('Load models with base', function () { it('should set up base class via base option', function () { var ds = new ModelBuilder(); var User = ds.define('User', {name: String}); User.staticMethod = function staticMethod() { }; User.prototype.instanceMethod = function instanceMethod() { }; var Customer = ds.define('Customer', {vip: Boolean}, {base: 'User'}); assert(Customer.prototype instanceof User); assert(Customer.staticMethod === User.staticMethod); assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod); assert.equal(Customer.base, User); assert.equal(Customer.base, Customer.super_); try { var Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'}); } catch (e) { assert(e); } }); it('should set up base class via parent arg', function () { var ds = new ModelBuilder(); var User = ds.define('User', {name: String}); User.staticMethod = function staticMethod() { }; User.prototype.instanceMethod = function instanceMethod() { }; var Customer = ds.define('Customer', {vip: Boolean}, {}, User); assert(Customer.prototype instanceof User); assert(Customer.staticMethod === User.staticMethod); assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod); assert.equal(Customer.base, User); assert.equal(Customer.base, Customer.super_); }); }); describe('Models attached to a dataSource', function() { var Post; before(function() { var ds = new DataSource('memory');// define models Post = ds.define('Post', { title: { type: String, length: 255, index: true }, content: { type: String }, comments: [String] }); }); beforeEach(function(done) { Post.destroyAll(done); }); it('updateOrCreate should update the instance', function (done) { Post.create({title: 'a', content: 'AAA'}, function (err, post) { post.title = 'b'; Post.updateOrCreate(post, function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); should.not.exist(p._id); Post.findById(post.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('b'); done(); }); }); }); }); it('updateOrCreate should update the instance without removing existing properties', function (done) { Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, function (err, post) { post = post.toObject(); delete post.title; delete post.comments; Post.updateOrCreate(post, function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); should.not.exist(p._id); Post.findById(post.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('a'); p.comments.length.should.be.equal(1); p.comments[0].should.be.equal('Comment1'); done(); }); }); }); }); it('updateOrCreate should create a new instance if it does not exist', function (done) { var post = {id: 123, title: 'a', content: 'AAA'}; Post.updateOrCreate(post, function (err, p) { should.not.exist(err); p.title.should.be.equal(post.title); p.content.should.be.equal(post.content); p.id.should.be.equal(post.id); Post.findById(p.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal(post.title); p.id.should.be.equal(post.id); done(); }); }); }); it('save should update the instance with the same id', function (done) { Post.create({title: 'a', content: 'AAA'}, function (err, post) { post.title = 'b'; post.save(function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); should.not.exist(p._id); Post.findById(post.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('b'); done(); }); }); }); }); it('save should update the instance without removing existing properties', function (done) { Post.create({title: 'a', content: 'AAA'}, function (err, post) { delete post.title; post.save(function (err, p) { should.not.exist(err); p.id.should.be.equal(post.id); p.content.should.be.equal(post.content); should.not.exist(p._id); Post.findById(post.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal('a'); done(); }); }); }); }); it('save should create a new instance if it does not exist', function (done) { var post = new Post({id: '123', title: 'a', content: 'AAA'}); post.save(post, function (err, p) { should.not.exist(err); p.title.should.be.equal(post.title); p.content.should.be.equal(post.content); p.id.should.be.equal(post.id); Post.findById(p.id, function (err, p) { p.id.should.be.equal(post.id); should.not.exist(p._id); p.content.should.be.equal(post.content); p.title.should.be.equal(post.title); p.id.should.be.equal(post.id); done(); }); }); }); }); describe('DataSource connector types', function() { it('should return an array of types', function() { var ds = new DataSource('memory'); var types = ds.getTypes(); assert.deepEqual(types, ['db', 'nosql', 'memory']); }); it('should test supported types by string', function() { var ds = new DataSource('memory'); var result = ds.supportTypes('db'); assert(result); }); it('should test supported types by array', function() { var ds = new DataSource('memory'); var result = ds.supportTypes(['db', 'memory']); assert(result); }); it('should test unsupported types by string', function() { var ds = new DataSource('memory'); var result = ds.supportTypes('rdbms'); assert(!result); }); it('should test unsupported types by array', function() { var ds = new DataSource('memory'); var result = ds.supportTypes(['rdbms', 'memory']); assert(!result); result = ds.supportTypes(['rdbms']); assert(!result); }); }); describe('DataSource constructor', function () { // Mocked require var loader = function (name) { if (name.indexOf('./connectors/') !== -1) { // ./connectors/ doesn't exist return null; } if (name === 'loopback-connector-abc') { // Assume loopback-connector-abc doesn't exist return null; } return { name: name }; }; it('should resolve connector by path', function () { var connector = DataSource._resolveConnector(__dirname + '/../lib/connectors/memory'); assert(connector.connector); }); it('should resolve connector by internal name', function () { var connector = DataSource._resolveConnector('memory'); assert(connector.connector); }); it('should try to resolve connector by module name starts with loopback-connector-', function () { var connector = DataSource._resolveConnector('loopback-connector-xyz', loader); assert(connector.connector); }); it('should try to resolve connector by short module name with full name first', function () { var connector = DataSource._resolveConnector('xyz', loader); assert(connector.connector); assert.equal(connector.connector.name, 'loopback-connector-xyz'); }); it('should try to resolve connector by short module name', function () { var connector = DataSource._resolveConnector('abc', loader); assert(connector.connector); assert.equal(connector.connector.name, 'abc'); }); it('should try to resolve connector by short module name for known connectors', function () { var connector = DataSource._resolveConnector('oracle', loader); assert(connector.connector); assert.equal(connector.connector.name, 'loopback-connector-oracle'); }); it('should try to resolve connector by full module name', function () { var connector = DataSource._resolveConnector('loopback-xyz', loader); assert(connector.connector); }); it('should fail to resolve connector by module name starts with loopback-connector-', function () { var connector = DataSource._resolveConnector('loopback-connector-xyz'); assert(!connector.connector); assert(connector.error.indexOf('loopback-connector-xyz') !== -1); }); it('should fail to resolve connector by short module name', function () { var connector = DataSource._resolveConnector('xyz'); assert(!connector.connector); assert(connector.error.indexOf('loopback-connector-xyz') !== -1); }); it('should fail to resolve connector by full module name', function () { var connector = DataSource._resolveConnector('loopback-xyz'); assert(!connector.connector); assert(connector.error.indexOf('loopback-connector-loopback-xyz') !== -1); }); }); describe('Load models with relations', function () { it('should set up relations', function (done) { var ds = new DataSource('memory'); var Post = ds.define('Post', {userId: Number, content: String}); var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}}}); assert(User.relations['posts']); done(); }); it('should set up belongsTo relations', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String}); var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}}); assert(Post.relations['user']); done(); }); it('should set up referencesMany relations', function (done) { var ds = new DataSource('memory'); var Post = ds.define('Post', {userId: Number, content: String}); var User = ds.define('User', {name: String}, {relations: {posts: {type: 'referencesMany', model: 'Post'}}}); assert(User.relations['posts']); done(); }); it('should set up embedsMany relations', function (done) { var ds = new DataSource('memory'); var Post = ds.define('Post', {userId: Number, content: String}); var User = ds.define('User', {name: String}, {relations: {posts: {type: 'embedsMany', model: 'Post' }}}); assert(User.relations['posts']); done(); }); it('should set up polymorphic relations', function (done) { var ds = new DataSource('memory'); var Author = ds.define('Author', {name: String}, {relations: { pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'} }}); var Picture = ds.define('Picture', {name: String}, {relations: { imageable: {type: 'belongsTo', polymorphic: true} }}); assert(Author.relations['pictures']); assert.deepEqual(Author.relations['pictures'].toJSON(), { name: 'pictures', type: 'hasMany', modelFrom: 'Author', keyFrom: 'id', modelTo: 'Picture', keyTo: 'imageableId', multiple: true, polymorphic: { as: 'imageable', foreignKey: 'imageableId', discriminator: 'imageableType' } }); assert(Picture.relations['imageable']); assert.deepEqual(Picture.relations['imageable'].toJSON(), { name: 'imageable', type: 'belongsTo', modelFrom: 'Picture', keyFrom: 'imageableId', modelTo: '', keyTo: 'id', multiple: false, polymorphic: { as: 'imageable', foreignKey: 'imageableId', discriminator: 'imageableType' } }); done(); }); it('should set up foreign key with the correct type', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, id: {type: String, id: true}}); var Post = ds.define('Post', {content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}}); var fk = Post.definition.properties['userId']; assert(fk, 'The foreign key should be added'); assert(fk.type === String, 'The foreign key should be the same type as primary key'); assert(Post.relations['user'], 'User relation should be set'); done(); }); it('should set up hasMany and belongsTo relations', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}, accounts: {type: 'hasMany', model: 'Account'}}}); assert(!User.relations['posts']); assert(!User.relations['accounts']); var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}}); var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}}); assert(Post.relations['user']); assert.deepEqual(Post.relations['user'].toJSON(), { name: 'user', type: 'belongsTo', modelFrom: 'Post', keyFrom: 'userId', modelTo: 'User', keyTo: 'id', multiple: false }); assert(User.relations['posts']); assert.deepEqual(User.relations['posts'].toJSON(), { name: 'posts', type: 'hasMany', modelFrom: 'User', keyFrom: 'id', modelTo: 'Post', keyTo: 'userId', multiple: true }); assert(User.relations['accounts']); assert.deepEqual(User.relations['accounts'].toJSON(), { name: 'accounts', type: 'hasMany', modelFrom: 'User', keyFrom: 'id', modelTo: 'Account', keyTo: 'userId', multiple: true }); done(); }); it('should throw if a relation is missing type', function (done) { var ds = new DataSource('memory'); var Post = ds.define('Post', {userId: Number, content: String}); try { var User = ds.define('User', {name: String}, {relations: {posts: {model: 'Post'}}}); } catch (e) { done(); } }); it('should throw if the relation type is invalid', function (done) { var ds = new DataSource('memory'); var Post = ds.define('Post', {userId: Number, content: String}); try { var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasXYZ', model: 'Post'}}}); } catch (e) { done(); } }); it('should handle hasMany through', function (done) { var ds = new DataSource('memory'); var Physician = ds.createModel('Physician', { name: String }, {relations: {patients: {model: 'Patient', type: 'hasMany', through: 'Appointment'}}}); var Patient = ds.createModel('Patient', { name: String }, {relations: {physicians: {model: 'Physician', type: 'hasMany', through: 'Appointment'}}}); assert(!Physician.relations['patients']); // Appointment hasn't been resolved yet assert(!Patient.relations['physicians']); // Appointment hasn't been resolved yet var Appointment = ds.createModel('Appointment', { physicianId: Number, patientId: Number, appointmentDate: Date }, {relations: {patient: {type: 'belongsTo', model: 'Patient'}, physician: {type: 'belongsTo', model: 'Physician'}}}); assert(Physician.relations['patients']); assert(Patient.relations['physicians']); done(); }); it('should set up relations after attach', function (done) { var ds = new DataSource('memory'); var modelBuilder = new ModelBuilder(); var Post = modelBuilder.define('Post', {userId: Number, content: String}); var User = modelBuilder.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}}}); assert(!User.relations['posts']); Post.attachTo(ds); User.attachTo(ds); assert(User.relations['posts']); done(); }); }); describe('Model with scopes', function () { it('should create scopes', function (done) { var ds = new DataSource('memory'); var User = ds.define('User', {name: String, vip: Boolean, age: Number}, {scopes: {vips: {where: {vip: true}}, top5: {limit: 5, order: 'age'}}}); var users = []; for (var i = 0; i < 10; i++) { users.push({name: 'User' + i, vip: i % 3 === 0, age: 20 + i * 2}); } async.each(users, function (user, callback) { User.create(user, callback); }, function (err) { User.vips(function (err, vips) { if(err) { return done(err); } assert.equal(vips.length, 4); User.top5(function (err, top5) { assert.equal(top5.length, 5); done(err); }); }); }); }); }); describe('DataAccessObject', function () { var ds, model, where, error; before(function () { ds = new DataSource('memory'); model = ds.createModel('M1', { id: {type: String, id: true}, age: Number, vip: Boolean, date: Date, location: 'GeoPoint', scores: [Number] }); }); beforeEach(function () { error = null; }); it('should be able to coerce where clause for string types', function () { where = model._coerce({id: 1}); assert.deepEqual(where, {id: '1'}); where = model._coerce({id: '1'}); assert.deepEqual(where, {id: '1'}); }); it('should be able to coerce where clause for number types', function () { where = model._coerce({age: '10'}); assert.deepEqual(where, {age: 10}); where = model._coerce({age: 10}); assert.deepEqual(where, {age: 10}); where = model._coerce({age: {gt: 10}}); assert.deepEqual(where, {age: {gt: 10}}); where = model._coerce({age: {gt: '10'}}); assert.deepEqual(where, {age: {gt: 10}}); where = model._coerce({age: {between: ['10', '20']}}); assert.deepEqual(where, {age: {between: [10, 20]}}); }); it('should be able to coerce where clause for array types', function () { where = model._coerce({scores: ['10', '20']}); assert.deepEqual(where, {scores: [10, 20]}); }); it('should be able to coerce where clause for date types', function () { var d = new Date(); where = model._coerce({date: d}); assert.deepEqual(where, {date: d}); where = model._coerce({date: d.toISOString()}); assert.deepEqual(where, {date: d}); }); it('should be able to coerce where clause for boolean types', function () { where = model._coerce({vip: 'true'}); assert.deepEqual(where, {vip: true}); where = model._coerce({vip: true}); assert.deepEqual(where, {vip: true}); where = model._coerce({vip: 'false'}); assert.deepEqual(where, {vip: false}); where = model._coerce({vip: false}); assert.deepEqual(where, {vip: false}); where = model._coerce({vip: '1'}); assert.deepEqual(where, {vip: true}); where = model._coerce({vip: 0}); assert.deepEqual(where, {vip: false}); where = model._coerce({vip: ''}); assert.deepEqual(where, {vip: false}); }); it('should be able to coerce where clause with and operators', function () { where = model._coerce({and: [{age: '10'}, {vip: 'true'}]}); assert.deepEqual(where, {and: [{age: 10}, {vip: true}]}); }); it('should be able to coerce where clause with or operators', function () { where = model._coerce({or: [{age: '10'}, {vip: 'true'}]}); assert.deepEqual(where, {or: [{age: 10}, {vip: true}]}); }); it('should throw if the where property is not an object', function () { try { // The where clause has to be an object model._coerce('abc'); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if the where property is an array', function () { try { // The where clause cannot be an array model._coerce([ {vip: true} ]); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if the and operator does not take an array', function () { try { // The and operator only takes an array of objects model._coerce({and: {x: 1}}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if the or operator does not take an array', function () { try { // The or operator only takes an array of objects model._coerce({or: {x: 1}}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if the or operator does not take an array of objects', function () { try { // The or operator only takes an array of objects model._coerce({or: ['x']}); } catch(err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter property is not an object', function () { var filter = null; try { // The filter clause has to be an object filter = model._normalize('abc'); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter.limit property is not a number', function () { try { // The limit param must be a valid number filter = model._normalize({limit: 'x'}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter.limit property is nagative', function () { try { // The limit param must be a valid number filter = model._normalize({limit: -1}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter.limit property is not an integer', function () { try { // The limit param must be a valid number filter = model._normalize({limit: 5.8}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter.offset property is not a number', function () { try { // The limit param must be a valid number filter = model._normalize({offset: 'x'}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should throw if filter.skip property is not a number', function () { try { // The limit param must be a valid number filter = model._normalize({skip: '_'}); } catch (err) { error = err; } assert(error, 'An error should have been thrown'); }); it('should normalize limit/offset/skip', function () { filter = model._normalize({limit: '10', skip: 5}); assert.deepEqual(filter, {limit: 10, offset: 5, skip: 5}); }); it('should set the default value for limit', function () { filter = model._normalize({skip: 5}); assert.deepEqual(filter, {limit: 100, offset: 5, skip: 5}); }); it('should skip GeoPoint', function () { where = model._coerce({location: {near: {lng: 10, lat: 20}, maxDistance: 20}}); assert.deepEqual(where, {location: {near: {lng: 10, lat: 20}, maxDistance: 20}}); }); it('should skip null values', function () { where = model._coerce({date: null}); assert.deepEqual(where, {date: null}); }); it('should skip undefined values', function () { where = model._coerce({date: undefined}); assert.deepEqual(where, {date: undefined}); }); it('should skip conversion if a simple property produces NaN for numbers', function () { where = model._coerce({age: 'xyz'}); assert.deepEqual(where, {age: 'xyz'}); }); it('should skip conversion if an array property produces NaN for numbers', function () { where = model._coerce({age: {inq: ['xyz', '12']}}); assert.deepEqual(where, {age: {inq: ['xyz', 12]}}); }); }); describe('Load models from json', function () { it('should be able to define models from json', function () { var path = require('path'), fs = require('fs'); /** * Load LDL schemas from a json doc * @param schemaFile The dataSource json file * @returns A map of schemas keyed by name */ function loadSchemasSync(schemaFile, dataSource) { // Set up the data source if (!dataSource) { dataSource = new DataSource('memory'); } // Read the dataSource JSON file var schemas = JSON.parse(fs.readFileSync(schemaFile)); return dataSource.modelBuilder.buildModels(schemas); } var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json')); models.should.have.property('AnonymousModel_0'); models.AnonymousModel_0.should.have.property('modelName', 'AnonymousModel_0'); var m1 = new models.AnonymousModel_0({title: 'Test'}); m1.should.have.property('title', 'Test'); m1.should.have.property('author', 'Raymond'); models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json')); models.should.have.property('Address'); models.should.have.property('Account'); models.should.have.property('Customer'); for (var s in models) { var m = models[s]; assert(new m()); } }); it('should allow customization of default model base class', function () { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, age: Number }); modelBuilder.defaultModelBaseClass = User; var Customer = modelBuilder.define('Customer', {customerId: {type: String, id: true}}); assert(Customer.prototype instanceof User); }); it('should allow model base class', function () { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, age: Number }); var Customer = modelBuilder.define('Customer', {customerId: {type: String, id: true}}, {}, User); assert(Customer.prototype instanceof User); }); it('should be able to extend models', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String, bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, age: Number }); var Customer = User.extend('Customer', {customerId: {type: String, id: true}}); var customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'}); customer.should.be.a('object').and.have.property('name', 'Joe'); customer.should.have.property('name', 'Joe'); customer.should.have.property('age', 20); customer.should.have.property('customerId', 'c01'); customer.should.not.have.property('bio'); // The properties are defined at prototype level assert.equal(Object.keys(customer).filter(function (k) { // Remove internal properties return k.indexOf('__') === -1; }).length, 0); var count = 0; for (var p in customer) { if (p.indexOf('__') === 0) { continue; } if (typeof customer[p] !== 'function') { count++; } } assert.equal(count, 7); // Please note there is an injected id from User prototype assert.equal(Object.keys(customer.toObject()).filter(function (k) { // Remove internal properties return k.indexOf('__') === -1; }).length, 6); done(null, customer); }); it('should be able to extend models with merged settings', function (done) { var modelBuilder = new ModelBuilder(); var User = modelBuilder.define('User', { name: String }, { defaultPermission: 'ALLOW', acls: [ { principalType: 'ROLE', principalId: '$everyone', permission: 'ALLOW' } ], relations: { posts: { type: 'hasMany', model: 'Post' } } }); var Customer = User.extend('Customer', {customerId: {type: String, id: true}}, { defaultPermission: 'DENY', acls: [ { principalType: 'ROLE', principalId: '$unauthenticated', permission: 'DENY' } ], relations: { orders: { type: 'hasMany', model: 'Order' } } } ); assert.deepEqual(User.settings, { defaultPermission: 'ALLOW', acls: [ { principalType: 'ROLE', principalId: '$everyone', permission: 'ALLOW' } ], relations: { posts: { type: 'hasMany', model: 'Post' } }, strict: false }); assert.deepEqual(Customer.settings, { defaultPermission: 'DENY', acls: [ { principalType: 'ROLE', principalId: '$everyone', permission: 'ALLOW' }, { principalType: 'ROLE', principalId: '$unauthenticated', permission: 'DENY' } ], relations: { posts: { type: 'hasMany', model: 'Post' }, orders: { type: 'hasMany', model: 'Order' } }, strict: false }); done(); }); }); describe('DataSource constructor', function () { it('Takes url as the settings', function () { var ds = new DataSource('memory://localhost/mydb?x=1'); assert.equal(ds.connector.name, 'memory'); }); it('Takes connector name', function () { var ds = new DataSource('memory'); assert.equal(ds.connector.name, 'memory'); }); it('Takes settings object', function () { var ds = new DataSource({connector: 'memory'}); assert.equal(ds.connector.name, 'memory'); }); it('Takes settings object and name', function () { var ds = new DataSource('x', {connector: 'memory'}); assert.equal(ds.connector.name, 'memory'); }); }); describe('ModelBuilder options.models', function () { it('should inject model classes from models', function () { var builder = new ModelBuilder(); var M1 = builder.define('M1'); var M2 = builder.define('M2', {}, {models: { 'M1': M1 }}); assert.equal(M2.M1, M1, 'M1 should be injected to M2'); }); it('should inject model classes by name in the models', function () { var builder = new ModelBuilder(); var M1 = builder.define('M1'); var M2 = builder.define('M2', {}, {models: { 'M1': 'M1' }}); assert.equal(M2.M1, M1, 'M1 should be injected to M2'); }); it('should inject model classes by name in the models before the class is defined', function () { var builder = new ModelBuilder(); var M2 = builder.define('M2', {}, {models: { 'M1': 'M1' }}); assert(M2.M1, 'M1 should be injected to M2'); assert(M2.M1.settings.unresolved, 'M1 is still a proxy'); var M1 = builder.define('M1'); assert.equal(M2.M1, M1, 'M1 should be injected to M2'); }); });