2013-12-20 01:49:47 +00:00
/ * !
2013-07-15 18:32:00 +00:00
* Module Dependencies .
* /
2014-06-06 09:47:25 +00:00
var registry = require ( '../registry' ) ;
2013-12-10 23:57:55 +00:00
var assert = require ( 'assert' ) ;
2014-05-19 22:56:26 +00:00
var SharedClass = require ( 'strong-remoting' ) . SharedClass ;
2013-07-15 18:32:00 +00:00
/ * *
2014-01-28 20:54:41 +00:00
* The base class for * * all models * * .
2013-12-20 01:49:47 +00:00
*
2014-01-28 20:54:41 +00:00
* * * Inheriting from ` Model ` * *
*
* ` ` ` js
* var properties = { ... } ;
* var options = { ... } ;
* var MyModel = loopback . Model . extend ( 'MyModel' , properties , options ) ;
* ` ` `
*
* * * Options * *
*
* - ` trackChanges ` - If true , changes to the model will be tracked . * * Required
* for replication . * *
*
* * * Events * *
*
* # # # # Event : ` changed `
*
* Emitted after a model has been successfully created , saved , or updated .
*
* ` ` ` js
* MyModel . on ( 'changed' , function ( inst ) {
* console . log ( 'model with id %s has been changed' , inst . id ) ;
* // => model with id 1 has been changed
* } ) ;
* ` ` `
*
* # # # # Event : ` deleted `
*
* Emitted after an individual model has been deleted .
*
* ` ` ` js
* MyModel . on ( 'deleted' , function ( inst ) {
* console . log ( 'model with id %s has been deleted' , inst . id ) ;
* // => model with id 1 has been deleted
* } ) ;
* ` ` `
*
* # # # # Event : ` deletedAll `
*
* Emitted after an individual model has been deleted .
*
* ` ` ` js
* MyModel . on ( 'deletedAll' , function ( where ) {
* if ( where ) {
* console . log ( 'all models where' , where , 'have been deleted' ) ;
* // => all models where
* // => {price: {gt: 100}}
* // => have been deleted
* }
* } ) ;
* ` ` `
*
* # # # # Event : ` attached `
*
* Emitted after a ` Model ` has been attached to an ` app ` .
*
* # # # # Event : ` dataSourceAttached `
*
* Emitted after a ` Model ` has been attached to a ` DataSource ` .
*
2013-12-20 01:49:47 +00:00
* @ class
* @ param { Object } data
2014-01-28 20:54:41 +00:00
* @ property { String } modelName The name of the model
* @ property { DataSource } dataSource
2013-07-15 18:32:00 +00:00
* /
2014-07-10 20:46:06 +00:00
var Model = module . exports = registry . modelBuilder . define ( 'Model' ) ;
2013-07-15 18:32:00 +00:00
/ * !
2013-07-15 21:07:17 +00:00
* Called when a model is extended .
2013-07-15 18:32:00 +00:00
* /
Model . setup = function ( ) {
var ModelCtor = this ;
2014-01-28 20:54:41 +00:00
var options = this . settings ;
2014-05-16 19:32:54 +00:00
2014-05-19 22:56:26 +00:00
// create a sharedClass
var sharedClass = ModelCtor . sharedClass = new SharedClass (
2014-07-21 14:56:46 +00:00
ModelCtor . modelName ,
2014-05-19 22:56:26 +00:00
ModelCtor ,
options . remoting
) ;
// support remoting prototype methods
2013-07-16 03:14:04 +00:00
ModelCtor . sharedCtor = function ( data , id , fn ) {
2014-05-19 22:56:26 +00:00
var ModelCtor = this ;
2013-07-16 02:45:27 +00:00
if ( typeof data === 'function' ) {
fn = data ;
data = null ;
id = null ;
} else if ( typeof id === 'function' ) {
fn = id ;
if ( typeof data !== 'object' ) {
id = data ;
data = null ;
} else {
id = null ;
}
}
if ( id && data ) {
2013-07-16 19:51:59 +00:00
var model = new ModelCtor ( data ) ;
2013-07-16 02:45:27 +00:00
model . id = id ;
fn ( null , model ) ;
} else if ( data ) {
2013-07-16 19:51:59 +00:00
fn ( null , new ModelCtor ( data ) ) ;
2013-07-16 02:45:27 +00:00
} else if ( id ) {
2013-08-17 05:11:58 +00:00
ModelCtor . findById ( id , function ( err , model ) {
2013-07-16 02:45:27 +00:00
if ( err ) {
fn ( err ) ;
} else if ( model ) {
fn ( null , model ) ;
} else {
err = new Error ( 'could not find a model with id ' + id ) ;
err . statusCode = 404 ;
fn ( err ) ;
}
} ) ;
} else {
fn ( new Error ( 'must specify an id or data' ) ) ;
}
}
2014-05-19 22:56:26 +00:00
var idDesc = ModelCtor . modelName + ' id' ;
ModelCtor . sharedCtor . accepts = [
2014-07-24 17:26:49 +00:00
{ arg : 'id' , type : 'any' , required : true , http : { source : 'path' } ,
description : idDesc }
2014-05-19 22:56:26 +00:00
// {arg: 'instance', type: 'object', http: {source: 'body'}}
] ;
ModelCtor . sharedCtor . http = [
{ path : '/:id' }
] ;
ModelCtor . sharedCtor . returns = { root : true } ;
2013-07-16 02:45:27 +00:00
// before remote hook
ModelCtor . beforeRemote = function ( name , fn ) {
var self = this ;
if ( this . app ) {
var remotes = this . app . remotes ( ) ;
2014-07-21 14:56:46 +00:00
var className = self . modelName ;
2014-01-22 10:22:23 +00:00
remotes . before ( className + '.' + name , function ( ctx , next ) {
2013-07-16 02:45:27 +00:00
fn ( ctx , ctx . result , next ) ;
} ) ;
} else {
var args = arguments ;
this . once ( 'attached' , function ( ) {
self . beforeRemote . apply ( self , args ) ;
} ) ;
}
2013-11-20 21:31:30 +00:00
} ;
2013-07-16 02:45:27 +00:00
2013-07-16 17:25:02 +00:00
// after remote hook
ModelCtor . afterRemote = function ( name , fn ) {
var self = this ;
if ( this . app ) {
var remotes = this . app . remotes ( ) ;
2014-07-21 14:56:46 +00:00
var className = self . modelName ;
2014-01-22 10:22:23 +00:00
remotes . after ( className + '.' + name , function ( ctx , next ) {
2013-07-16 17:25:02 +00:00
fn ( ctx , ctx . result , next ) ;
} ) ;
} else {
var args = arguments ;
this . once ( 'attached' , function ( ) {
self . afterRemote . apply ( self , args ) ;
} ) ;
}
2013-11-20 21:31:30 +00:00
} ;
2013-08-16 23:39:13 +00:00
2014-05-19 22:56:26 +00:00
// resolve relation functions
sharedClass . resolve ( function resolver ( define ) {
var relations = ModelCtor . relations ;
2014-07-16 23:35:43 +00:00
if ( ! relations ) {
return ;
}
2014-05-19 22:56:26 +00:00
// get the relations
2014-07-16 23:35:43 +00:00
for ( var relationName in relations ) {
2014-05-19 22:56:26 +00:00
var relation = relations [ relationName ] ;
2014-07-16 23:35:43 +00:00
if ( relation . type === 'belongsTo' ) {
2014-05-19 22:56:26 +00:00
ModelCtor . belongsToRemoting ( relationName , relation , define )
2014-07-16 23:35:43 +00:00
} else if ( relation . type === 'hasMany' ) {
ModelCtor . hasManyRemoting ( relationName , relation , define ) ;
ModelCtor . scopeRemoting ( relationName , relation , define ) ;
2014-05-19 22:56:26 +00:00
} else {
ModelCtor . scopeRemoting ( relationName , relation , define ) ;
}
}
} ) ;
2014-05-03 03:04:06 +00:00
2013-07-15 18:32:00 +00:00
return ModelCtor ;
2013-11-20 21:31:30 +00:00
} ;
/ * !
* Get the reference to ACL in a lazy fashion to avoid race condition in require
* /
2014-02-04 00:00:01 +00:00
var _aclModel = null ;
Model . _ACL = function getACL ( ACL ) {
if ( ACL !== undefined ) {
// The function is used as a setter
_aclModel = ACL ;
}
if ( _aclModel ) {
return _aclModel ;
}
var aclModel = require ( './acl' ) . ACL ;
2014-06-06 09:47:25 +00:00
_aclModel = registry . getModelByType ( aclModel ) ;
2014-02-04 00:00:01 +00:00
return _aclModel ;
} ;
2013-11-20 21:31:30 +00:00
/ * *
* Check if the given access token can invoke the method
*
* @ param { AccessToken } token The access token
2014-06-11 21:55:47 +00:00
* @ param { * } modelId The model ID .
* @ param { SharedMethod } sharedMethod The method in question
* @ callback { Function } callback The callback function
2013-11-20 21:31:30 +00:00
* @ param { String | Error } err The error object
2014-06-11 21:55:47 +00:00
* @ param { Boolean } allowed True if the request is allowed ; false otherwise .
2013-11-20 21:31:30 +00:00
* /
2014-01-26 22:02:56 +00:00
2014-05-31 02:29:30 +00:00
Model . checkAccess = function ( token , modelId , sharedMethod , callback ) {
2013-12-10 23:57:55 +00:00
var ANONYMOUS = require ( './access-token' ) . ANONYMOUS ;
token = token || ANONYMOUS ;
2014-02-04 00:00:01 +00:00
var aclModel = Model . _ACL ( ) ;
2014-05-31 02:29:30 +00:00
aclModel . checkAccessForContext ( {
accessToken : token ,
model : this ,
property : sharedMethod . name ,
method : sharedMethod . name ,
sharedMethod : sharedMethod ,
2014-06-02 20:41:14 +00:00
modelId : modelId ,
accessType : this . _getAccessTypeForMethod ( sharedMethod )
2014-05-31 02:29:30 +00:00
} , function ( err , accessRequest ) {
if ( err ) return callback ( err ) ;
callback ( null , accessRequest . isAllowed ( ) ) ;
} ) ;
2013-11-20 21:31:30 +00:00
} ;
2013-12-20 01:49:47 +00:00
/ * !
2013-12-07 01:04:47 +00:00
* Determine the access type for the given ` RemoteMethod ` .
*
* @ api private
* @ param { RemoteMethod } method
* /
Model . _getAccessTypeForMethod = function ( method ) {
if ( typeof method === 'string' ) {
method = { name : method } ;
}
assert (
typeof method === 'object' ,
'method is a required argument and must be a RemoteMethod object'
) ;
2014-02-04 00:00:01 +00:00
var ACL = Model . _ACL ( ) ;
2013-12-07 01:04:47 +00:00
switch ( method . name ) {
case 'create' :
return ACL . WRITE ;
case 'updateOrCreate' :
return ACL . WRITE ;
case 'upsert' :
return ACL . WRITE ;
case 'exists' :
return ACL . READ ;
case 'findById' :
return ACL . READ ;
case 'find' :
return ACL . READ ;
case 'findOne' :
return ACL . READ ;
case 'destroyById' :
return ACL . WRITE ;
case 'deleteById' :
return ACL . WRITE ;
case 'removeById' :
return ACL . WRITE ;
case 'count' :
return ACL . READ ;
default :
return ACL . EXECUTE ;
}
}
2014-02-21 03:43:50 +00:00
/ * *
* Get the ` Application ` the Model is attached to .
*
* @ callback { Function } callback
* @ param { Error } err
* @ param { Application } app
* @ end
* /
Model . getApp = function ( callback ) {
var Model = this ;
if ( this . app ) {
callback ( null , this . app ) ;
} else {
Model . once ( 'attached' , function ( ) {
assert ( Model . app ) ;
callback ( null , Model . app ) ;
} ) ;
}
}
2014-05-16 19:32:54 +00:00
/ * *
* Enable remote invocation for the method with the given name .
*
* @ param { String } name The name of the method .
* ` ` ` js
* // static method example (eg. Model.myMethod())
* Model . remoteMethod ( 'myMethod' ) ;
* @ param { Object } options The remoting options .
* See [ loopback . remoteMethod ( ) ] ( http : //docs.strongloop.com/display/DOC/Remote+methods+and+hooks#Remotemethodsandhooks-loopback.remoteMethod(fn,[options])) for details.
* /
Model . remoteMethod = function ( name , options ) {
2014-05-19 22:56:26 +00:00
if ( options . isStatic === undefined ) {
options . isStatic = true ;
}
2014-05-16 19:32:54 +00:00
this . sharedClass . defineMethod ( name , options ) ;
}
2014-05-19 22:56:26 +00:00
Model . belongsToRemoting = function ( relationName , relation , define ) {
var fn = this . prototype [ relationName ] ;
2014-06-21 06:48:46 +00:00
define ( '__get__' + relationName , {
2014-05-19 22:56:26 +00:00
isStatic : false ,
http : { verb : 'get' , path : '/' + relationName } ,
accepts : { arg : 'refresh' , type : 'boolean' , http : { source : 'query' } } ,
description : 'Fetches belongsTo relation ' + relationName ,
returns : { arg : relationName , type : relation . modelTo . modelName , root : true }
} , fn ) ;
}
2014-07-16 23:35:43 +00:00
Model . hasManyRemoting = function ( relationName , relation , define ) {
2014-07-24 17:26:49 +00:00
var toModelName = relation . modelTo . modelName ;
2014-07-16 23:35:43 +00:00
var findByIdFunc = this . prototype [ '__findById__' + relationName ] ;
define ( '__findById__' + relationName , {
isStatic : false ,
http : { verb : 'get' , path : '/' + relationName + '/:fk' } ,
accepts : { arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
description : 'Find a related item by id for ' + relationName ,
2014-07-24 17:26:49 +00:00
returns : { arg : 'result' , type : toModelName , root : true }
2014-07-16 23:35:43 +00:00
} , findByIdFunc ) ;
var destroyByIdFunc = this . prototype [ '__destroyById__' + relationName ] ;
define ( '__destroyById__' + relationName , {
isStatic : false ,
http : { verb : 'delete' , path : '/' + relationName + '/:fk' } ,
accepts : { arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
description : 'Delete a related item by id for ' + relationName ,
returns : { }
} , destroyByIdFunc ) ;
var updateByIdFunc = this . prototype [ '__updateById__' + relationName ] ;
define ( '__updateById__' + relationName , {
isStatic : false ,
http : { verb : 'put' , path : '/' + relationName + '/:fk' } ,
2014-07-24 17:26:49 +00:00
accepts : [
{ arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
{ arg : 'data' , type : toModelName , http : { source : 'body' } }
] ,
2014-07-16 23:35:43 +00:00
description : 'Update a related item by id for ' + relationName ,
2014-07-24 17:26:49 +00:00
returns : { arg : 'result' , type : toModelName , root : true }
2014-07-16 23:35:43 +00:00
} , updateByIdFunc ) ;
if ( relation . modelThrough ) {
var addFunc = this . prototype [ '__link__' + relationName ] ;
define ( '__link__' + relationName , {
isStatic : false ,
http : { verb : 'put' , path : '/' + relationName + '/rel/:fk' } ,
accepts : { arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
description : 'Add a related item by id for ' + relationName ,
returns : { arg : relationName , type : relation . modelThrough . modelName , root : true }
} , addFunc ) ;
var removeFunc = this . prototype [ '__unlink__' + relationName ] ;
define ( '__unlink__' + relationName , {
isStatic : false ,
http : { verb : 'delete' , path : '/' + relationName + '/rel/:fk' } ,
accepts : { arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
description : 'Remove the ' + relationName + ' relation to an item by id' ,
returns : { }
} , removeFunc ) ;
// FIXME: [rfeng] How to map a function with callback(err, true|false) to HEAD?
// true --> 200 and false --> 404?
var existsFunc = this . prototype [ '__exists__' + relationName ] ;
define ( '__exists__' + relationName , {
isStatic : false ,
http : { verb : 'head' , path : '/' + relationName + '/rel/:fk' } ,
accepts : { arg : 'fk' , type : 'any' ,
description : 'Foreign key for ' + relationName , required : true ,
http : { source : 'path' } } ,
description : 'Check the existence of ' + relationName + ' relation to an item by id' ,
2014-07-28 16:37:50 +00:00
returns : { arg : 'exists' , type : 'boolean' , root : true } ,
rest : {
// After hook to map exists to 200/404 for HEAD
after : function ( ctx , cb ) {
if ( ctx . result === false ) {
var modelName = ctx . method . sharedClass . name ;
var id = ctx . getArgByName ( 'id' ) ;
var msg = 'Unknown "' + modelName + '" id "' + id + '".' ;
var error = new Error ( msg ) ;
error . statusCode = error . status = 404 ;
cb ( error ) ;
} else {
cb ( ) ;
}
}
}
2014-07-16 23:35:43 +00:00
} , existsFunc ) ;
}
} ;
2014-05-19 22:56:26 +00:00
Model . scopeRemoting = function ( relationName , relation , define ) {
var toModelName = relation . modelTo . modelName ;
define ( '__get__' + relationName , {
isStatic : false ,
http : { verb : 'get' , path : '/' + relationName } ,
accepts : { arg : 'filter' , type : 'object' } ,
description : 'Queries ' + relationName + ' of ' + this . modelName + '.' ,
returns : { arg : relationName , type : [ toModelName ] , root : true }
} ) ;
define ( '__create__' + relationName , {
isStatic : false ,
http : { verb : 'post' , path : '/' + relationName } ,
2014-07-24 17:26:49 +00:00
accepts : { arg : 'data' , type : toModelName , http : { source : 'body' } } ,
2014-05-19 22:56:26 +00:00
description : 'Creates a new instance in ' + relationName + ' of this model.' ,
returns : { arg : 'data' , type : toModelName , root : true }
} ) ;
define ( '__delete__' + relationName , {
isStatic : false ,
http : { verb : 'delete' , path : '/' + relationName } ,
description : 'Deletes all ' + relationName + ' of this model.'
} ) ;
}
2013-07-15 18:32:00 +00:00
// setup the initial model
2013-11-11 21:35:54 +00:00
Model . setup ( ) ;
2013-11-20 21:31:30 +00:00