Merge branch 'release/2.1.0' into production
This commit is contained in:
commit
55def26343
|
@ -17,7 +17,7 @@ For more details, see http://loopback.io/.
|
||||||
|
|
||||||
## LoopBack modules
|
## LoopBack modules
|
||||||
|
|
||||||
The LoopBack framework includes of a set of Node.js modules that you can use independently or together.
|
The LoopBack framework is a set of Node.js modules that you can use independently or together.
|
||||||
|
|
||||||
![LoopBack modules](https://github.com/strongloop/loopback/raw/master/docs/assets/lb-modules.png "LoopBack modules")
|
![LoopBack modules](https://github.com/strongloop/loopback/raw/master/docs/assets/lb-modules.png "LoopBack modules")
|
||||||
|
|
||||||
|
|
|
@ -223,13 +223,14 @@ app.models = function () {
|
||||||
*
|
*
|
||||||
* @param {String} name The data source name
|
* @param {String} name The data source name
|
||||||
* @param {Object} config The data source config
|
* @param {Object} config The data source config
|
||||||
|
* @param {DataSource} The registered data source
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.dataSource = function (name, config) {
|
app.dataSource = function (name, config) {
|
||||||
|
var ds = dataSourcesFromConfig(config, this.connectors);
|
||||||
this.dataSources[name] =
|
this.dataSources[name] =
|
||||||
this.dataSources[classify(name)] =
|
this.dataSources[classify(name)] =
|
||||||
this.dataSources[camelize(name)] =
|
this.dataSources[camelize(name)] = ds;
|
||||||
dataSourcesFromConfig(config, this.connectors);
|
return ds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -269,6 +270,7 @@ app.remoteObjects = function () {
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Get a handler of the specified type from the handler cache.
|
* Get a handler of the specified type from the handler cache.
|
||||||
|
* @triggers `mounted` events on shared class constructors (models)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.handler = function (type) {
|
app.handler = function (type) {
|
||||||
|
@ -279,6 +281,11 @@ app.handler = function (type) {
|
||||||
|
|
||||||
var remotes = this.remotes();
|
var remotes = this.remotes();
|
||||||
var handler = this._handlers[type] = remotes.handler(type);
|
var handler = this._handlers[type] = remotes.handler(type);
|
||||||
|
|
||||||
|
remotes.classes().forEach(function(sharedClass) {
|
||||||
|
sharedClass.ctor.emit('mounted', app, sharedClass, remotes);
|
||||||
|
});
|
||||||
|
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,8 +60,8 @@ var ACLSchema = {
|
||||||
property: String, // The name of the property, method, scope, or relation
|
property: String, // The name of the property, method, scope, or relation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the access type - READ/WRITE/EXEC
|
* Name of the access type - READ/WRITE/EXECUTE
|
||||||
* @property accessType {String} Name of the access type - READ/WRITE/EXEC
|
* @property accessType {String} Name of the access type - READ/WRITE/EXECUTE
|
||||||
*/
|
*/
|
||||||
accessType: String,
|
accessType: String,
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ ACL.prototype.debug = function() {
|
||||||
* @property {String|Model} model The model name or model class.
|
* @property {String|Model} model The model name or model class.
|
||||||
* @property {*} id The model instance ID.
|
* @property {*} id The model instance ID.
|
||||||
* @property {String} property The property/method/relation name.
|
* @property {String} property The property/method/relation name.
|
||||||
* @property {String} accessType The access type: READE, WRITE, or EXEC.
|
* @property {String} accessType The access type: READE, WRITE, or EXECUTE.
|
||||||
* @param {Function} callback Callback function
|
* @param {Function} callback Callback function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
var registry = require('../registry');
|
var registry = require('../registry');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var SharedClass = require('strong-remoting').SharedClass;
|
var SharedClass = require('strong-remoting').SharedClass;
|
||||||
|
var extend = require('util')._extend;
|
||||||
|
var stringUtils = require('underscore.string');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base class for **all models**.
|
* The base class for **all models**.
|
||||||
|
@ -192,7 +194,12 @@ Model.setup = function () {
|
||||||
var relation = relations[relationName];
|
var relation = relations[relationName];
|
||||||
if (relation.type === 'belongsTo') {
|
if (relation.type === 'belongsTo') {
|
||||||
ModelCtor.belongsToRemoting(relationName, relation, define)
|
ModelCtor.belongsToRemoting(relationName, relation, define)
|
||||||
} else if (relation.type === 'hasMany') {
|
} else if (relation.type === 'hasOne') {
|
||||||
|
ModelCtor.hasOneRemoting(relationName, relation, define)
|
||||||
|
} else if (
|
||||||
|
relation.type === 'hasMany' ||
|
||||||
|
relation.type === 'embedsMany' ||
|
||||||
|
relation.type === 'referencesMany') {
|
||||||
ModelCtor.hasManyRemoting(relationName, relation, define);
|
ModelCtor.hasManyRemoting(relationName, relation, define);
|
||||||
ModelCtor.scopeRemoting(relationName, relation, define);
|
ModelCtor.scopeRemoting(relationName, relation, define);
|
||||||
} else {
|
} else {
|
||||||
|
@ -320,13 +327,15 @@ Model.getApp = function(callback) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable remote invocation for the method with the given name.
|
* Enable remote invocation for the method with the given name.
|
||||||
|
* See [Remote methods and hooks](http://docs.strongloop.com/display/LB/Remote+methods+and+hooks for more information.
|
||||||
|
* Static method example:
|
||||||
|
* ```js
|
||||||
|
* Model.myMethod();
|
||||||
|
* Model.remoteMethod('myMethod');
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param {String} name The name of the method.
|
* @param {String} name The name of the method.
|
||||||
* ```js
|
|
||||||
* // static method example (eg. Model.myMethod())
|
|
||||||
* Model.remoteMethod('myMethod');
|
|
||||||
* @param {Object} options The remoting options.
|
* @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) {
|
Model.remoteMethod = function(name, options) {
|
||||||
|
@ -338,21 +347,36 @@ Model.remoteMethod = function(name, options) {
|
||||||
|
|
||||||
Model.belongsToRemoting = function(relationName, relation, define) {
|
Model.belongsToRemoting = function(relationName, relation, define) {
|
||||||
var fn = this.prototype[relationName];
|
var fn = this.prototype[relationName];
|
||||||
|
var pathName = (relation.options.http && relation.options.http.path) || relationName;
|
||||||
define('__get__' + relationName, {
|
define('__get__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + relationName},
|
http: {verb: 'get', path: '/' + pathName},
|
||||||
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
||||||
description: 'Fetches belongsTo relation ' + relationName,
|
description: 'Fetches belongsTo relation ' + relationName,
|
||||||
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
|
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
|
||||||
}, fn);
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Model.hasOneRemoting = function(relationName, relation, define) {
|
||||||
|
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'}},
|
||||||
|
description: 'Fetches hasOne relation ' + relationName,
|
||||||
|
returns: {arg: relationName, type: relation.modelTo.modelName, root: true}
|
||||||
|
}, fn);
|
||||||
|
}
|
||||||
|
|
||||||
Model.hasManyRemoting = function (relationName, relation, define) {
|
Model.hasManyRemoting = function (relationName, relation, define) {
|
||||||
|
var pathName = (relation.options.http && relation.options.http.path) || relationName;
|
||||||
var toModelName = relation.modelTo.modelName;
|
var toModelName = relation.modelTo.modelName;
|
||||||
|
|
||||||
var findByIdFunc = this.prototype['__findById__' + relationName];
|
var findByIdFunc = this.prototype['__findById__' + relationName];
|
||||||
define('__findById__' + relationName, {
|
define('__findById__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + relationName + '/:fk'},
|
http: {verb: 'get', path: '/' + pathName + '/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: {arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
|
@ -363,7 +387,7 @@ Model.hasManyRemoting = function (relationName, relation, define) {
|
||||||
var destroyByIdFunc = this.prototype['__destroyById__' + relationName];
|
var destroyByIdFunc = this.prototype['__destroyById__' + relationName];
|
||||||
define('__destroyById__' + relationName, {
|
define('__destroyById__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + relationName + '/:fk'},
|
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: {arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
|
@ -374,7 +398,7 @@ Model.hasManyRemoting = function (relationName, relation, define) {
|
||||||
var updateByIdFunc = this.prototype['__updateById__' + relationName];
|
var updateByIdFunc = this.prototype['__updateById__' + relationName];
|
||||||
define('__updateById__' + relationName, {
|
define('__updateById__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'put', path: '/' + relationName + '/:fk'},
|
http: {verb: 'put', path: '/' + pathName + '/:fk'},
|
||||||
accepts: [
|
accepts: [
|
||||||
{arg: 'fk', type: 'any',
|
{arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
|
@ -385,22 +409,24 @@ Model.hasManyRemoting = function (relationName, relation, define) {
|
||||||
returns: {arg: 'result', type: toModelName, root: true}
|
returns: {arg: 'result', type: toModelName, root: true}
|
||||||
}, updateByIdFunc);
|
}, updateByIdFunc);
|
||||||
|
|
||||||
if (relation.modelThrough) {
|
if (relation.modelThrough || relation.type === 'referencesMany') {
|
||||||
|
var modelThrough = relation.modelThrough || relation.modelTo;
|
||||||
|
|
||||||
var addFunc = this.prototype['__link__' + relationName];
|
var addFunc = this.prototype['__link__' + relationName];
|
||||||
define('__link__' + relationName, {
|
define('__link__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'put', path: '/' + relationName + '/rel/:fk'},
|
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: {arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
description: 'Add a related item by id for ' + relationName,
|
description: 'Add a related item by id for ' + relationName,
|
||||||
returns: {arg: relationName, type: relation.modelThrough.modelName, root: true}
|
returns: {arg: relationName, type: modelThrough.modelName, root: true}
|
||||||
}, addFunc);
|
}, addFunc);
|
||||||
|
|
||||||
var removeFunc = this.prototype['__unlink__' + relationName];
|
var removeFunc = this.prototype['__unlink__' + relationName];
|
||||||
define('__unlink__' + relationName, {
|
define('__unlink__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + relationName + '/rel/:fk'},
|
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: {arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
|
@ -413,22 +439,38 @@ Model.hasManyRemoting = function (relationName, relation, define) {
|
||||||
var existsFunc = this.prototype['__exists__' + relationName];
|
var existsFunc = this.prototype['__exists__' + relationName];
|
||||||
define('__exists__' + relationName, {
|
define('__exists__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'head', path: '/' + relationName + '/rel/:fk'},
|
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: {arg: 'fk', type: 'any',
|
||||||
description: 'Foreign key for ' + relationName, required: true,
|
description: 'Foreign key for ' + relationName, required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
description: 'Check the existence of ' + relationName + ' relation to an item by id',
|
description: 'Check the existence of ' + relationName + ' relation to an item by id',
|
||||||
returns: {}
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}, existsFunc);
|
}, existsFunc);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Model.scopeRemoting = function(relationName, relation, define) {
|
Model.scopeRemoting = function(relationName, relation, define) {
|
||||||
|
var pathName = (relation.options.http && relation.options.http.path) || relationName;
|
||||||
var toModelName = relation.modelTo.modelName;
|
var toModelName = relation.modelTo.modelName;
|
||||||
|
|
||||||
define('__get__' + relationName, {
|
define('__get__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + relationName},
|
http: {verb: 'get', path: '/' + pathName},
|
||||||
accepts: {arg: 'filter', type: 'object'},
|
accepts: {arg: 'filter', type: 'object'},
|
||||||
description: 'Queries ' + relationName + ' of ' + this.modelName + '.',
|
description: 'Queries ' + relationName + ' of ' + this.modelName + '.',
|
||||||
returns: {arg: relationName, type: [toModelName], root: true}
|
returns: {arg: relationName, type: [toModelName], root: true}
|
||||||
|
@ -436,7 +478,7 @@ Model.scopeRemoting = function(relationName, relation, define) {
|
||||||
|
|
||||||
define('__create__' + relationName, {
|
define('__create__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'post', path: '/' + relationName},
|
http: {verb: 'post', path: '/' + pathName},
|
||||||
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
|
accepts: {arg: 'data', type: toModelName, http: {source: 'body'}},
|
||||||
description: 'Creates a new instance in ' + relationName + ' of this model.',
|
description: 'Creates a new instance in ' + relationName + ' of this model.',
|
||||||
returns: {arg: 'data', type: toModelName, root: true}
|
returns: {arg: 'data', type: toModelName, root: true}
|
||||||
|
@ -444,10 +486,154 @@ Model.scopeRemoting = function(relationName, relation, define) {
|
||||||
|
|
||||||
define('__delete__' + relationName, {
|
define('__delete__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + relationName},
|
http: {verb: 'delete', path: '/' + pathName},
|
||||||
description: 'Deletes all ' + relationName + ' of this model.'
|
description: 'Deletes all ' + relationName + ' of this model.'
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Model.nestRemoting = function(relationName, options, cb) {
|
||||||
|
if (typeof options === 'function' && !cb) {
|
||||||
|
cb = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var regExp = /^__([^_]+)__([^_]+)$/;
|
||||||
|
var relation = this.relations[relationName];
|
||||||
|
if (relation && relation.modelTo && relation.modelTo.sharedClass) {
|
||||||
|
var self = this;
|
||||||
|
var sharedClass = this.sharedClass;
|
||||||
|
var sharedToClass = relation.modelTo.sharedClass;
|
||||||
|
var toModelName = relation.modelTo.modelName;
|
||||||
|
|
||||||
|
var pathName = options.pathName || relation.options.path || relationName;
|
||||||
|
var paramName = options.paramName || 'nk';
|
||||||
|
|
||||||
|
var http = [].concat(sharedToClass.http || [])[0];
|
||||||
|
|
||||||
|
if (relation.multiple) {
|
||||||
|
var httpPath = pathName + '/:' + paramName;
|
||||||
|
var acceptArgs = [
|
||||||
|
{
|
||||||
|
arg: paramName, type: 'any', http: { source: 'path' },
|
||||||
|
description: 'Foreign key for ' + relation.name,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
var httpPath = pathName;
|
||||||
|
var acceptArgs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A method should return the method name to use, if it is to be
|
||||||
|
// included as a nested method - a falsy return value will skip.
|
||||||
|
var filter = cb || options.filterMethod || function(method, relation) {
|
||||||
|
var matches = method.name.match(regExp);
|
||||||
|
if (matches) {
|
||||||
|
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sharedToClass.methods().forEach(function(method) {
|
||||||
|
var methodName;
|
||||||
|
if (!method.isStatic && (methodName = filter(method, relation))) {
|
||||||
|
var prefix = relation.multiple ? '__findById__' : '__get__';
|
||||||
|
var getterName = options.getterName || (prefix + relationName);
|
||||||
|
|
||||||
|
var getterFn = relation.modelFrom.prototype[getterName];
|
||||||
|
if (typeof getterFn !== 'function') {
|
||||||
|
throw new Error('Invalid remote method: `' + getterName + '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedFn = relation.modelTo.prototype[method.name];
|
||||||
|
if (typeof nestedFn !== 'function') {
|
||||||
|
throw new Error('Invalid remote method: `' + method.name + '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {};
|
||||||
|
|
||||||
|
opts.accepts = acceptArgs.concat(method.accepts || []);
|
||||||
|
opts.returns = [].concat(method.returns || []);
|
||||||
|
opts.description = method.description;
|
||||||
|
opts.rest = extend({}, method.rest || {});
|
||||||
|
opts.rest.delegateTo = method.name;
|
||||||
|
|
||||||
|
opts.http = [];
|
||||||
|
var routes = [].concat(method.http || []);
|
||||||
|
routes.forEach(function(route) {
|
||||||
|
if (route.path) {
|
||||||
|
var copy = extend({}, route);
|
||||||
|
copy.path = httpPath + route.path;
|
||||||
|
opts.http.push(copy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relation.multiple) {
|
||||||
|
sharedClass.defineMethod(methodName, opts, function(fkId) {
|
||||||
|
var args = Array.prototype.slice.call(arguments, 1);
|
||||||
|
var last = args[args.length - 1];
|
||||||
|
var cb = typeof last === 'function' ? last : null;
|
||||||
|
this[getterName](fkId, function(err, inst) {
|
||||||
|
if (err && cb) return cb(err);
|
||||||
|
if (inst instanceof relation.modelTo) {
|
||||||
|
nestedFn.apply(inst, args);
|
||||||
|
} else if (cb) {
|
||||||
|
cb(err, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, method.isStatic);
|
||||||
|
} else {
|
||||||
|
sharedClass.defineMethod(methodName, opts, function() {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
var last = args[args.length - 1];
|
||||||
|
var cb = typeof last === 'function' ? last : null;
|
||||||
|
this[getterName](function(err, inst) {
|
||||||
|
if (err && cb) return cb(err);
|
||||||
|
if (inst instanceof relation.modelTo) {
|
||||||
|
nestedFn.apply(inst, args);
|
||||||
|
} else if (cb) {
|
||||||
|
cb(err, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, method.isStatic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.hooks === false) return; // don't inherit before/after hooks
|
||||||
|
|
||||||
|
self.once('mounted', function(app, sc, remotes) {
|
||||||
|
var listenerTree = extend({}, remotes.listenerTree || {});
|
||||||
|
listenerTree.before = listenerTree.before || {};
|
||||||
|
listenerTree.after = listenerTree.after || {};
|
||||||
|
|
||||||
|
var beforeListeners = remotes.listenerTree.before[toModelName] || {};
|
||||||
|
var afterListeners = remotes.listenerTree.after[toModelName] || {};
|
||||||
|
|
||||||
|
sharedClass.methods().forEach(function(method) {
|
||||||
|
var delegateTo = method.rest && method.rest.delegateTo;
|
||||||
|
if (delegateTo) {
|
||||||
|
var before = method.isStatic ? beforeListeners : beforeListeners['prototype'];
|
||||||
|
var after = method.isStatic ? afterListeners : afterListeners['prototype'];
|
||||||
|
var m = method.isStatic ? method.name : 'prototype.' + method.name;
|
||||||
|
if (before[delegateTo]) {
|
||||||
|
self.beforeRemote(m, function(ctx, result, next) {
|
||||||
|
before[delegateTo]._listeners.call(null, ctx, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (after[delegateTo]) {
|
||||||
|
self.afterRemote(m, function(ctx, result, next) {
|
||||||
|
after[delegateTo]._listeners.call(null, ctx, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error('Relation `' + relationName + '` does not exist for model `' + this.modelName + '`');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// setup the initial model
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
|
@ -128,7 +128,7 @@ PersistedModel.findOrCreate._delegate = true;
|
||||||
* Check whether a model instance exists in database
|
* Check whether a model instance exists in database
|
||||||
*
|
*
|
||||||
* @param {id} id - identifier of object (primary key value)
|
* @param {id} id - identifier of object (primary key value)
|
||||||
* @param {Function} cb - callbacl called with (err, exists: Bool)
|
* @param {Function} cb - callback called with (err, exists: Bool)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
PersistedModel.exists = function exists(id, cb) {
|
PersistedModel.exists = function exists(id, cb) {
|
||||||
|
@ -448,7 +448,25 @@ PersistedModel.setupRemoting = function() {
|
||||||
description: 'Check whether a model instance exists in the data source',
|
description: 'Check whether a model instance exists in the data source',
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
||||||
returns: {arg: 'exists', type: 'boolean'},
|
returns: {arg: 'exists', type: 'boolean'},
|
||||||
http: {verb: 'get', path: '/:id/exists'}
|
http: [
|
||||||
|
{verb: 'get', path: '/:id/exists'},
|
||||||
|
{verb: 'head', path: '/:id'}
|
||||||
|
],
|
||||||
|
rest: {
|
||||||
|
// After hook to map exists to 200/404 for HEAD
|
||||||
|
after: function(ctx, cb) {
|
||||||
|
if(!ctx.result.exists) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setRemoting(PersistedModel, 'findById', {
|
setRemoting(PersistedModel, 'findById', {
|
||||||
|
|
|
@ -277,6 +277,7 @@ User.prototype.hasPassword = function (plain, fn) {
|
||||||
|
|
||||||
User.prototype.verify = function (options, fn) {
|
User.prototype.verify = function (options, fn) {
|
||||||
var user = this;
|
var user = this;
|
||||||
|
var userModel = this.constructor;
|
||||||
assert(typeof options === 'object', 'options required when calling user.verify()');
|
assert(typeof options === 'object', 'options required when calling user.verify()');
|
||||||
assert(options.type, 'You must supply a verification type (options.type)');
|
assert(options.type, 'You must supply a verification type (options.type)');
|
||||||
assert(options.type === 'email', 'Unsupported verification type');
|
assert(options.type === 'email', 'Unsupported verification type');
|
||||||
|
@ -287,13 +288,20 @@ User.prototype.verify = function (options, fn) {
|
||||||
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs'));
|
||||||
options.user = this;
|
options.user = this;
|
||||||
options.protocol = options.protocol || 'http';
|
options.protocol = options.protocol || 'http';
|
||||||
options.host = options.host || 'localhost';
|
|
||||||
|
var app = this.app;
|
||||||
|
options.host = options.host || (app && app.get('host')) || 'localhost';
|
||||||
|
options.port = options.port || (app && app.get('port')) || 3000;
|
||||||
|
options.restApiRoot = options.restApiRoot || (app && app.get('restApiRoot')) || '/api';
|
||||||
options.verifyHref = options.verifyHref ||
|
options.verifyHref = options.verifyHref ||
|
||||||
options.protocol
|
options.protocol
|
||||||
+ '://'
|
+ '://'
|
||||||
+ options.host
|
+ options.host
|
||||||
+ User.http.path
|
+ ':'
|
||||||
+ User.confirm.http.path
|
+ options.port
|
||||||
|
+ options.restApiRoot
|
||||||
|
+ userModel.http.path
|
||||||
|
+ userModel.confirm.http.path
|
||||||
+ '?uid='
|
+ '?uid='
|
||||||
+ options.user.id
|
+ options.user.id
|
||||||
+ '&redirect='
|
+ '&redirect='
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"mobile",
|
"mobile",
|
||||||
"mBaaS"
|
"mBaaS"
|
||||||
],
|
],
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "grunt mocha-and-karma"
|
"test": "grunt mocha-and-karma"
|
||||||
},
|
},
|
||||||
|
|
|
@ -213,6 +213,45 @@ describe('relations - integration', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('HEAD /physicians/:id/patients/rel/:fk', function () {
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
var self = this;
|
||||||
|
setup(true, function (err, root) {
|
||||||
|
self.url = root.relUrl;
|
||||||
|
self.patient = root.patient;
|
||||||
|
self.physician = root.physician;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lt.describe.whenCalledRemotely('HEAD', '/api/physicians/:id/patients/rel/:fk', function () {
|
||||||
|
it('should succeed with statusCode 200', function () {
|
||||||
|
assert.equal(this.res.statusCode, 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HEAD /physicians/:id/patients/rel/:fk that does not exist', function () {
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
var self = this;
|
||||||
|
setup(true, function (err, root) {
|
||||||
|
self.url = '/api/physicians/' + root.physician.id
|
||||||
|
+ '/patients/rel/' + '999';
|
||||||
|
self.patient = root.patient;
|
||||||
|
self.physician = root.physician;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lt.describe.whenCalledRemotely('HEAD', '/api/physicians/:id/patients/rel/:fk', function () {
|
||||||
|
it('should succeed with statusCode 404', function () {
|
||||||
|
assert.equal(this.res.statusCode, 404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('DELETE /physicians/:id/patients/rel/:fk', function () {
|
describe('DELETE /physicians/:id/patients/rel/:fk', function () {
|
||||||
|
|
||||||
before(function (done) {
|
before(function (done) {
|
||||||
|
@ -443,4 +482,623 @@ describe('relations - integration', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('embedsMany', function() {
|
||||||
|
|
||||||
|
before(function defineProductAndCategoryModels() {
|
||||||
|
var todoList = app.model(
|
||||||
|
'todoList',
|
||||||
|
{ properties: { name: 'string' },
|
||||||
|
dataSource: 'db',
|
||||||
|
plural: 'todo-lists'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var todoItem = app.model(
|
||||||
|
'todoItem',
|
||||||
|
{ properties: { content: 'string' }, dataSource: 'db' }
|
||||||
|
);
|
||||||
|
todoList.embedsMany(todoItem, { as: 'items' });
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function createTodoList(done) {
|
||||||
|
var test = this;
|
||||||
|
app.models.todoList.create({ name: 'List A' },
|
||||||
|
function(err, list) {
|
||||||
|
if (err) return done(err);
|
||||||
|
test.todoList = list;
|
||||||
|
list.items.build({ content: 'Todo 1' });
|
||||||
|
list.items.build({ content: 'Todo 2' });
|
||||||
|
list.save(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function(done) {
|
||||||
|
this.app.models.todoList.destroyAll(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the embedded models', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body.name).to.be.equal('List A');
|
||||||
|
expect(res.body.todoItems).to.be.eql([
|
||||||
|
{ content: 'Todo 1', id: 1 },
|
||||||
|
{ content: 'Todo 2', id: 2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the embedded models', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ content: 'Todo 1', id: 1 },
|
||||||
|
{ content: 'Todo 2', id: 2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters the embedded models', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items';
|
||||||
|
url += '?filter[where][id]=2';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ content: 'Todo 2', id: 2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates embedded models', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items';
|
||||||
|
|
||||||
|
var expected = { content: 'Todo 3', id: 3 };
|
||||||
|
|
||||||
|
this.post(url)
|
||||||
|
.send({ content: 'Todo 3' })
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the embedded models', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ content: 'Todo 1', id: 1 },
|
||||||
|
{ content: 'Todo 2', id: 2 },
|
||||||
|
{ content: 'Todo 3', id: 3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an embedded model by (internal) id', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items/3';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql(
|
||||||
|
{ content: 'Todo 3', id: 3 }
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes an embedded model', function(done) {
|
||||||
|
var expectedProduct = this.product;
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items/2';
|
||||||
|
|
||||||
|
this.del(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the embedded models - verify', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ content: 'Todo 1', id: 1 },
|
||||||
|
{ content: 'Todo 3', id: 3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO - this.head is undefined
|
||||||
|
|
||||||
|
it.skip('checks if an embedded model exists - ok', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items/3';
|
||||||
|
|
||||||
|
this.head(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('checks if an embedded model exists - fail', function(done) {
|
||||||
|
var url = '/api/todo-lists/' + this.todoList.id + '/items/2';
|
||||||
|
|
||||||
|
this.head(url)
|
||||||
|
.expect(404, function(err, res) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('referencesMany', function() {
|
||||||
|
|
||||||
|
before(function defineProductAndCategoryModels() {
|
||||||
|
var recipe = app.model(
|
||||||
|
'recipe',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db' }
|
||||||
|
);
|
||||||
|
var ingredient = app.model(
|
||||||
|
'ingredient',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db' }
|
||||||
|
);
|
||||||
|
var photo = app.model(
|
||||||
|
'photo',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db' }
|
||||||
|
);
|
||||||
|
recipe.referencesMany(ingredient);
|
||||||
|
// contrived example for test:
|
||||||
|
recipe.hasOne(photo, { as: 'picture', options: {
|
||||||
|
http: { path: 'image' }
|
||||||
|
} });
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function createRecipe(done) {
|
||||||
|
var test = this;
|
||||||
|
app.models.recipe.create({ name: 'Recipe' },
|
||||||
|
function(err, recipe) {
|
||||||
|
if (err) return done(err);
|
||||||
|
test.recipe = recipe;
|
||||||
|
recipe.ingredients.create({
|
||||||
|
name: 'Chocolate' },
|
||||||
|
function(err, ing) {
|
||||||
|
test.ingredient1 = ing.id;
|
||||||
|
recipe.picture.create({ name: 'Photo 1' }, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function createIngredient(done) {
|
||||||
|
var test = this;
|
||||||
|
app.models.ingredient.create({ name: 'Sugar' }, function(err, ing) {
|
||||||
|
test.ingredient2 = ing.id;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function(done) {
|
||||||
|
var app = this.app;
|
||||||
|
app.models.recipe.destroyAll(function() {
|
||||||
|
app.models.ingredient.destroyAll(function() {
|
||||||
|
app.models.photo.destroyAll(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps an array of ids', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body.ingredientIds).to.eql([test.ingredient1]);
|
||||||
|
expect(res.body).to.not.have.property('ingredients');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates referenced models', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.post(url)
|
||||||
|
.send({ name: 'Butter' })
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body.name).to.be.eql('Butter');
|
||||||
|
test.ingredient3 = res.body.id;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has created models', function(done) {
|
||||||
|
var url = '/api/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 },
|
||||||
|
{ name: 'Butter', id: test.ingredient3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the referenced models', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Butter', id: test.ingredient3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters the referenced models', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
url += '?filter[where][name]=Butter';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Butter', id: test.ingredient3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the referenced models', function(done) {
|
||||||
|
var url = '/api/recipes/findOne?filter[where][id]=' + this.recipe.id;
|
||||||
|
url += '&filter[include]=ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body.ingredientIds).to.eql([
|
||||||
|
test.ingredient1, test.ingredient3
|
||||||
|
]);
|
||||||
|
expect(res.body.ingredients).to.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Butter', id: test.ingredient3 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a referenced model by id', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients/';
|
||||||
|
url += this.ingredient3;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql(
|
||||||
|
{ name: 'Butter', id: test.ingredient3 }
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps an array of ids - verify', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
var expected = [test.ingredient1, test.ingredient3];
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body.ingredientIds).to.eql(expected);
|
||||||
|
expect(res.body).to.not.have.property('ingredients');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('destroys a referenced model', function(done) {
|
||||||
|
var expectedProduct = this.product;
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients/';
|
||||||
|
url += this.ingredient3;
|
||||||
|
|
||||||
|
this.del(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has destroyed a referenced model', function(done) {
|
||||||
|
var url = '/api/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the referenced models - verify', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates/links a reference by id', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
url += '/rel/' + this.ingredient2;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.put(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql(
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 }
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the referenced models - verify', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes/unlinks a reference by id', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
url += '/rel/' + this.ingredient1;
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.del(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the referenced models - verify', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has not destroyed an unlinked model', function(done) {
|
||||||
|
var url = '/api/ingredients';
|
||||||
|
var test = this;
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.eql([
|
||||||
|
{ name: 'Chocolate', id: test.ingredient1 },
|
||||||
|
{ name: 'Sugar', id: test.ingredient2 }
|
||||||
|
]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses a custom relation path', function(done) {
|
||||||
|
var url = '/api/recipes/' + this.recipe.id + '/image';
|
||||||
|
|
||||||
|
this.get(url)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(res.body.name).to.equal('Photo 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO - this.head is undefined
|
||||||
|
|
||||||
|
// it.skip('checks if a referenced model exists - ok', function(done) {
|
||||||
|
// var url = '/api/recipes/' + this.recipe.id + '/ingredients/';
|
||||||
|
// url += this.ingredient1;
|
||||||
|
//
|
||||||
|
// this.head(url)
|
||||||
|
// .expect(200, function(err, res) {
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it.skip('checks if an referenced model exists - fail', function(done) {
|
||||||
|
// var url = '/api/recipes/' + this.recipe.id + '/ingredients/';
|
||||||
|
// url += this.ingredient3;
|
||||||
|
//
|
||||||
|
// this.head(url)
|
||||||
|
// .expect(404, function(err, res) {
|
||||||
|
// done();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nested relations', function() {
|
||||||
|
|
||||||
|
before(function defineProductAndCategoryModels() {
|
||||||
|
var Book = app.model(
|
||||||
|
'Book',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db',
|
||||||
|
plural: 'books' }
|
||||||
|
);
|
||||||
|
var Page = app.model(
|
||||||
|
'Page',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db',
|
||||||
|
plural: 'pages' }
|
||||||
|
);
|
||||||
|
var Image = app.model(
|
||||||
|
'Image',
|
||||||
|
{ properties: { name: 'string' }, dataSource: 'db',
|
||||||
|
plural: 'images' }
|
||||||
|
);
|
||||||
|
var Note = app.model(
|
||||||
|
'Note',
|
||||||
|
{ properties: { text: 'string' }, dataSource: 'db',
|
||||||
|
plural: 'notes' }
|
||||||
|
);
|
||||||
|
Book.hasMany(Page);
|
||||||
|
Page.hasMany(Note);
|
||||||
|
Image.belongsTo(Book);
|
||||||
|
|
||||||
|
Book.nestRemoting('pages');
|
||||||
|
Image.nestRemoting('book');
|
||||||
|
|
||||||
|
expect(Book.prototype['__findById__pages__notes']).to.be.a.function;
|
||||||
|
expect(Image.prototype['__findById__book__pages']).to.be.a.function;
|
||||||
|
|
||||||
|
Page.beforeRemote('prototype.__findById__notes', function(ctx, result, next) {
|
||||||
|
ctx.res.set('x-before', 'before');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
Page.afterRemote('prototype.__findById__notes', function(ctx, result, next) {
|
||||||
|
ctx.res.set('x-after', 'after');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function createBook(done) {
|
||||||
|
var test = this;
|
||||||
|
app.models.Book.create({ name: 'Book 1' },
|
||||||
|
function(err, book) {
|
||||||
|
if (err) return done(err);
|
||||||
|
test.book = book;
|
||||||
|
book.pages.create({ name: 'Page 1' },
|
||||||
|
function(err, page) {
|
||||||
|
if (err) return done(err);
|
||||||
|
test.page = page;
|
||||||
|
page.notes.create({ text: 'Page Note 1' },
|
||||||
|
function(err, note) {
|
||||||
|
test.note = note;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
before(function createCover(done) {
|
||||||
|
var test = this;
|
||||||
|
app.models.Image.create({ name: 'Cover 1', book: test.book },
|
||||||
|
function(err, image) {
|
||||||
|
if (err) return done(err);
|
||||||
|
test.image = image;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has regular relationship routes - pages', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/books/' + test.book.id + '/pages')
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.an.array;
|
||||||
|
expect(res.body).to.have.length(1);
|
||||||
|
expect(res.body[0].name).to.equal('Page 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has regular relationship routes - notes', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.headers['x-before']).to.equal('before');
|
||||||
|
expect(res.headers['x-after']).to.equal('after');
|
||||||
|
expect(res.body).to.be.an.object;
|
||||||
|
expect(res.body.text).to.equal('Page Note 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a basic error handler', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/books/unknown/pages/' + test.page.id + '/notes')
|
||||||
|
.expect(404, function(err, res) {
|
||||||
|
expect(res.body.error).to.be.an.object;
|
||||||
|
var expected = 'could not find a model with id unknown';
|
||||||
|
expect(res.body.error.message).to.equal(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables nested relationship routes - belongsTo find', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/images/' + test.image.id + '/book/pages')
|
||||||
|
.end(function(err, res) {
|
||||||
|
expect(res.body).to.be.an.array;
|
||||||
|
expect(res.body).to.have.length(1);
|
||||||
|
expect(res.body[0].name).to.equal('Page 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables nested relationship routes - belongsTo findById', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/images/' + test.image.id + '/book/pages/' + test.page.id)
|
||||||
|
.end(function(err, res) {
|
||||||
|
expect(res.body).to.be.an.object;
|
||||||
|
expect(res.body.name).to.equal('Page 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables nested relationship routes - hasMany find', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes')
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.body).to.be.an.array;
|
||||||
|
expect(res.body).to.have.length(1);
|
||||||
|
expect(res.body[0].text).to.equal('Page Note 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables nested relationship routes - hasMany findById', function(done) {
|
||||||
|
var test = this;
|
||||||
|
this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes/' + test.note.id)
|
||||||
|
.expect(200, function(err, res) {
|
||||||
|
expect(res.headers['x-before']).to.equal('before');
|
||||||
|
expect(res.headers['x-after']).to.equal('after');
|
||||||
|
expect(res.body).to.be.an.object;
|
||||||
|
expect(res.body.text).to.equal('Page Note 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,54 @@
|
||||||
describe('loopback.rest', function() {
|
describe('loopback.rest', function() {
|
||||||
|
var MyModel;
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
app.dataSource('db', { connector: loopback.Memory });
|
var ds = app.dataSource('db', { connector: loopback.Memory });
|
||||||
|
MyModel = ds.createModel('MyModel', {name: String});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('works out-of-the-box', function(done) {
|
it('works out-of-the-box', function(done) {
|
||||||
app.model('MyModel', { dataSource: 'db' });
|
app.model(MyModel);
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
request(app).get('/mymodels')
|
request(app).get('/mymodels')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should report 404 for GET /:id not found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
request(app).get('/mymodels/1')
|
||||||
|
.expect(404)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report 404 for HEAD /:id not found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
request(app).head('/mymodels/1')
|
||||||
|
.expect(404)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report 200 for GET /:id found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
MyModel.create({name: 'm1'}, function(err, inst) {
|
||||||
|
request(app).get('/mymodels/' + inst.id)
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report 200 for HEAD /:id found', function(done) {
|
||||||
|
app.model(MyModel);
|
||||||
|
app.use(loopback.rest());
|
||||||
|
MyModel.create({name: 'm2'}, function(err, inst) {
|
||||||
|
request(app).head('/mymodels/' + inst.id)
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('includes loopback.token when necessary', function(done) {
|
it('includes loopback.token when necessary', function(done) {
|
||||||
givenUserModelWithAuth();
|
givenUserModelWithAuth();
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
|
|
|
@ -454,7 +454,9 @@ describe('User', function(){
|
||||||
assert(result.email);
|
assert(result.email);
|
||||||
assert(result.email.response);
|
assert(result.email.response);
|
||||||
assert(result.token);
|
assert(result.token);
|
||||||
assert(~result.email.response.toString('utf-8').indexOf('To: bar@bat.com'));
|
var msg = result.email.response.toString('utf-8');
|
||||||
|
assert(~msg.indexOf('/api/users/confirm'));
|
||||||
|
assert(~msg.indexOf('To: bar@bat.com'));
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue