2011-10-08 17:11:26 +00:00
/ * *
* Module dependencies
* /
var AbstractClass = require ( './abstract-class' ) . AbstractClass ;
var util = require ( 'util' ) ;
2011-10-23 19:43:53 +00:00
var path = require ( 'path' ) ;
2012-07-02 11:59:44 +00:00
var fs = require ( 'fs' ) ;
var existsSync = fs . existsSync || path . existsSync ;
2011-10-08 17:11:26 +00:00
/ * *
* Export public API
* /
exports . Schema = Schema ;
// exports.AbstractClass = AbstractClass;
/ * *
* Helpers
* /
var slice = Array . prototype . slice ;
/ * *
2012-03-27 14:22:24 +00:00
* Schema - adapter - specific classes factory .
*
* All classes in single schema shares same adapter type and
* one database connection
*
2011-10-08 17:11:26 +00:00
* @ param name - type of schema adapter ( mysql , mongoose , sequelize , redis )
* @ param settings - any database - specific settings which we need to
* establish connection ( of course it depends on specific adapter )
2012-03-27 14:22:24 +00:00
*
* - host
* - port
* - username
* - password
* - database
* - debug { Boolean } = false
*
* @ example Schema creation , waiting for connection callback
* ` ` `
* var schema = new Schema ( 'mysql' , { database : 'myapp_test' } ) ;
* schema . define ( ... ) ;
* schema . on ( 'connected' , function ( ) {
* // work with database
* } ) ;
* ` ` `
2011-10-08 17:11:26 +00:00
* /
function Schema ( name , settings ) {
2011-11-11 13:16:09 +00:00
var schema = this ;
2011-10-08 17:11:26 +00:00
// just save everything we get
this . name = name ;
this . settings = settings ;
2012-04-10 14:30:55 +00:00
// Disconnected by default
this . connected = false ;
2011-10-08 17:11:26 +00:00
// create blank models pool
this . models = { } ;
this . definitions = { } ;
// and initialize schema using adapter
// this is only one initialization entry point of adapter
// this module should define `adapter` member of `this` (schema)
var adapter ;
2013-01-19 13:50:53 +00:00
if ( typeof name === 'object' ) {
adapter = name ;
this . name = adapter . name ;
} else if ( name . match ( /^\// ) ) {
2012-12-13 21:50:02 +00:00
// try absolute path
adapter = require ( name ) ;
} else if ( existsSync ( _ _dirname + '/adapters/' + name + '.js' ) ) {
// try built-in adapter
2011-10-08 17:11:26 +00:00
adapter = require ( './adapters/' + name ) ;
2011-10-23 19:43:53 +00:00
} else {
2012-12-13 21:50:02 +00:00
// try foreign adapter
2011-10-08 17:11:26 +00:00
try {
2012-11-14 04:29:29 +00:00
adapter = require ( 'jugglingdb-' + name ) ;
2011-10-08 17:11:26 +00:00
} catch ( e ) {
2012-12-21 08:56:16 +00:00
return console . log ( '\nWARNING: JugglingDB adapter "' + name + '" is not installed,\nso your models would not work, to fix run:\n\n npm install jugglingdb-' + name , '\n' ) ;
2011-10-08 17:11:26 +00:00
}
}
adapter . initialize ( this , function ( ) {
2012-03-11 04:48:38 +00:00
// we have an adaper now?
if ( ! this . adapter ) {
throw new Error ( 'Adapter is not defined correctly: it should create `adapter` member of schema' ) ;
}
2011-11-11 13:16:09 +00:00
2012-03-11 04:48:38 +00:00
this . adapter . log = function ( query , start ) {
schema . log ( query , start ) ;
} ;
2011-11-11 13:16:09 +00:00
2012-03-11 04:48:38 +00:00
this . adapter . logger = function ( query ) {
var t1 = Date . now ( ) ;
var log = this . log ;
return function ( q ) {
log ( q || query , t1 ) ;
} ;
2011-11-11 13:16:09 +00:00
} ;
2012-03-11 04:48:38 +00:00
this . connected = true ;
this . emit ( 'connected' ) ;
} . bind ( this ) ) ;
2011-10-08 17:11:26 +00:00
} ;
2011-11-21 21:40:50 +00:00
util . inherits ( Schema , require ( 'events' ) . EventEmitter ) ;
2011-10-08 17:11:26 +00:00
2012-03-27 14:22:24 +00:00
function Text ( ) { }
2011-10-08 17:11:26 +00:00
Schema . Text = Text ;
2012-08-13 06:16:01 +00:00
function JSON ( ) { }
Schema . JSON = JSON ;
2011-10-08 17:11:26 +00:00
/ * *
* Define class
2012-03-27 14:22:24 +00:00
*
* @ param { String } className
* @ param { Object } properties - hash of class properties in format
* ` {property: Type, property2: Type2, ...} `
* or
* ` {property: {type: Type}, property2: {type: Type2}, ...} `
* @ param { Object } settings - other configuration of class
* @ return newly created class
*
* @ example simple case
* ` ` `
2012-04-07 13:43:15 +00:00
* var User = schema . define ( 'User' , {
2012-03-27 14:22:24 +00:00
* email : String ,
* password : String ,
* birthDate : Date ,
* activated : Boolean
* } ) ;
* ` ` `
* @ example more advanced case
* ` ` `
2012-08-16 20:32:04 +00:00
* var User = schema . define ( 'User' , {
2012-03-27 14:22:24 +00:00
* email : { type : String , limit : 150 , index : true } ,
* password : { type : String , limit : 50 } ,
* birthDate : Date ,
* registrationDate : { type : Date , default : function ( ) { return new Date } } ,
* activated : { type : Boolean , default : false }
* } ) ;
* ` ` `
2011-10-08 17:11:26 +00:00
* /
Schema . prototype . define = function defineClass ( className , properties , settings ) {
var schema = this ;
var args = slice . call ( arguments ) ;
if ( ! className ) throw new Error ( 'Class name required' ) ;
if ( args . length == 1 ) properties = { } , args . push ( properties ) ;
if ( args . length == 2 ) settings = { } , args . push ( settings ) ;
standartize ( properties , settings ) ;
// every class can receive hash of data as optional param
2012-10-13 13:59:25 +00:00
var NewClass = function ModelConstructor ( data ) {
2012-01-09 12:59:58 +00:00
if ( ! ( this instanceof ModelConstructor ) ) {
return new ModelConstructor ( data ) ;
}
2011-10-08 17:11:26 +00:00
AbstractClass . call ( this , data ) ;
} ;
2012-10-13 13:59:25 +00:00
hiddenProperty ( NewClass , 'schema' , schema ) ;
hiddenProperty ( NewClass , 'modelName' , className ) ;
hiddenProperty ( NewClass , 'cache' , { } ) ;
hiddenProperty ( NewClass , 'mru' , [ ] ) ;
2012-12-14 15:28:29 +00:00
hiddenProperty ( NewClass , 'relations' , { } ) ;
2011-10-08 17:11:26 +00:00
2012-10-22 13:33:57 +00:00
// inherit AbstractClass methods
for ( var i in AbstractClass ) {
NewClass [ i ] = AbstractClass [ i ] ;
}
for ( var j in AbstractClass . prototype ) {
NewClass . prototype [ j ] = AbstractClass . prototype [ j ] ;
}
NewClass . getter = { } ;
NewClass . setter = { } ;
2011-10-08 17:11:26 +00:00
// store class in model pool
2012-10-13 13:59:25 +00:00
this . models [ className ] = NewClass ;
2011-10-08 17:11:26 +00:00
this . definitions [ className ] = {
properties : properties ,
settings : settings
} ;
// pass controll to adapter
this . adapter . define ( {
2012-10-13 13:59:25 +00:00
model : NewClass ,
2011-10-08 17:11:26 +00:00
properties : properties ,
settings : settings
} ) ;
2012-10-13 13:59:25 +00:00
NewClass . prototype . _ _defineGetter _ _ ( 'id' , function ( ) {
return this . _ _data . id ;
} ) ;
properties . id = properties . id || { type : Number } ;
NewClass . forEachProperty = function ( cb ) {
Object . keys ( properties ) . forEach ( cb ) ;
} ;
NewClass . registerProperty = function ( attr ) {
Object . defineProperty ( NewClass . prototype , attr , {
get : function ( ) {
if ( NewClass . getter [ attr ] ) {
return NewClass . getter [ attr ] . call ( this ) ;
} else {
return this . _ _data [ attr ] ;
}
} ,
set : function ( value ) {
if ( NewClass . setter [ attr ] ) {
NewClass . setter [ attr ] . call ( this , value ) ;
} else {
this . _ _data [ attr ] = value ;
}
} ,
configurable : true ,
enumerable : true
} ) ;
NewClass . prototype . _ _defineGetter _ _ ( attr + '_was' , function ( ) {
return this . _ _dataWas [ attr ] ;
} ) ;
Object . defineProperty ( NewClass . prototype , '_' + attr , {
get : function ( ) {
return this . _ _data [ attr ] ;
} ,
set : function ( value ) {
this . _ _data [ attr ] = value ;
} ,
configurable : true ,
enumerable : false
} ) ;
} ;
NewClass . forEachProperty ( NewClass . registerProperty ) ;
return NewClass ;
2011-10-08 17:11:26 +00:00
function standartize ( properties , settings ) {
Object . keys ( properties ) . forEach ( function ( key ) {
var v = properties [ key ] ;
2012-09-10 15:57:21 +00:00
if (
typeof v === 'function' ||
typeof v === 'object' && v && v . constructor . name === 'Array'
) {
2011-10-08 17:11:26 +00:00
properties [ key ] = { type : v } ;
}
} ) ;
// TODO: add timestamps fields
// when present in settings: {timestamps: true}
// or {timestamps: {created: 'created_at', updated: false}}
// by default property names: createdAt, updatedAt
}
} ;
2012-03-27 14:22:24 +00:00
/ * *
* Define single property named ` prop ` on ` model `
*
* @ param { String } model - name of model
* @ param { String } prop - name of propery
* @ param { Object } params - property settings
* /
Schema . prototype . defineProperty = function ( model , prop , params ) {
this . definitions [ model ] . properties [ prop ] = params ;
2012-10-13 13:59:25 +00:00
this . models [ model ] . registerProperty ( prop ) ;
2012-03-27 14:22:24 +00:00
if ( this . adapter . defineProperty ) {
this . adapter . defineProperty ( model , prop , params ) ;
}
} ;
2013-01-21 18:48:04 +00:00
/ * *
* Extend existing model with bunch of properties
*
* @ param { String } model - name of model
* @ param { Object } props - hash of properties
*
* Example :
*
* // Instead of doing this:
*
* // amend the content model with competition attributes
* db . defineProperty ( 'Content' , 'competitionType' , { type : String } ) ;
* db . defineProperty ( 'Content' , 'expiryDate' , { type : Date , index : true } ) ;
* db . defineProperty ( 'Content' , 'isExpired' , { type : Boolean , index : true } ) ;
*
* // schema.extend allows to
* // extend the content model with competition attributes
* db . extendModel ( 'Content' , {
* competitionType : String ,
* expiryDate : { type : Date , index : true } ,
* isExpired : { type : Boolean , index : true }
* } ) ;
* /
Schema . prototype . extendModel = function ( model , props ) {
var t = this ;
Object . keys ( props ) . forEach ( function ( propName ) {
var definition = props [ propName ] ;
if ( typeof definition === 'function' || typeof definition !== 'object' ) {
definition = { type : definition } ;
}
t . defineProperty ( model , propName , definition ) ;
} ) ;
} ;
2012-03-27 14:22:24 +00:00
/ * *
* Drop each model table and re - create .
* This method make sense only for sql adapters .
*
* @ warning All data will be lost ! Use autoupdate if you need your data .
* /
Schema . prototype . automigrate = function ( cb ) {
this . freeze ( ) ;
if ( this . adapter . automigrate ) {
this . adapter . automigrate ( cb ) ;
} else if ( cb ) {
cb ( ) ;
}
} ;
/ * *
* Update existing database tables .
* This method make sense only for sql adapters .
* /
Schema . prototype . autoupdate = function ( cb ) {
this . freeze ( ) ;
if ( this . adapter . autoupdate ) {
this . adapter . autoupdate ( cb ) ;
} else if ( cb ) {
cb ( ) ;
}
} ;
/ * *
* Check whether migrations needed
* This method make sense only for sql adapters .
* /
Schema . prototype . isActual = function ( cb ) {
this . freeze ( ) ;
if ( this . adapter . isActual ) {
this . adapter . isActual ( cb ) ;
} else if ( cb ) {
cb ( null , true ) ;
}
} ;
/ * *
* Log benchmarked message . Do not redefine this method , if you need to grab
* chema logs , use ` schema.on('log', ...) ` emitter event
*
* @ private used by adapters
* /
Schema . prototype . log = function ( sql , t ) {
this . emit ( 'log' , sql , t ) ;
} ;
/ * *
* Freeze schema . Behavior depends on adapter
* /
Schema . prototype . freeze = function freeze ( ) {
if ( this . adapter . freezeSchema ) {
this . adapter . freezeSchema ( ) ;
}
}
/ * *
* Return table name for specified ` modelName `
* @ param { String } modelName
* /
2012-03-10 07:55:25 +00:00
Schema . prototype . tableName = function ( modelName ) {
return this . definitions [ modelName ] . settings . table = this . definitions [ modelName ] . settings . table || modelName
} ;
2012-03-27 14:22:24 +00:00
/ * *
* Define foreign key
* @ param { String } className
* @ param { String } key - name of key field
* /
2011-10-08 17:11:26 +00:00
Schema . prototype . defineForeignKey = function defineForeignKey ( className , key ) {
2012-10-13 13:59:25 +00:00
// quit if key already defined
2011-10-08 17:11:26 +00:00
if ( this . definitions [ className ] . properties [ key ] ) return ;
if ( this . adapter . defineForeignKey ) {
this . adapter . defineForeignKey ( className , key , function ( err , keyType ) {
if ( err ) throw err ;
2011-10-23 19:43:53 +00:00
this . definitions [ className ] . properties [ key ] = { type : keyType } ;
2011-10-08 17:11:26 +00:00
} . bind ( this ) ) ;
} else {
2011-10-23 19:43:53 +00:00
this . definitions [ className ] . properties [ key ] = { type : Number } ;
2011-10-08 17:11:26 +00:00
}
2012-10-13 13:59:25 +00:00
this . models [ className ] . registerProperty ( key ) ;
2011-10-08 17:11:26 +00:00
} ;
2012-03-27 14:22:24 +00:00
/ * *
* Close database connection
* /
2011-10-21 12:46:09 +00:00
Schema . prototype . disconnect = function disconnect ( ) {
if ( typeof this . adapter . disconnect === 'function' ) {
2012-10-21 20:14:05 +00:00
this . connected = false ;
2011-10-21 12:46:09 +00:00
this . adapter . disconnect ( ) ;
}
} ;
2012-03-27 14:22:24 +00:00
/ * *
* Define hidden property
* /
2011-10-10 13:22:51 +00:00
function hiddenProperty ( where , property , value ) {
Object . defineProperty ( where , property , {
writable : false ,
enumerable : false ,
configurable : false ,
value : value
} ) ;
}
2012-10-13 13:59:25 +00:00
/ * *
* Define readonly property on object
*
* @ param { Object } obj
* @ param { String } key
* @ param { Mixed } value
* /
function defineReadonlyProp ( obj , key , value ) {
Object . defineProperty ( obj , key , {
writable : false ,
enumerable : true ,
configurable : true ,
value : value
} ) ;
}