// Copyright IBM Corp. 2013,2016. All Rights Reserved. // Node module: loopback-datasource-juggler // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT // 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.type('object').and.have.property('User').exactly(User); modelBuilder.definitions.should.be.type('object').and.have.property('User'); var user = new User({ name: 'Joe', age: 20, xyz: false }); User.modelName.should.equal('User'); user.should.be.type('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.have.property('bio', undefined); 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.type('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.have.property('bio', undefined); 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.type('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.type('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.type('object').and.have.property('User', User); modelBuilder.definitions.should.be.type('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.type('object').and.have.property('name', 'Joe'); user.should.have.property('name', 'Joe'); user.should.have.property('age', 20); user.should.have.property('bio', undefined); 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.type('object'); assert(user.name === 'Joe'); user.address.should.have.property('city', 'San Jose'); user.address.should.have.property('state', 'CA'); done(null, User); }); it('should define an id property for composite ids', function() { var modelBuilder = new ModelBuilder(); var Follow = modelBuilder.define('Follow', { followerId: { type: String, id: 1 }, followeeId: { type: String, id: 2 }, followAt: Date, }); var follow = new Follow({ followerId: 1, followeeId: 2 }); follow.should.have.property('id'); assert.deepEqual(follow.id, { followerId: 1, followeeId: 2 }); }); }); 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.type('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); }); describe('strict mode "validate"', function() { it('should report validation error for unknown properties', function() { var ds = new DataSource('memory'); var User = ds.define('User', { name: String }, { strict: 'validate' }); var user = new User({ name: 'Joe', age: 20 }); user.isValid().should.be.false; var codes = user.errors && user.errors.codes || {}; codes.should.have.property('age').eql(['unknown-property']); }); }); 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.type('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.type('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.type('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.type('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.type('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'); }); it('should allow an explicit remoting path with leading /', 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 inherit properties from base option', function() { var ds = new ModelBuilder(); var User = ds.define('User', { name: String }); var Customer = ds.define('Customer', { vip: Boolean }, { base: 'User' }); Customer.definition.properties.should.have.property('name'); Customer.definition.properties.name.should.have.property('type', String); }); it('should inherit properties by clone from base option', function() { var ds = new ModelBuilder(); var User = ds.define('User', { name: String }); var Customer1 = ds.define('Customer1', { vip: Boolean }, { base: 'User' }); var Customer2 = ds.define('Customer2', { vip: Boolean }, { base: 'User' }); Customer1.definition.properties.should.have.property('name'); Customer2.definition.properties.should.have.property('name'); Customer1.definition.properties.name.should.not.be.equal( Customer2.definition.properties.name); Customer1.definition.properties.name.should.eql( Customer2.definition.properties.name); }); it('should revert properties from base model', function() { var ds = new ModelBuilder(); var User = ds.define('User', { username: String, email: String }); var Customer = ds.define('Customer', { name: String, username: null, email: false }, { base: 'User' }); Customer.definition.properties.should.have.property('name'); // username/email are now shielded Customer.definition.properties.should.not.have.property('username'); Customer.definition.properties.should.not.have.property('email'); var c = new Customer({ name: 'John' }); c.should.have.property('username', undefined); c.should.have.property('email', undefined); c.should.have.property('name', 'John'); var u = new User({ username: 'X', email: 'x@y.com' }); u.should.not.have.property('name'); u.should.have.property('username', 'X'); u.should.have.property('email', 'x@y.com'); }); 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); Customer.definition.properties.should.have.property('name'); Customer.definition.properties.name.should.have.property('type', String); 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 handle hasMany through options', function(done) { var ds = new DataSource('memory'); var Physician = ds.createModel('Physician', { name: String }, { relations: { patients: { model: 'Patient', type: 'hasMany', foreignKey: 'leftId', through: 'Appointment', }, }, } ); var Patient = ds.createModel('Patient', { name: String }, { relations: { physicians: { model: 'Physician', type: 'hasMany', foreignKey: 'rightId', through: 'Appointment', }, }, } ); 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'].keyTo === 'leftId'); assert(Patient.relations['physicians'].keyTo === 'rightId'); 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' }); // Mockup MongoDB ObjectID function ObjectID(id) { this.id = id; } ObjectID.prototype.toString = function() { return this.id; }; where = model._coerce({ id: new ObjectID('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 apply settings for handling undefined', function() { filter = model._normalize({ filter: { x: undefined }}); assert.deepEqual(filter, { filter: {}}); ds.settings.normalizeUndefinedInQuery = 'ignore'; filter = model._normalize({ filter: { x: undefined }}); assert.deepEqual(filter, { filter: {}}, 'Should ignore undefined'); ds.settings.normalizeUndefinedInQuery = 'nullify'; filter = model._normalize({ filter: { x: undefined }}); assert.deepEqual(filter, { filter: { x: null }}, 'Should nullify undefined'); ds.settings.normalizeUndefinedInQuery = 'throw'; (function() { model._normalize({ filter: { x: undefined }}); }).should.throw(/`undefined` in query/); }); 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] }}); }); // settings it('should get settings in priority', function() { ds.settings.test = 'test'; assert.equal(model._getSetting('test'), ds.settings.test, 'Should get datasource setting'); ds.settings.test = undefined; model.settings.test = 'test'; assert.equal(model._getSetting('test'), model.settings.test, 'Should get model settings'); ds.settings.test = 'willNotGet'; assert.notEqual(model._getSetting('test'), ds.settings.test, 'Should not get datasource setting'); }); }); describe('Load 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) { var modelBuilder, createModel; // Set up the data source if (!dataSource) { modelBuilder = new ModelBuilder(); } else { modelBuilder = dataSource.modelBuilder; createModel = dataSource.createModel.bind(dataSource); } // Read the dataSource JSON file var schemas = JSON.parse(fs.readFileSync(schemaFile)); return modelBuilder.buildModels(schemas, createModel); } it('should be able to define models from json', function() { 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 be able to define models from json using dataSource', function() { var ds = new DataSource('memory'); var models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json'), ds); models.should.have.property('Address'); models.should.have.property('Account'); models.should.have.property('Customer'); assert.equal(models.Address.dataSource, ds); }); 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.type('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.have.property('bio', undefined); // 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, base: User, }); 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'); }); it('should use false strict mode for embedded models by default', function() { var builder = new ModelBuilder(); var M1 = builder.define('testEmbedded', { name: 'string', address: { street: 'string', }, }); var m1 = new M1({ name: 'Jim', address: { street: 'washington st', number: 5512, }, }); assert.equal(m1.address.number, 5512, 'm1 should contain number property in address'); }); it('should use the strictEmbeddedModels setting (true) when applied on modelBuilder', function() { var builder = new ModelBuilder(); builder.settings.strictEmbeddedModels = true; var M1 = builder.define('testEmbedded', { name: 'string', address: { street: 'string', }, }); var m1 = new M1({ name: 'Jim', address: { street: 'washington st', number: 5512, }, }); assert.equal(m1.address.number, undefined, 'm1 should not contain number property in address'); }); it('should use strictEmbeddedModels setting (validate) when applied on modelBuilder', function() { var builder = new ModelBuilder(); builder.settings.strictEmbeddedModels = 'validate'; var M1 = builder.define('testEmbedded', { name: 'string', address: { street: 'string', }, }); var m1 = new M1({ name: 'Jim', address: { street: 'washington st', number: 5512, }, }); assert.equal(m1.address.number, undefined, 'm1 should not contain number property in address'); assert.equal(m1.address.isValid(), false, 'm1 address should not validate with extra property'); var codes = m1.address.errors && m1.address.errors.codes || {}; assert.deepEqual(codes.number, ['unknown-property']); }); it('should use the strictEmbeddedModels setting (throw) when applied on modelBuilder', function() { var builder = new ModelBuilder(); builder.settings.strictEmbeddedModels = 'throw'; var M1 = builder.define('testEmbedded', { name: 'string', address: { street: 'string', }, }); assert.throws(function() { var m1 = new M1({ name: 'Jim', address: { street: 'washington st', number: 5512, }, }); }); }); });