diff --git a/lib/model.js b/lib/model.js index e98bf164..c5c138d5 100644 --- a/lib/model.js +++ b/lib/model.js @@ -233,36 +233,8 @@ module.exports = function(registry) { }); }; - // resolve relation functions - sharedClass.resolve(function resolver(define) { - var relations = ModelCtor.relations || {}; - - // get the relations - for (var relationName in relations) { - var relation = relations[relationName]; - if (relation.type === 'belongsTo') { - ModelCtor.belongsToRemoting(relationName, relation, define); - } else if ( - relation.type === 'hasOne' || - relation.type === 'embedsOne' - ) { - ModelCtor.hasOneRemoting(relationName, relation, define); - } else if ( - relation.type === 'hasMany' || - relation.type === 'embedsMany' || - relation.type === 'referencesMany') { - ModelCtor.hasManyRemoting(relationName, relation, define); - } - } - - // handle scopes - var scopes = ModelCtor.scopes || {}; - /* eslint-disable one-var */ - for (var scopeName in scopes) { - ModelCtor.scopeRemoting(scopeName, scopes[scopeName], define); - } - /* eslint-enable one-var */ - }); + // setRemoting for relation methods + ModelCtor.setupRemotingRelation(); return ModelCtor; }; @@ -444,131 +416,196 @@ module.exports = function(registry) { this.sharedClass.disableMethod(name, isStatic || false); this.emit('remoteMethodDisabled', this.sharedClass, name); }; + - Model.belongsToRemoting = function(relationName, relation, define) { - var modelName = relation.modelTo && relation.modelTo.modelName; - modelName = modelName || 'PersistedModel'; - var fn = this.prototype[relationName]; - var pathName = (relation.options.http && relation.options.http.path) || relationName; - define('__get__' + relationName, { - isStatic: false, - http: { verb: 'get', path: '/' + pathName }, - accepts: { arg: 'refresh', type: 'boolean', http: { source: 'query' }}, - accessType: 'READ', - description: format('Fetches belongsTo relation %s.', relationName), - returns: { arg: relationName, type: modelName, root: true }, - }, fn); - }; + Model.setupRemotingRelation = function() { + var ModelCtor = this; + var typeName = ModelCtor.modelName; + var options = ModelCtor.settings; - function convertNullToNotFoundError(toModelName, ctx, cb) { - if (ctx.result !== null) return cb(); + var relations = ModelCtor.settings.relations || {}; - var fk = ctx.getArgByName('fk'); - var msg = g.f('Unknown "%s" id "%s".', toModelName, fk); - var error = new Error(msg); - error.statusCode = error.status = 404; - error.code = 'MODEL_NOT_FOUND'; - cb(error); - } + // get the relations + for (var relationName in relations) { + var relation = relations[relationName]; + if (relation.type === 'belongsTo') { + ModelCtor.prototype['__get__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + belongsToRemoting(relationName, relation); + } else if ( + relation.type === 'hasOne' || + relation.type === 'embedsOne' + ) { + ModelCtor.prototype['__get__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, '__get__' + relationName); + }; + ModelCtor.prototype['__create__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, '__create__' + relationName); + }; + ModelCtor.prototype['__update__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, '__update__' + relationName); + }; + ModelCtor.prototype['__destroy__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, '__destory__' + relationName); + }; + hasOneRemoting(relationName, relation); + } else if ( + relation.type === 'hasMany' || + relation.type === 'embedsMany' || + relation.type === 'referencesMany' + ) { + ModelCtor.prototype['__findById__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + ModelCtor.prototype['__destroyById__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + ModelCtor.prototype['__updateById__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + hasManyRemoting(relationName, relation); + + if (relation.through || relation.type === 'referencesMany') { + var modelThrough = relation.through || relation.model; - Model.hasOneRemoting = function(relationName, relation, define) { - var pathName = (relation.options.http && relation.options.http.path) || relationName; - var toModelName = relation.modelTo.modelName; + var accepts = []; + if (relation.type === 'hasMany' && relation.through) { + // Restrict: only hasManyThrough relation can have additional properties + accepts.push({ arg: 'data', type: modelThrough.modelName, http: { source: 'body' }}); + } + ModelCtor.prototype['__link__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + ModelCtor.prototype['__unlink__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + ModelCtor.prototype['__exists__' + relationName] = function(data, callback) { + throwNotAttached(this.modelName, relationName); + }; + throughOrReferencesRemoting(relationName, relation); + } + } + }; - define('__get__' + relationName, { - isStatic: false, - http: { verb: 'get', path: '/' + pathName }, - accepts: { arg: 'refresh', type: 'boolean', http: { source: 'query' }}, - description: format('Fetches hasOne relation %s.', relationName), - accessType: 'READ', - returns: { arg: relationName, type: relation.modelTo.modelName, root: true }, - rest: { after: convertNullToNotFoundError.bind(null, toModelName) }, - }); + function setRemoting(name, options) { + var fn = ModelCtor.prototype[name]; + fn._delegate = true; + ModelCtor.remoteMethod(name, options); + }; - define('__create__' + relationName, { - isStatic: false, - http: { verb: 'post', path: '/' + pathName }, - accepts: { arg: 'data', type: toModelName, http: { source: 'body' }}, - description: format('Creates a new instance in %s of this model.', relationName), - accessType: 'WRITE', - returns: { arg: 'data', type: toModelName, root: true }, - }); + function belongsToRemoting(relationName, relation) { + relation.options = relation.options || {}; + var modelName = relation.model; + var pathName = (relation.options.http && relation.options.http.path) || relationName; - define('__update__' + relationName, { - isStatic: false, - http: { verb: 'put', path: '/' + pathName }, - accepts: { arg: 'data', type: toModelName, http: { source: 'body' }}, - description: format('Update %s of this model.', relationName), - accessType: 'WRITE', - returns: { arg: 'data', type: toModelName, root: true }, - }); + setRemoting('__get__' + relationName, { + isStatic: false, + http: { verb: 'get', path: '/' + pathName }, + accepts: { arg: 'refresh', type: 'boolean', http: { source: 'query' }}, + accessType: 'READ', + description: format('Fetches belongsTo relation %s.', relationName), + returns: { arg: relationName, type: modelName, root: true }, + }); + }; - define('__destroy__' + relationName, { - isStatic: false, - http: { verb: 'delete', path: '/' + pathName }, - description: format('Deletes %s of this model.', relationName), - accessType: 'WRITE', - }); - }; + function convertNullToNotFoundError(toModelName, ctx, cb) { + if (ctx.result !== null) return cb(); - Model.hasManyRemoting = function(relationName, relation, define) { - var pathName = (relation.options.http && relation.options.http.path) || relationName; - var toModelName = relation.modelTo.modelName; + var fk = ctx.getArgByName('fk'); + var msg = g.f('Unknown "%s" id "%s".', toModelName, fk); + var error = new Error(msg); + error.statusCode = error.status = 404; + error.code = 'MODEL_NOT_FOUND'; + cb(error); + }; - var findByIdFunc = this.prototype['__findById__' + relationName]; - define('__findById__' + relationName, { - isStatic: false, - http: { verb: 'get', path: '/' + pathName + '/:fk' }, - accepts: { arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: { source: 'path' }}, - description: format('Find a related item by id for %s.', relationName), - accessType: 'READ', - returns: { arg: 'result', type: toModelName, root: true }, - rest: { after: convertNullToNotFoundError.bind(null, toModelName) }, - }, findByIdFunc); + function hasOneRemoting(relationName, relation) { + relation.options = relation.options || {}; + var pathName = (relation.options.http && relation.options.http.path) || relationName; + var toModelName = relation.modelName; - var destroyByIdFunc = this.prototype['__destroyById__' + relationName]; - define('__destroyById__' + relationName, { - isStatic: false, - http: { verb: 'delete', path: '/' + pathName + '/:fk' }, - accepts: { arg: 'fk', type: 'any', - description: format('Foreign key for %s', relationName), - required: true, - http: { source: 'path' }}, - description: format('Delete a related item by id for %s.', relationName), - accessType: 'WRITE', - returns: [], - }, destroyByIdFunc); + setRemoting('__get__' + relationName, { + isStatic: false, + http: { verb: 'get', path: '/' + pathName }, + accepts: { arg: 'refresh', type: 'boolean', http: { source: 'query' }}, + description: format('Fetches hasOne relation %s.', relationName), + accessType: 'READ', + returns: { arg: relationName, type: toModelName, root: true }, + rest: { after: convertNullToNotFoundError.bind(null, toModelName) }, + }); - var updateByIdFunc = this.prototype['__updateById__' + relationName]; - define('__updateById__' + relationName, { - isStatic: false, - http: { verb: 'put', path: '/' + pathName + '/:fk' }, - accepts: [ - { arg: 'fk', type: 'any', + setRemoting('__create__' + relationName, { + isStatic: false, + http: { verb: 'post', path: '/' + pathName }, + accepts: { arg: 'data', type: toModelName, http: { source: 'body' }}, + description: format('Creates a new instance in %s of this model.', relationName), + accessType: 'WRITE', + returns: { arg: 'data', type: toModelName, root: true }, + }); + + setRemoting('__update__' + relationName, { + isStatic: false, + http: { verb: 'put', path: '/' + pathName }, + accepts: { arg: 'data', type: toModelName, http: { source: 'body' }}, + description: format('Update %s of this model.', relationName), + accessType: 'WRITE', + returns: { arg: 'data', type: toModelName, root: true }, + }); + + setRemoting('__destroy__' + relationName, { + isStatic: false, + http: { verb: 'delete', path: '/' + pathName }, + description: format('Deletes %s of this model.', relationName), + accessType: 'WRITE', + }); + }; + function hasManyRemoting(relationName, relation) { + relation.options = relation.options || {}; + var pathName = (relation.options.http && relation.options.http.path) || relationName; + var toModelName = relation.modelName; + + setRemoting('__findById__' + relationName, { + isStatic: false, + http: { verb: 'get', path: '/' + pathName + '/:fk' }, + accepts: { arg: 'fk', type: 'any', description: format('Foreign key for %s', relationName), required: true, http: { source: 'path' }}, - { arg: 'data', type: toModelName, http: { source: 'body' }}, - ], - description: format('Update a related item by id for %s.', relationName), - accessType: 'WRITE', - returns: { arg: 'result', type: toModelName, root: true }, - }, updateByIdFunc); + description: format('Find a related item by id for %s.', relationName), + accessType: 'READ', + returns: { arg: 'result', type: toModelName, root: true }, + rest: { after: convertNullToNotFoundError.bind(null, toModelName) }, + }); + setRemoting('__destroyById__' + relationName, { + isStatic: false, + http: { verb: 'delete', path: '/' + pathName + '/:fk' }, + accepts: { arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: { source: 'path' }}, + description: format('Delete a related item by id for %s.', relationName), + accessType: 'WRITE', + returns: [], + }); + setRemoting('__updateById__' + relationName, { + isStatic: false, + http: { verb: 'put', path: '/' + pathName + '/:fk' }, + accepts: [ + { arg: 'fk', type: 'any', + description: format('Foreign key for %s', relationName), + required: true, + http: { source: 'path' }}, + { arg: 'data', type: toModelName, http: { source: 'body' }}, + ], + description: format('Update a related item by id for %s.', relationName), + accessType: 'WRITE', + returns: { arg: 'result', type: toModelName, root: true }, + }); + }; - if (relation.modelThrough || relation.type === 'referencesMany') { - var modelThrough = relation.modelThrough || relation.modelTo; - - var accepts = []; - if (relation.type === 'hasMany' && relation.modelThrough) { - // Restrict: only hasManyThrough relation can have additional properties - accepts.push({ arg: 'data', type: modelThrough.modelName, http: { source: 'body' }}); - } - - var addFunc = this.prototype['__link__' + relationName]; - define('__link__' + relationName, { + function throughOrReferenceRemoting(relationName, relation) { + setRemoting('__link__' + relationName, { isStatic: false, http: { verb: 'put', path: '/' + pathName + '/rel/:fk' }, accepts: [{ arg: 'fk', type: 'any', @@ -578,10 +615,9 @@ module.exports = function(registry) { description: format('Add a related item by id for %s.', relationName), accessType: 'WRITE', returns: { arg: relationName, type: modelThrough.modelName, root: true }, - }, addFunc); + }); - var removeFunc = this.prototype['__unlink__' + relationName]; - define('__unlink__' + relationName, { + setRemoting('__unlink__' + relationName, { isStatic: false, http: { verb: 'delete', path: '/' + pathName + '/rel/:fk' }, accepts: { arg: 'fk', type: 'any', @@ -591,12 +627,11 @@ module.exports = function(registry) { description: format('Remove the %s relation to an item by id.', relationName), accessType: 'WRITE', 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, { + setRemoting('__exists__' + relationName, { isStatic: false, http: { verb: 'head', path: '/' + pathName + '/rel/:fk' }, accepts: { arg: 'fk', type: 'any', @@ -622,8 +657,23 @@ module.exports = function(registry) { } }, }, - }, existsFunc); - } + }); + }; + // // setRemoting for scope methods + // var scopes = ModelCtor.scopes || {}; + // /* eslint-disable one-var */ + // for (var scopeName in scopes) { + // ModelCtor.scopeRemoting(scopeName, scopes[scopeName], define); + // } + // /* eslint-enable one-var */ + function throwNotAttached(modelName, methodName) { + throw new Error( + g.f('Cannot call %s.%s().' + + ' The %s method has not been setup.' + + ' The {{Model}} has not been correctly attached to a {{DataSource}}!', + modelName, methodName, methodName) + ); + }; }; Model.scopeRemoting = function(scopeName, scope, define) {