From 1963ea9fb10f0743cd370725d8b8f50597e87b02 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 2 Oct 2013 15:18:50 -0700 Subject: [PATCH] Use ModelDefinition to access model name/properties/settings --- lib/connector.js | 8 +- lib/dao.js | 2 +- lib/datasource.js | 5 +- lib/model-builder.js | 254 +++++++++++++++------------------------ lib/model-definition.js | 73 +++++++---- lib/model.js | 6 +- test/loopback-dl.test.js | 6 +- 7 files changed, 164 insertions(+), 190 deletions(-) diff --git a/lib/connector.js b/lib/connector.js index 561bee63..9ebea6af 100644 --- a/lib/connector.js +++ b/lib/connector.js @@ -86,11 +86,11 @@ Connector.prototype.define = function (modelDefinition) { /** * Hook to be called by DataSource for defining a model property * @param {String} model The model name - * @param {String} prop The property name - * @param {Object} params The object for property metadata + * @param {String} propertyName The property name + * @param {Object} propertyDefinition The object for property metadata */ -Connector.prototype.defineProperty = function (model, prop, params) { - this._models[model].properties[prop] = params; +Connector.prototype.defineProperty = function (model, propertyName, propertyDefinition) { + this._models[model].properties[propertyName] = propertyDefinition; }; /** diff --git a/lib/dao.js b/lib/dao.js index 4dec58e8..9bee58a3 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -371,7 +371,7 @@ DataAccessObject.find = function find(params, cb) { // normalize fields as array of included property names if(fields) { - params.fields = fieldsToArray(fields, Object.keys(this.properties)); + params.fields = fieldsToArray(fields, Object.keys(this.definition.properties)); } if(near) { diff --git a/lib/datasource.js b/lib/datasource.js index 7196d4f0..a7301b76 100644 --- a/lib/datasource.js +++ b/lib/datasource.js @@ -340,7 +340,7 @@ DataSource.prototype.createModel = DataSource.prototype.define = function define // pass control to connector this.connector.define({ model: NewClass, - properties: properties, + properties: NewClass.definition.properties, settings: settings }); } @@ -441,8 +441,9 @@ DataSource.prototype.attach = function (ModelCtor) { DataSource.prototype.defineProperty = function (model, prop, params) { ModelBuilder.prototype.defineProperty.call(this, model, prop, params); + var resolvedProp = this.definitions[model].properties[prop]; if (this.connector.defineProperty) { - this.connector.defineProperty(model, prop, params); + this.connector.defineProperty(model, prop, resolvedProp); } }; diff --git a/lib/model-builder.js b/lib/model-builder.js index c51d3d92..25a18179 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -104,8 +104,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett settings.strict = false; } - this.buildSchema(className, properties); - // every class can receive hash of data as optional param var ModelClass = function ModelConstructor(data, dataSource) { if(!(this instanceof ModelConstructor)) { @@ -116,14 +114,16 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett hiddenProperty(this, 'dataSource', dataSource || this.constructor.dataSource); } }; - - // mix in EventEmitter (dont inherit from) - var events = new EventEmitter(); - ModelClass.on = events.on.bind(events); - ModelClass.once = events.once.bind(events); - ModelClass.emit = events.emit.bind(events); - ModelClass.setMaxListeners = events.setMaxListeners.bind(events); + // mix in EventEmitter (don't inherit from) + var events = new EventEmitter(); + for (var f in EventEmitter.prototype) { + if (typeof EventEmitter.prototype[f]) { + ModelClass[f] = events[f].bind(events); + } + } + + // Add metadata to the ModelClass hiddenProperty(ModelClass, 'dataSource', dataSource); hiddenProperty(ModelClass, 'schema', dataSource); // For backward compatibility hiddenProperty(ModelClass, 'modelName', className); @@ -141,82 +141,105 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett ModelClass.getter = {}; ModelClass.setter = {}; - normalize(properties, settings); var modelDefinition = new ModelDefinition(this, className, properties, settings); // store class in model pool this.models[className] = ModelClass; - this.definitions[className] = { - properties: properties, - settings: settings, - schema: modelDefinition - }; + this.definitions[className] = modelDefinition; // expose properties on the ModelClass - ModelClass.properties = properties; - ModelClass.settings = settings; ModelClass.definition = modelDefinition; var idInjection = settings.idInjection; if(idInjection !== false) { + // Default to true if undefined idInjection = true; } - for(var p in properties) { - if(properties[p].id) { - idInjection = false; - ModelClass.prototype.__defineGetter__('id', function () { - return this.__data[p]; - }); - break; - } + + var idNames = modelDefinition.idNames(); + if(idNames.length > 0) { + // id already exists + idInjection = false; } + // Add the id property if (idInjection) { - - ModelClass.prototype.__defineGetter__('id', function () { - return this.__data.id; - }); - // Set up the id property - properties.id = properties.id || { type: Number, id: 1, generated: true }; - if (!properties.id.id) { - properties.id.id = 1; - } + ModelClass.definition.defineProperty('id', { type: Number, id: 1, generated: true }); } + idNames = modelDefinition.idNames(); // Reload it after rebuild + // Create a virtual property 'id' + if (idNames.length === 1) { + var idProp = idNames[0]; + if (idProp !== 'id') { + Object.defineProperty(ModelClass.prototype, 'id', { + get: function () { + var idProp = ModelClass.definition.idNames[0]; + return this.__data[idProp]; + }, + configurable: true, + enumerable: false + }); + } + } else { + // Now the id property is an object that consists of multiple keys + Object.defineProperty(ModelClass.prototype, 'id', { + get: function () { + var compositeId = {}; + var idNames = ModelClass.definition.idNames(); + for (var p in idNames) { + compositeId[p] = this.__data[p]; + } + return compositeId; + }, + configurable: true, + enumerable: false + }); + } + + // A function to loop through the properties ModelClass.forEachProperty = function (cb) { - Object.keys(properties).forEach(cb); + Object.keys(ModelClass.definition.properties).forEach(cb); }; + // A function to attach the model class to a data source ModelClass.attachTo = function (dataSource) { dataSource.attach(this); }; - + + // A function to extend the model ModelClass.extend = function (className, subclassProperties, subclassSettings) { + var properties = ModelClass.definition.properties; + var settings = ModelClass.definition.settings; + subclassProperties = subclassProperties || {}; subclassSettings = subclassSettings || {}; - + // Merging the properties Object.keys(properties).forEach(function (key) { - // dont inherit the id property + // don't inherit the id property if(key !== 'id' && typeof subclassProperties[key] === 'undefined') { subclassProperties[key] = properties[key]; } }); - + + // Merge the settings Object.keys(settings).forEach(function (key) { if(typeof subclassSettings[key] === 'undefined') { subclassSettings[key] = settings[key]; } }); - - var c = dataSource.define(className, subclassProperties, subclassSettings, ModelClass); - - if(typeof c.setup === 'function') { - c.setup.call(c); + + // Define the subclass + var subClass = dataSource.define(className, subclassProperties, subclassSettings, ModelClass); + + // Calling the setup function + if(typeof subClass.setup === 'function') { + subClass.setup.call(subClass); } - return c; + return subClass; }; /** @@ -224,6 +247,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett * @param propertyName */ ModelClass.registerProperty = function (propertyName) { + var properties = modelDefinition.build(); var prop = properties[propertyName]; var DataType = prop.type; if(!DataType) { @@ -309,20 +333,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett }; -/*! - * Normalize the property definitions - * @param properties - * @param settings - */ -function normalize(properties, settings) { - Object.keys(properties).forEach(function (key) { - var v = properties[key]; - if (typeof v === 'function' || Array.isArray(v)) { - properties[key] = { type: v }; - } - }); -} - /** * Define single property named `propertyName` on `model` * @@ -331,7 +341,7 @@ function normalize(properties, settings) { * @param {Object} propertyDefinition - property settings */ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyDefinition) { - this.definitions[model].properties[propertyName] = propertyDefinition; + this.definitions[model].defineProperty(propertyName, propertyDefinition); this.models[model].registerProperty(propertyName); }; @@ -360,11 +370,11 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD */ ModelBuilder.prototype.extendModel = function (model, props) { var t = this; - normalize(props, {}); Object.keys(props).forEach(function (propName) { var definition = props[propName]; t.defineProperty(model, propName, definition); }); + t.definitions[model].build(true); }; @@ -412,89 +422,20 @@ function hiddenProperty(where, property, value) { } /** - * Resolve the type string to be a function, for example, 'String' to String - * @param {String} type The type string, such as 'number', 'Number', 'boolean', or 'String'. It's case insensitive - * @returns {Function} if the type is resolved + * Get the schema name */ -ModelBuilder.prototype.getSchemaType = function(type) { - if (!type) { - return type; +ModelBuilder.prototype.getSchemaName = function (name) { + if (name) { + return name; } - if (Array.isArray(type) && type.length > 0) { - // For array types, the first item should be the type string - var itemType = this.getSchemaType(type[0]); - if (typeof itemType === 'function') { - return [itemType]; - } - else return itemType; // Not resolved, return the type string + if (typeof this._nameCount !== 'number') { + this._nameCount = 0; + } else { + this._nameCount++; } - if (typeof type === 'string') { - var schemaType = ModelBuilder.schemaTypes[type.toLowerCase()]; - if (schemaType) { - return schemaType; - } else { - return type; - } - } else if (type.constructor.name == 'Object') { - // We also support the syntax {type: 'string', ...} - if (type.type) { - return this.getSchemaType(type.type); - } else { - if(!this.anonymousTypesCount) { - this.anonymousTypesCount = 0; - } - this.anonymousTypesCount++; - return this.define('AnonymousType' + this.anonymousTypesCount, type, {idInjection: false}); - /* - console.error(type); - throw new Error('Missing type property'); - */ - } - } else if('function' === typeof type ) { - return type; - } - return type; + return 'AnonymousModel_' + this._nameCount; }; -/** - * Build a dataSource - * @param {String} name The name of the dataSource - * @param {Object} properties The properties of the dataSource - * @param {Object[]} associations An array of associations between models - * @returns {*} - */ -ModelBuilder.prototype.buildSchema = function(name, properties, associations) { - for (var p in properties) { - // console.log(name + "." + p + ": " + properties[p]); - var type = this.getSchemaType(properties[p]); - if (typeof type === 'string') { - // console.log('Association: ' + type); - if (Array.isArray(associations)) { - associations.push({ - source: name, - target: type, - relation: Array.isArray(properties[p]) ? 'hasMany' : 'belongsTo', - as: p - }); - delete properties[p]; - } - } else { - var typeDef = { - type: type - }; - for (var a in properties[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] = properties[p][a]; - } - } - properties[p] = typeDef; - } - } - return properties; -}; - - /** * Build models from dataSource definitions * @@ -510,27 +451,30 @@ ModelBuilder.prototype.buildSchema = function(name, properties, associations) { ModelBuilder.prototype.buildModels = function (schemas) { var models = {}; - if (Array.isArray(schemas)) { - // An array already - } else if (schemas.properties && schemas.name) { - // Only one item - schemas = [schemas]; - } else { - // Anonymous dataSource - schemas = [ - { - name: 'Anonymous', - properties: schemas - } - ]; + // Normalize the schemas to be an array of the schema objects {name: , properties: {}, options: {}} + if (!Array.isArray(schemas)) { + if (schemas.properties && schemas.name) { + // Only one item + schemas = [schemas]; + } else { + // Anonymous dataSource + schemas = [ + { + name: this.getSchemaName(), + properties: schemas, + options: {anonymous: true} + } + ]; + } } var associations = []; for (var s in schemas) { - var name = schemas[s].name; - var dataSource = this.buildSchema(name, schemas[s].properties, associations); - var model = this.define(name, dataSource, schemas[s].options); + var name = this.getSchemaName(schemas[s].name); + schemas[s].name = name; + var model = this.define(schemas[s].name, schemas[s].properties, schemas[s].options); models[name] = model; + associations = associations.concat(model.definition.associations); } // Connect the models based on the associations diff --git a/lib/model-definition.js b/lib/model-definition.js index a981407a..721f2f30 100644 --- a/lib/model-definition.js +++ b/lib/model-definition.js @@ -13,7 +13,7 @@ module.exports = ModelDefinition; /** * Constructor for ModelDefinition * @param {ModelBuilder} modelBuilder A model builder instance - * @param {String} name The model name + * @param {String|Object} name The model name or the schema object * @param {Object} properties The model properties, optional * @param {Object} settings The model settings, optional * @returns {ModelDefinition} @@ -22,22 +22,25 @@ module.exports = ModelDefinition; */ function ModelDefinition(modelBuilder, name, properties, settings) { if (!(this instanceof ModelDefinition)) { + // Allow to call ModelDefinition without new return new ModelDefinition(modelBuilder, name, properties, settings); } this.modelBuilder = modelBuilder || ModelBuilder.defaultInstance; assert(name, 'name is missing'); if (arguments.length === 2 && typeof name === 'object') { - this.name = name.name; - this.properties = name.properties || {}; - this.settings = name.settings || {}; + var schema = name; + this.name = schema.name; + this.rawProperties = schema.properties || {}; // Keep the raw property definitions + this.settings = schema.settings || {}; } else { assert(typeof name === 'string', 'name must be a string'); this.name = name; - this.properties = properties || {}; + this.rawProperties = properties || {}; // Keep the raw property definitions this.settings = settings || {}; } this.associations = []; + this.properties = null; } util.inherits(ModelDefinition, EventEmitter); @@ -69,6 +72,7 @@ ModelDefinition.prototype.columnName = function (connectorType, propertyName) { if(!propertyName) { return propertyName; } + this.build(); var property = this.properties[propertyName]; if(property && property[connectorType]) { return property[connectorType].columnName || propertyName; @@ -87,6 +91,7 @@ ModelDefinition.prototype.columnMetadata = function (connectorType, propertyName if(!propertyName) { return propertyName; } + this.build(); var property = this.properties[propertyName]; if(property && property[connectorType]) { return property[connectorType]; @@ -101,6 +106,7 @@ ModelDefinition.prototype.columnMetadata = function (connectorType, propertyName * @returns {String[]} column names */ ModelDefinition.prototype.columnNames = function (connectorType) { + this.build(); var props = this.properties; var cols = []; for(var p in props) { @@ -122,6 +128,7 @@ ModelDefinition.prototype.ids = function () { return this._ids; } var ids = []; + this.build(); var props = this.properties; for (var key in props) { var id = props[key].id; @@ -175,6 +182,7 @@ ModelDefinition.prototype.idNames = function () { * @returns {{}} */ ModelDefinition.prototype.indexes = function () { + this.build(); var indexes = {}; if (this.settings.indexes) { for (var i in this.settings.indexes) { @@ -239,38 +247,53 @@ ModelDefinition.prototype.resolveType = function(type) { /** * Build a model definition + * @param {Boolean} force Forcing rebuild */ -ModelDefinition.prototype.build = function () { - if (this.resolvedProperties) { - return this.resolvedProperties; +ModelDefinition.prototype.build = function (forceRebuild) { + if(forceRebuild) { + this.properties = null; + this.associations = []; } - this.resolvedProperties = {}; - for (var p in this.properties) { - var type = this.resolveType(this.properties[p]); + if (this.properties) { + return this.properties; + } + this.properties = {}; + for (var p in this.rawProperties) { + var type = this.resolveType(this.rawProperties[p]); if (typeof type === 'string') { if (Array.isArray(this.associations)) { this.associations.push({ source: this.name, target: type, - relation: Array.isArray(this.properties[p]) ? 'hasMany' : 'belongsTo', + relation: Array.isArray(this.rawProperties[p]) ? 'hasMany' : 'belongsTo', as: p }); - // delete this.properties[p]; + // delete this.rawProperties[p]; } } else { var typeDef = { type: type }; - for (var a in this.properties[p]) { + 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.properties[p][a]; + typeDef[a] = this.rawProperties[p][a]; } } - this.resolvedProperties[p] = typeDef; + this.properties[p] = typeDef; } } - return this.resolvedProperties; + return this.properties; +}; + +/** + * Define a property + * @param {String} propertyName The property name + * @param {Object} propertyDefinition The property definition + */ +ModelDefinition.prototype.defineProperty = function (propertyName, propertyDefinition) { + this.rawProperties[propertyName] = propertyDefinition; + this.build(true); }; @@ -286,14 +309,19 @@ function isModelClass(cls) { } } -ModelDefinition.prototype.toJSON = function() { - +ModelDefinition.prototype.toJSON = function(forceRebuild) { + if(forceRebuild) { + this.json = null; + } + if(this.json) { + return json; + } var json = { name: this.name, properties: {}, settings: this.settings }; - this.build(); + this.build(forceRebuild); var mapper = function(val) { if(val === undefined || val === null) { @@ -316,8 +344,9 @@ ModelDefinition.prototype.toJSON = function() { return val; } }; - for(var p in this.resolvedProperties) { - json.properties[p] = traverse(this.resolvedProperties[p]).map(mapper); + for(var p in this.properties) { + json.properties[p] = traverse(this.properties[p]).map(mapper); } + this.json = json; return json; }; diff --git a/lib/model.js b/lib/model.js index dbac8cd2..ec454050 100644 --- a/lib/model.js +++ b/lib/model.js @@ -52,7 +52,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { var self = this; var ctor = this.constructor; - var properties = ctor.properties; + var properties = ctor.definition.build(); data = data || {}; Object.defineProperty(this, '__cachedRelations', { @@ -81,7 +81,7 @@ ModelBaseClass.prototype._initProperties = function (data, applySetters) { } // Check if the strict option is set to false for the model - var strict = ctor.settings.strict; + var strict = ctor.definition.settings.strict; for (var i in data) { if (i in properties) { @@ -208,7 +208,7 @@ ModelBaseClass.prototype.toObject = function (onlySchema) { var data = {}; var self = this; - var schemaLess = this.constructor.settings.strict === false || !onlySchema; + var schemaLess = this.constructor.definition.settings.strict === false || !onlySchema; this.constructor.forEachProperty(function (propertyName) { if (self[propertyName] instanceof List) { data[propertyName] = self[propertyName].toObject(!schemaLess); diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index 53d91a74..c2c4fdca 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -398,10 +398,10 @@ describe('Load models from json', function () { var models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json')); - models.should.have.property('Anonymous'); - models.Anonymous.should.have.property('modelName', 'Anonymous'); + models.should.have.property('AnonymousModel_0'); + models.AnonymousModel_0.should.have.property('modelName', 'AnonymousModel_0'); - var m1 = new models.Anonymous({title: 'Test'}); + var m1 = new models.AnonymousModel_0({title: 'Test'}); m1.should.have.property('title', 'Test'); m1.should.have.property('author', 'Raymond');