From 7f170a0542d5e000b5bc2691f79018546e6bf01b Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 4 Oct 2013 21:21:12 -0700 Subject: [PATCH 1/5] Add more tests and fix toJSON --- lib/model-definition.js | 36 +++++----- test/model-definition.test.js | 121 ++++++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 28 deletions(-) diff --git a/lib/model-definition.js b/lib/model-definition.js index 3ec785b1..bb45fdf1 100644 --- a/lib/model-definition.js +++ b/lib/model-definition.js @@ -56,7 +56,7 @@ require('./types')(ModelDefinition); ModelDefinition.prototype.tableName = function (connectorType) { var settings = this.settings; if(settings[connectorType]) { - return settings[connectorType].table || this.name; + return settings[connectorType].table || settings[connectorType].tableName || this.name; } else { return this.name; } @@ -75,7 +75,7 @@ ModelDefinition.prototype.columnName = function (connectorType, propertyName) { this.build(); var property = this.properties[propertyName]; if(property && property[connectorType]) { - return property[connectorType].columnName || propertyName; + return property[connectorType].column || property[connectorType].columnName || propertyName; } else { return propertyName; } @@ -111,7 +111,7 @@ ModelDefinition.prototype.columnNames = function (connectorType) { var cols = []; for(var p in props) { if(props[p][connectorType]) { - cols.push(props[p][connectorType].columnName || p); + cols.push(property[connectorType].column || props[p][connectorType].columnName || p); } else { cols.push(p); } @@ -231,11 +231,7 @@ ModelDefinition.prototype.resolveType = function(type) { if (type.type) { return this.resolveType(type.type); } else { - if(!this.anonymousTypesCount) { - this.anonymousTypesCount = 0; - } - this.anonymousTypesCount++; - return this.modelBuilder.define('AnonymousType' + this.anonymousTypesCount, + return this.modelBuilder.define(this.modelBuilder.getSchemaName(null), type, {anonymous: true, idInjection: false}); /* console.error(type); @@ -278,10 +274,13 @@ ModelDefinition.prototype.build = function (forceRebuild) { var typeDef = { type: type }; - for (var a in this.rawProperties[p]) { - // Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class - if (a !== 'type') { - typeDef[a] = this.rawProperties[p][a]; + var prop = this.rawProperties[p]; + if (typeof prop === 'object' && prop !== null) { + for (var a in prop) { + // Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class + if (a !== 'type') { + typeDef[a] = prop[a]; + } } } this.properties[p] = typeDef; @@ -302,15 +301,10 @@ ModelDefinition.prototype.defineProperty = function (propertyName, propertyDefin function isModelClass(cls) { - while (true) { - if (!cls) { - return false; - } - if (ModelBaseClass === cls) { - return true; - } - cls = cls.prototype; + if(!cls) { + return false; } + return cls.prototype instanceof ModelBaseClass; } ModelDefinition.prototype.toJSON = function(forceRebuild) { @@ -338,7 +332,7 @@ ModelDefinition.prototype.toJSON = function(forceRebuild) { if('function' === typeof val) { if(isModelClass(val)) { if(val.settings && val.settings.anonymous) { - return val.definition && val.definition.toJSON(); + return val.definition && val.definition.toJSON().properties; } else { return val.modelName; } diff --git a/test/model-definition.test.js b/test/model-definition.test.js index e907f1d9..ea9cfc8e 100644 --- a/test/model-definition.test.js +++ b/test/model-definition.test.js @@ -14,15 +14,55 @@ describe('ModelDefinition class', function () { var modelBuilder = new ModelBuilder(); var User = new ModelDefinition(modelBuilder, 'User', { - name: String, + name: "string", bio: ModelBuilder.Text, approved: Boolean, joinedAt: Date, - age: Number + age: "number" }); - // console.log(User.toJSON()); + User.build(); + assert.equal(User.properties.name.type, String); + assert.equal(User.properties.bio.type, ModelBuilder.Text); + assert.equal(User.properties.approved.type, Boolean); + assert.equal(User.properties.joinedAt.type, Date); + assert.equal(User.properties.age.type, Number); + + var json = User.toJSON(); + assert.equal(json.name, "User"); + assert.equal(json.properties.name.type, "String"); + assert.equal(json.properties.bio.type, "Text"); + assert.equal(json.properties.approved.type, "Boolean"); + assert.equal(json.properties.joinedAt.type, "Date"); + assert.equal(json.properties.age.type, "Number"); + + done(); + + + }); + + it('should be able to define additional properties', function (done) { + var modelBuilder = new ModelBuilder(); + + var User = new ModelDefinition(modelBuilder, 'User', { + name: "string", + bio: ModelBuilder.Text, + approved: Boolean, + joinedAt: Date, + age: "number" + }); + + User.build(); + + User.defineProperty("id", {type: "number", id: true}); + assert.equal(User.properties.name.type, String); + assert.equal(User.properties.bio.type, ModelBuilder.Text); + assert.equal(User.properties.approved.type, Boolean); + assert.equal(User.properties.joinedAt.type, Date); + assert.equal(User.properties.age.type, Number); + + assert.equal(User.properties.id.type, Number); done(); @@ -46,11 +86,29 @@ describe('ModelDefinition class', function () { } }); - // console.log(JSON.stringify(User.toJSON(), null, ' ')); + User.build(); + assert.equal(User.properties.name.type, String); + assert.equal(User.properties.bio.type, ModelBuilder.Text); + assert.equal(User.properties.approved.type, Boolean); + assert.equal(User.properties.joinedAt.type, Date); + assert.equal(User.properties.age.type, Number); + assert.equal(typeof User.properties.address.type, 'function'); + + var json = User.toJSON(); + assert.equal(json.name, "User"); + assert.equal(json.properties.name.type, "String"); + assert.equal(json.properties.bio.type, "Text"); + assert.equal(json.properties.approved.type, "Boolean"); + assert.equal(json.properties.joinedAt.type, "Date"); + assert.equal(json.properties.age.type, "Number"); + + assert.deepEqual(json.properties.address.type, { street: { type: 'String' }, + city: { type: 'String' }, + zipCode: { type: 'String' }, + state: { type: 'String' } }); done(); - }); @@ -73,11 +131,60 @@ describe('ModelDefinition class', function () { }); - // console.log(JSON.stringify(User.toJSON(), null, ' ')); + User.build(); + assert.equal(User.properties.name.type, String); + assert.equal(User.properties.bio.type, ModelBuilder.Text); + assert.equal(User.properties.approved.type, Boolean); + assert.equal(User.properties.joinedAt.type, Date); + assert.equal(User.properties.age.type, Number); + assert.equal(User.properties.address.type, Address); + + + var json = User.toJSON(); + assert.equal(json.name, "User"); + assert.equal(json.properties.name.type, "String"); + assert.equal(json.properties.bio.type, "Text"); + assert.equal(json.properties.approved.type, "Boolean"); + assert.equal(json.properties.joinedAt.type, "Date"); + assert.equal(json.properties.age.type, "Number"); + + assert.equal(json.properties.address.type, 'Address'); done(); - }); + + it('should report correct id names', function (done) { + var modelBuilder = new ModelBuilder(); + + var User = new ModelDefinition(modelBuilder, 'User', { + userId: {type: String, id: true}, + name: "string", + bio: ModelBuilder.Text, + approved: Boolean, + joinedAt: Date, + age: "number" + }); + + assert.equal(User.idName(), 'userId'); + assert.deepEqual(User.idNames(), ['userId']); + done(); + }); + + it('should report correct table/column names', function (done) { + var modelBuilder = new ModelBuilder(); + + var User = new ModelDefinition(modelBuilder, 'User', { + userId: {type: String, id: true, oracle: {column: 'ID'}}, + name: "string" + }, {oracle: {table: 'USER'}}); + + assert.equal(User.tableName('oracle'), 'USER'); + assert.equal(User.tableName('mysql'), 'User'); + assert.equal(User.columnName('oracle', 'userId'), 'ID'); + assert.equal(User.columnName('mysql', 'userId'), 'userId'); + done(); + }); + }); From 2f575482c036483b10d9fd023dd3447307c8d25f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 5 Oct 2013 10:53:13 -0700 Subject: [PATCH 2/5] Code clean up --- lib/model-definition.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/model-definition.js b/lib/model-definition.js index bb45fdf1..c141fdb8 100644 --- a/lib/model-definition.js +++ b/lib/model-definition.js @@ -233,10 +233,6 @@ ModelDefinition.prototype.resolveType = function(type) { } else { return this.modelBuilder.define(this.modelBuilder.getSchemaName(null), type, {anonymous: true, idInjection: false}); - /* - console.error(type); - throw new Error('Missing type property'); - */ } } else if('function' === typeof type ) { return type; @@ -259,22 +255,19 @@ ModelDefinition.prototype.build = function (forceRebuild) { } this.properties = {}; for (var p in this.rawProperties) { - var type = this.resolveType(this.rawProperties[p]); + var prop = this.rawProperties[p]; + var type = this.resolveType(prop); if (typeof type === 'string') { - if (Array.isArray(this.associations)) { - this.associations.push({ - source: this.name, - target: type, - relation: Array.isArray(this.rawProperties[p]) ? 'hasMany' : 'belongsTo', - as: p - }); - // delete this.rawProperties[p]; - } + this.associations.push({ + source: this.name, + target: type, + relation: Array.isArray(prop) ? 'hasMany' : 'belongsTo', + as: p + }); } else { var typeDef = { type: type }; - var prop = this.rawProperties[p]; if (typeof prop === 'object' && prop !== null) { for (var a in prop) { // Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class From 294ebc1ccf0bfa592360edd9337d6ec735baaef3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 5 Oct 2013 11:13:10 -0700 Subject: [PATCH 3/5] Allow to reference a model as type by name --- lib/model-definition.js | 2 +- test/model-definition.test.js | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/lib/model-definition.js b/lib/model-definition.js index c141fdb8..6fae9191 100644 --- a/lib/model-definition.js +++ b/lib/model-definition.js @@ -220,7 +220,7 @@ ModelDefinition.prototype.resolveType = function(type) { } } if (typeof type === 'string') { - var schemaType = ModelDefinition.schemaTypes[type.toLowerCase()]; + var schemaType = ModelDefinition.schemaTypes[type.toLowerCase()] || this.modelBuilder.models[type]; if (schemaType) { return schemaType; } else { diff --git a/test/model-definition.test.js b/test/model-definition.test.js index ea9cfc8e..3fed1eb8 100644 --- a/test/model-definition.test.js +++ b/test/model-definition.test.js @@ -154,6 +154,48 @@ describe('ModelDefinition class', function () { }); + it('should be able to define referencing models by name', function (done) { + var modelBuilder = new ModelBuilder(); + + var Address = modelBuilder.define('Address', { + street: String, + city: String, + zipCode: String, + state: String + }); + var User = new ModelDefinition(modelBuilder, 'User', { + name: String, + bio: ModelBuilder.Text, + approved: Boolean, + joinedAt: Date, + age: Number, + address: 'Address' + + }); + + User.build(); + assert.equal(User.properties.name.type, String); + assert.equal(User.properties.bio.type, ModelBuilder.Text); + assert.equal(User.properties.approved.type, Boolean); + assert.equal(User.properties.joinedAt.type, Date); + assert.equal(User.properties.age.type, Number); + assert.equal(User.properties.address.type, Address); + + + var json = User.toJSON(); + assert.equal(json.name, "User"); + assert.equal(json.properties.name.type, "String"); + assert.equal(json.properties.bio.type, "Text"); + assert.equal(json.properties.approved.type, "Boolean"); + assert.equal(json.properties.joinedAt.type, "Date"); + assert.equal(json.properties.age.type, "Number"); + + assert.equal(json.properties.address.type, 'Address'); + + done(); + + }); + it('should report correct id names', function (done) { var modelBuilder = new ModelBuilder(); From 931636eda4210abb9d82d4dab6ec7f6ab69dbeb8 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 6 Oct 2013 21:13:52 -0700 Subject: [PATCH 4/5] Allow the id(s) to be redefined by subclass --- lib/model-builder.js | 21 ++++++++++++++++----- test/loopback-dl.test.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/model-builder.js b/lib/model-builder.js index c27300bd..bdb842cb 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -217,18 +217,30 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett subclassProperties = subclassProperties || {}; subclassSettings = subclassSettings || {}; - + + // Check if subclass redefines the ids + var idFound = false; + for(var k in subclassProperties) { + if(subclassProperties[k].id) { + idFound = true; + break; + } + } + // Merging the properties Object.keys(properties).forEach(function (key) { - // don't inherit the id property - if(key !== 'id' && typeof subclassProperties[key] === 'undefined') { + if(idFound && properties[key].id) { + // don't inherit id properties + return; + } + if(subclassProperties[key] === undefined) { subclassProperties[key] = properties[key]; } }); // Merge the settings Object.keys(settings).forEach(function (key) { - if(typeof subclassSettings[key] === 'undefined') { + if(subclassSettings[key] === undefined) { subclassSettings[key] = settings[key]; } }); @@ -376,7 +388,6 @@ ModelBuilder.prototype.extendModel = function (model, props) { var definition = props[propName]; t.defineProperty(model, propName, definition); }); - t.definitions[model].build(true); }; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index e095a0cb..c884e9d3 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -429,5 +429,40 @@ describe('Load models from json', function () { assert(new m()); } }); + + 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).length, 0); + var count = 0; + for(var p in customer) { + 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()).length, 6); + + done(null, customer); + }); }); From 0bfc362b18815943064ec6ed5f3809b8a7693aba Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 6 Oct 2013 21:27:02 -0700 Subject: [PATCH 5/5] Use for-in loop to support properties from the super class --- lib/connectors/memory.js | 4 ++-- lib/dao.js | 16 ++++++++-------- lib/model.js | 20 ++++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 7af66e29..c974a0b9 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -117,7 +117,7 @@ Memory.prototype.fromDb = function(model, data) { if (!data) return null; data = JSON.parse(data); var props = this._models[model].properties; - Object.keys(data).forEach(function (key) { + for(var key in data) { var val = data[key]; if (typeof val === 'undefined' || val === null) { return; @@ -136,7 +136,7 @@ Memory.prototype.fromDb = function(model, data) { } } data[key] = val; - }); + } return data; }; diff --git a/lib/dao.js b/lib/dao.js index 9bee58a3..6d2675f9 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -58,14 +58,14 @@ DataAccessObject._forDB = function (data) { return data; } var res = {}; - Object.keys(data).forEach(function (propName) { + for(var propName in data) { var type = this.getPropertyType(propName); if (type === 'JSON' || type === 'Any' || type === 'Object' || data[propName] instanceof Array) { res[propName] = JSON.stringify(data[propName]); } else { res[propName] = data[propName]; } - }.bind(this)); + } return res; }; @@ -700,9 +700,9 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb } // update instance's properties - Object.keys(data).forEach(function (key) { + for(var key in data) { inst[key] = data[key]; - }); + } inst.isValid(function (valid) { if (!valid) { @@ -713,16 +713,16 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb inst.trigger('save', function (saveDone) { inst.trigger('update', function (done) { - Object.keys(data).forEach(function (key) { + for(var key in data) { inst[key] = data[key]; - }); + } inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), inst.constructor._forDB(data), function (err) { if (!err) { // update $was attrs - Object.keys(data).forEach(function (key) { + for(var key in data) { inst.__dataWas[key] = inst.__data[key]; - }); + }; } done.call(inst, function () { saveDone.call(inst, function () { diff --git a/lib/model.js b/lib/model.js index 766f7d45..88a8ceb3 100644 --- a/lib/model.js +++ b/lib/model.js @@ -99,20 +99,20 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { } if (applySetters === true) { - Object.keys(data).forEach(function (propertyName) { + for(var propertyName in data) { if((propertyName in properties) || (propertyName in ctor.relations)) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } - }); + } } // Set the unknown properties as properties to the object if(strict === false) { - Object.keys(data).forEach(function (propertyName) { + for(var propertyName in data) { if(!(propertyName in properties)) { self[propertyName] = self.__data[propertyName] || data[propertyName]; } - }); + } } ctor.forEachProperty(function (propertyName) { @@ -224,7 +224,7 @@ ModelBaseClass.prototype.toObject = function (onlySchema) { }); if (schemaLess) { - Object.keys(self.__data).forEach(function (propertyName) { + for(var propertyName in self.__data) { if (!data.hasOwnProperty(propertyName)) { var val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName]; if(val !== undefined && val!== null && val.toObject) { @@ -233,7 +233,7 @@ ModelBaseClass.prototype.toObject = function (onlySchema) { data[propertyName] = val; } } - }); + } } return data; }; @@ -248,9 +248,9 @@ ModelBaseClass.prototype.toJSON = function () { }; ModelBaseClass.prototype.fromObject = function (obj) { - Object.keys(obj).forEach(function (key) { + for(var key in obj) { this[key] = obj[key]; - }.bind(this)); + } }; /** @@ -271,14 +271,14 @@ ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName */ ModelBaseClass.prototype.reset = function () { var obj = this; - Object.keys(obj).forEach(function (k) { + for(var k in obj) { if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) { delete obj[k]; } if (obj.propertyChanged(k)) { obj[k] = obj[k + '$was']; } - }); + } }; ModelBaseClass.prototype.inspect = function () {