2013-08-24 16:38:12 +00:00
|
|
|
var assert = require('assert');
|
|
|
|
var util = require('util');
|
|
|
|
var EventEmitter = require('events').EventEmitter;
|
2013-10-02 05:14:21 +00:00
|
|
|
var traverse = require('traverse');
|
|
|
|
var ModelBaseClass = require('./model');
|
|
|
|
var ModelBuilder = require('./model-builder');
|
2013-08-24 16:38:12 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Model definition
|
|
|
|
*/
|
|
|
|
module.exports = ModelDefinition;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Constructor for ModelDefinition
|
2013-10-02 05:14:21 +00:00
|
|
|
* @param {ModelBuilder} modelBuilder A model builder instance
|
2013-10-02 22:18:50 +00:00
|
|
|
* @param {String|Object} name The model name or the schema object
|
2013-10-02 05:14:21 +00:00
|
|
|
* @param {Object} properties The model properties, optional
|
|
|
|
* @param {Object} settings The model settings, optional
|
2013-08-24 16:38:12 +00:00
|
|
|
* @returns {ModelDefinition}
|
|
|
|
* @constructor
|
|
|
|
*
|
|
|
|
*/
|
2013-10-02 05:14:21 +00:00
|
|
|
function ModelDefinition(modelBuilder, name, properties, settings) {
|
2013-08-24 16:38:12 +00:00
|
|
|
if (!(this instanceof ModelDefinition)) {
|
2013-10-02 22:18:50 +00:00
|
|
|
// Allow to call ModelDefinition without new
|
2013-10-02 05:14:21 +00:00
|
|
|
return new ModelDefinition(modelBuilder, name, properties, settings);
|
2013-08-24 16:38:12 +00:00
|
|
|
}
|
2013-10-02 05:14:21 +00:00
|
|
|
this.modelBuilder = modelBuilder || ModelBuilder.defaultInstance;
|
2013-08-24 16:38:12 +00:00
|
|
|
assert(name, 'name is missing');
|
|
|
|
|
2013-10-02 05:14:21 +00:00
|
|
|
if (arguments.length === 2 && typeof name === 'object') {
|
2013-10-02 22:18:50 +00:00
|
|
|
var schema = name;
|
|
|
|
this.name = schema.name;
|
|
|
|
this.rawProperties = schema.properties || {}; // Keep the raw property definitions
|
|
|
|
this.settings = schema.settings || {};
|
2013-08-24 16:38:12 +00:00
|
|
|
} else {
|
|
|
|
assert(typeof name === 'string', 'name must be a string');
|
|
|
|
this.name = name;
|
2013-10-02 22:18:50 +00:00
|
|
|
this.rawProperties = properties || {}; // Keep the raw property definitions
|
2013-08-24 16:38:12 +00:00
|
|
|
this.settings = settings || {};
|
|
|
|
}
|
2013-10-02 05:14:21 +00:00
|
|
|
this.associations = [];
|
2013-10-02 22:18:50 +00:00
|
|
|
this.properties = null;
|
2013-08-24 16:38:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
util.inherits(ModelDefinition, EventEmitter);
|
|
|
|
|
2013-10-02 05:14:21 +00:00
|
|
|
// Set up types
|
|
|
|
require('./types')(ModelDefinition);
|
2013-08-24 16:38:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return table name for specified `modelName`
|
|
|
|
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.tableName = function (connectorType) {
|
|
|
|
var settings = this.settings;
|
|
|
|
if(settings[connectorType]) {
|
|
|
|
return settings[connectorType].table || this.name;
|
|
|
|
} else {
|
|
|
|
return this.name;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return column name for specified modelName and propertyName
|
|
|
|
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
|
|
|
|
* @param propertyName The property name
|
|
|
|
* @returns {String} columnName
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.columnName = function (connectorType, propertyName) {
|
|
|
|
if(!propertyName) {
|
|
|
|
return propertyName;
|
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build();
|
2013-08-24 16:38:12 +00:00
|
|
|
var property = this.properties[propertyName];
|
|
|
|
if(property && property[connectorType]) {
|
|
|
|
return property[connectorType].columnName || propertyName;
|
|
|
|
} else {
|
|
|
|
return propertyName;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return column metadata for specified modelName and propertyName
|
|
|
|
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
|
|
|
|
* @param propertyName The property name
|
|
|
|
* @returns {Object} column metadata
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.columnMetadata = function (connectorType, propertyName) {
|
|
|
|
if(!propertyName) {
|
|
|
|
return propertyName;
|
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build();
|
2013-08-24 16:38:12 +00:00
|
|
|
var property = this.properties[propertyName];
|
|
|
|
if(property && property[connectorType]) {
|
|
|
|
return property[connectorType];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return column names for specified modelName
|
|
|
|
* @param {String} connectorType The connector type, such as 'oracle' or 'mongodb'
|
|
|
|
* @returns {String[]} column names
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.columnNames = function (connectorType) {
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build();
|
2013-08-24 16:38:12 +00:00
|
|
|
var props = this.properties;
|
|
|
|
var cols = [];
|
|
|
|
for(var p in props) {
|
|
|
|
if(props[p][connectorType]) {
|
|
|
|
cols.push(props[p][connectorType].columnName || p);
|
|
|
|
} else {
|
|
|
|
cols.push(p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cols;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the ID properties sorted by the index
|
|
|
|
* @returns {Object[]} property name/index for IDs
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.ids = function () {
|
2013-10-02 05:14:21 +00:00
|
|
|
if(this._ids) {
|
|
|
|
return this._ids;
|
2013-08-24 16:38:12 +00:00
|
|
|
}
|
|
|
|
var ids = [];
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build();
|
2013-08-24 16:38:12 +00:00
|
|
|
var props = this.properties;
|
|
|
|
for (var key in props) {
|
|
|
|
var id = props[key].id;
|
|
|
|
if(!id) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if(typeof id !== 'number') {
|
|
|
|
id = 1;
|
|
|
|
}
|
|
|
|
ids.push({name: key, id: id});
|
|
|
|
}
|
|
|
|
ids.sort(function (a, b) {
|
|
|
|
return a.key - b.key;
|
|
|
|
});
|
2013-10-02 05:14:21 +00:00
|
|
|
this._ids = ids;
|
2013-08-24 16:38:12 +00:00
|
|
|
return ids;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the ID column name
|
|
|
|
* @param {String} modelName The model name
|
|
|
|
* @returns {String} columnName for ID
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.idColumnName = function(connectorType) {
|
|
|
|
return this.columnName(connectorType, this.idName());
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the ID property name
|
|
|
|
* @returns {String} property name for ID
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.idName = function() {
|
2013-10-02 05:14:21 +00:00
|
|
|
var id = this.ids()[0];
|
2013-10-03 00:20:54 +00:00
|
|
|
if(this.properties.id && this.properties.id.id) {
|
|
|
|
return 'id';
|
|
|
|
}
|
2013-08-24 16:38:12 +00:00
|
|
|
return id && id.name;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the ID property names sorted by the index
|
|
|
|
* @returns {String[]} property names for IDs
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.idNames = function () {
|
2013-10-02 05:14:21 +00:00
|
|
|
var ids = this.ids();
|
2013-08-24 16:38:12 +00:00
|
|
|
var names = ids.map(function (id) {
|
|
|
|
return id.name;
|
|
|
|
});
|
|
|
|
return names;
|
|
|
|
};
|
2013-10-02 05:14:21 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @returns {{}}
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.indexes = function () {
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build();
|
2013-10-02 05:14:21 +00:00
|
|
|
var indexes = {};
|
|
|
|
if (this.settings.indexes) {
|
|
|
|
for (var i in this.settings.indexes) {
|
|
|
|
indexes[i] = this.settings.indexes[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (var p in this.properties) {
|
|
|
|
if (this.properties[p].index) {
|
|
|
|
indexes[p + '_index'] = this.properties[p].index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return indexes;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
ModelDefinition.prototype.resolveType = function(type) {
|
|
|
|
if (!type) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
if (Array.isArray(type) && type.length > 0) {
|
|
|
|
// For array types, the first item should be the type string
|
|
|
|
var itemType = this.resolveType(type[0]);
|
|
|
|
if (typeof itemType === 'function') {
|
|
|
|
return [itemType];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return itemType; // Not resolved, return the type string
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (typeof type === 'string') {
|
|
|
|
var schemaType = ModelDefinition.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.resolveType(type.type);
|
|
|
|
} else {
|
|
|
|
if(!this.anonymousTypesCount) {
|
|
|
|
this.anonymousTypesCount = 0;
|
|
|
|
}
|
|
|
|
this.anonymousTypesCount++;
|
|
|
|
return this.modelBuilder.define('AnonymousType' + this.anonymousTypesCount,
|
|
|
|
type, {anonymous: true, idInjection: false});
|
|
|
|
/*
|
|
|
|
console.error(type);
|
|
|
|
throw new Error('Missing type property');
|
|
|
|
*/
|
|
|
|
}
|
|
|
|
} else if('function' === typeof type ) {
|
|
|
|
return type;
|
|
|
|
}
|
|
|
|
return type;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build a model definition
|
2013-10-02 22:18:50 +00:00
|
|
|
* @param {Boolean} force Forcing rebuild
|
2013-10-02 05:14:21 +00:00
|
|
|
*/
|
2013-10-02 22:18:50 +00:00
|
|
|
ModelDefinition.prototype.build = function (forceRebuild) {
|
|
|
|
if(forceRebuild) {
|
|
|
|
this.properties = null;
|
|
|
|
this.associations = [];
|
2013-10-02 05:14:21 +00:00
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
if (this.properties) {
|
|
|
|
return this.properties;
|
|
|
|
}
|
|
|
|
this.properties = {};
|
|
|
|
for (var p in this.rawProperties) {
|
|
|
|
var type = this.resolveType(this.rawProperties[p]);
|
2013-10-02 05:14:21 +00:00
|
|
|
if (typeof type === 'string') {
|
|
|
|
if (Array.isArray(this.associations)) {
|
|
|
|
this.associations.push({
|
|
|
|
source: this.name,
|
|
|
|
target: type,
|
2013-10-02 22:18:50 +00:00
|
|
|
relation: Array.isArray(this.rawProperties[p]) ? 'hasMany' : 'belongsTo',
|
2013-10-02 05:14:21 +00:00
|
|
|
as: p
|
|
|
|
});
|
2013-10-02 22:18:50 +00:00
|
|
|
// delete this.rawProperties[p];
|
2013-10-02 05:14:21 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var typeDef = {
|
|
|
|
type: type
|
|
|
|
};
|
2013-10-02 22:18:50 +00:00
|
|
|
for (var a in this.rawProperties[p]) {
|
2013-10-02 05:14:21 +00:00
|
|
|
// Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class
|
|
|
|
if (a !== 'type') {
|
2013-10-02 22:18:50 +00:00
|
|
|
typeDef[a] = this.rawProperties[p][a];
|
2013-10-02 05:14:21 +00:00
|
|
|
}
|
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
this.properties[p] = typeDef;
|
2013-10-02 05:14:21 +00:00
|
|
|
}
|
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
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);
|
2013-10-02 05:14:21 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function isModelClass(cls) {
|
|
|
|
while (true) {
|
|
|
|
if (!cls) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (ModelBaseClass === cls) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
cls = cls.prototype;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-10-02 22:18:50 +00:00
|
|
|
ModelDefinition.prototype.toJSON = function(forceRebuild) {
|
|
|
|
if(forceRebuild) {
|
|
|
|
this.json = null;
|
|
|
|
}
|
|
|
|
if(this.json) {
|
|
|
|
return json;
|
|
|
|
}
|
2013-10-02 05:14:21 +00:00
|
|
|
var json = {
|
|
|
|
name: this.name,
|
|
|
|
properties: {},
|
|
|
|
settings: this.settings
|
|
|
|
};
|
2013-10-02 22:18:50 +00:00
|
|
|
this.build(forceRebuild);
|
2013-10-02 05:14:21 +00:00
|
|
|
|
|
|
|
var mapper = function(val) {
|
|
|
|
if(val === undefined || val === null) {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
if('function' === typeof val.toJSON) {
|
|
|
|
// The value has its own toJSON() object
|
|
|
|
return val.toJSON();
|
|
|
|
}
|
|
|
|
if('function' === typeof val) {
|
|
|
|
if(isModelClass(val)) {
|
|
|
|
if(val.settings && val.settings.anonymous) {
|
|
|
|
return val.definition && val.definition.toJSON();
|
|
|
|
} else {
|
|
|
|
return val.modelName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return val.name;
|
|
|
|
} else {
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
};
|
2013-10-02 22:18:50 +00:00
|
|
|
for(var p in this.properties) {
|
|
|
|
json.properties[p] = traverse(this.properties[p]).map(mapper);
|
2013-10-02 05:14:21 +00:00
|
|
|
}
|
2013-10-02 22:18:50 +00:00
|
|
|
this.json = json;
|
2013-10-02 05:14:21 +00:00
|
|
|
return json;
|
|
|
|
};
|