Merge pull request #3023 from strongloop/feature/options-from-context-v2
Inject remoting context to options arg
This commit is contained in:
commit
a21fca6089
230
lib/model.js
230
lib/model.js
|
@ -9,6 +9,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
var g = require('./globalize');
|
var g = require('./globalize');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var debug = require('debug')('loopback:model');
|
||||||
var RemoteObjects = require('strong-remoting');
|
var RemoteObjects = require('strong-remoting');
|
||||||
var SharedClass = require('strong-remoting').SharedClass;
|
var SharedClass = require('strong-remoting').SharedClass;
|
||||||
var extend = require('util')._extend;
|
var extend = require('util')._extend;
|
||||||
|
@ -136,15 +137,29 @@ module.exports = function(registry) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// support remoting prototype methods
|
// support remoting prototype methods
|
||||||
ModelCtor.sharedCtor = function(data, id, fn) {
|
ModelCtor.sharedCtor = function(data, id, options, fn) {
|
||||||
var ModelCtor = this;
|
var ModelCtor = this;
|
||||||
|
|
||||||
if (typeof data === 'function') {
|
var isRemoteInvocationWithOptions = typeof data !== 'object' &&
|
||||||
|
typeof id === 'object' &&
|
||||||
|
typeof options === 'function';
|
||||||
|
if (isRemoteInvocationWithOptions) {
|
||||||
|
// sharedCtor(id, options, fn)
|
||||||
|
fn = options;
|
||||||
|
options = id;
|
||||||
|
id = data;
|
||||||
|
data = null;
|
||||||
|
} else if (typeof data === 'function') {
|
||||||
|
// sharedCtor(fn)
|
||||||
fn = data;
|
fn = data;
|
||||||
data = null;
|
data = null;
|
||||||
id = null;
|
id = null;
|
||||||
|
options = null;
|
||||||
} else if (typeof id === 'function') {
|
} else if (typeof id === 'function') {
|
||||||
|
// sharedCtor(data, fn)
|
||||||
|
// sharedCtor(id, fn)
|
||||||
fn = id;
|
fn = id;
|
||||||
|
options = null;
|
||||||
|
|
||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
id = data;
|
id = data;
|
||||||
|
@ -161,7 +176,8 @@ module.exports = function(registry) {
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
fn(null, new ModelCtor(data));
|
fn(null, new ModelCtor(data));
|
||||||
} else if (id) {
|
} else if (id) {
|
||||||
ModelCtor.findById(id, function(err, model) {
|
var filter = {};
|
||||||
|
ModelCtor.findById(id, filter, options, function(err, model) {
|
||||||
if (err) {
|
if (err) {
|
||||||
fn(err);
|
fn(err);
|
||||||
} else if (model) {
|
} else if (model) {
|
||||||
|
@ -183,6 +199,7 @@ module.exports = function(registry) {
|
||||||
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
|
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
|
||||||
description: idDesc},
|
description: idDesc},
|
||||||
// {arg: 'instance', type: 'object', http: {source: 'body'}}
|
// {arg: 'instance', type: 'object', http: {source: 'body'}}
|
||||||
|
{arg: 'options', type: 'object', http: createOptionsViaModelMethod},
|
||||||
];
|
];
|
||||||
|
|
||||||
ModelCtor.sharedCtor.http = [
|
ModelCtor.sharedCtor.http = [
|
||||||
|
@ -232,6 +249,14 @@ module.exports = function(registry) {
|
||||||
// resolve relation functions
|
// resolve relation functions
|
||||||
sharedClass.resolve(function resolver(define) {
|
sharedClass.resolve(function resolver(define) {
|
||||||
var relations = ModelCtor.relations || {};
|
var relations = ModelCtor.relations || {};
|
||||||
|
var defineRaw = define;
|
||||||
|
define = function(name, options, fn) {
|
||||||
|
if (options.accepts) {
|
||||||
|
options = extend({}, options);
|
||||||
|
options.accepts = setupOptionsArgs(options.accepts);
|
||||||
|
}
|
||||||
|
defineRaw(name, options, fn);
|
||||||
|
};
|
||||||
|
|
||||||
// get the relations
|
// get the relations
|
||||||
for (var relationName in relations) {
|
for (var relationName in relations) {
|
||||||
|
@ -424,9 +449,40 @@ module.exports = function(registry) {
|
||||||
options.isStatic = !m;
|
options.isStatic = !m;
|
||||||
name = options.isStatic ? name : m[1];
|
name = options.isStatic ? name : m[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.accepts) {
|
||||||
|
options = extend({}, options);
|
||||||
|
options.accepts = setupOptionsArgs(options.accepts);
|
||||||
|
}
|
||||||
|
|
||||||
this.sharedClass.defineMethod(name, options);
|
this.sharedClass.defineMethod(name, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function setupOptionsArgs(accepts) {
|
||||||
|
if (!Array.isArray(accepts))
|
||||||
|
accepts = [accepts];
|
||||||
|
|
||||||
|
return accepts.map(function(arg) {
|
||||||
|
if (arg.http && arg.http === 'optionsFromRequest') {
|
||||||
|
// clone to preserve the input value
|
||||||
|
arg = extend({}, arg);
|
||||||
|
arg.http = createOptionsViaModelMethod;
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createOptionsViaModelMethod(ctx) {
|
||||||
|
var EMPTY_OPTIONS = {};
|
||||||
|
var ModelCtor = ctx.method && ctx.method.ctor;
|
||||||
|
if (!ModelCtor)
|
||||||
|
return EMPTY_OPTIONS;
|
||||||
|
if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
|
||||||
|
return EMPTY_OPTIONS;
|
||||||
|
debug('createOptionsFromRemotingContext for %s', ctx.method.stringName);
|
||||||
|
return ModelCtor.createOptionsFromRemotingContext(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable remote invocation for the method with the given name.
|
* Disable remote invocation for the method with the given name.
|
||||||
*
|
*
|
||||||
|
@ -464,7 +520,10 @@ module.exports = function(registry) {
|
||||||
define('__get__' + relationName, {
|
define('__get__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + pathName},
|
http: {verb: 'get', path: '/' + pathName},
|
||||||
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
accepts: [
|
||||||
|
{arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
description: format('Fetches belongsTo relation %s.', relationName),
|
description: format('Fetches belongsTo relation %s.', relationName),
|
||||||
returns: {arg: relationName, type: modelName, root: true},
|
returns: {arg: relationName, type: modelName, root: true},
|
||||||
|
@ -489,7 +548,10 @@ module.exports = function(registry) {
|
||||||
define('__get__' + relationName, {
|
define('__get__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + pathName},
|
http: {verb: 'get', path: '/' + pathName},
|
||||||
accepts: {arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
accepts: [
|
||||||
|
{arg: 'refresh', type: 'boolean', http: {source: 'query'}},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Fetches hasOne relation %s.', relationName),
|
description: format('Fetches hasOne relation %s.', relationName),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
|
returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
|
||||||
|
@ -499,7 +561,13 @@ module.exports = function(registry) {
|
||||||
define('__create__' + relationName, {
|
define('__create__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'post', path: '/' + pathName},
|
http: {verb: 'post', path: '/' + pathName},
|
||||||
accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'data', type: 'object', model: toModelName,
|
||||||
|
http: {source: 'body'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Creates a new instance in %s of this model.', relationName),
|
description: format('Creates a new instance in %s of this model.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: {arg: 'data', type: toModelName, root: true},
|
returns: {arg: 'data', type: toModelName, root: true},
|
||||||
|
@ -508,7 +576,13 @@ module.exports = function(registry) {
|
||||||
define('__update__' + relationName, {
|
define('__update__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'put', path: '/' + pathName},
|
http: {verb: 'put', path: '/' + pathName},
|
||||||
accepts: {arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'data', type: 'object', model: toModelName,
|
||||||
|
http: {source: 'body'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Update %s of this model.', relationName),
|
description: format('Update %s of this model.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: {arg: 'data', type: toModelName, root: true},
|
returns: {arg: 'data', type: toModelName, root: true},
|
||||||
|
@ -517,6 +591,9 @@ module.exports = function(registry) {
|
||||||
define('__destroy__' + relationName, {
|
define('__destroy__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + pathName},
|
http: {verb: 'delete', path: '/' + pathName},
|
||||||
|
accepts: [
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Deletes %s of this model.', relationName),
|
description: format('Deletes %s of this model.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
});
|
});
|
||||||
|
@ -530,10 +607,15 @@ module.exports = function(registry) {
|
||||||
define('__findById__' + relationName, {
|
define('__findById__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'get', path: '/' + pathName + '/:fk'},
|
http: {verb: 'get', path: '/' + pathName + '/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: [
|
||||||
description: format('Foreign key for %s', relationName),
|
{
|
||||||
required: true,
|
arg: 'fk', type: 'any',
|
||||||
http: {source: 'path'}},
|
description: format('Foreign key for %s', relationName),
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Find a related item by id for %s.', relationName),
|
description: format('Find a related item by id for %s.', relationName),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {arg: 'result', type: toModelName, root: true},
|
returns: {arg: 'result', type: toModelName, root: true},
|
||||||
|
@ -544,10 +626,15 @@ module.exports = function(registry) {
|
||||||
define('__destroyById__' + relationName, {
|
define('__destroyById__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
|
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: [
|
||||||
description: format('Foreign key for %s', relationName),
|
{
|
||||||
required: true,
|
arg: 'fk', type: 'any',
|
||||||
http: {source: 'path'}},
|
description: format('Foreign key for %s', relationName),
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Delete a related item by id for %s.', relationName),
|
description: format('Delete a related item by id for %s.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: [],
|
returns: [],
|
||||||
|
@ -563,6 +650,7 @@ module.exports = function(registry) {
|
||||||
required: true,
|
required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
{arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
|
{arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
],
|
],
|
||||||
description: format('Update a related item by id for %s.', relationName),
|
description: format('Update a related item by id for %s.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
|
@ -588,7 +676,10 @@ module.exports = function(registry) {
|
||||||
accepts: [{arg: 'fk', type: 'any',
|
accepts: [{arg: 'fk', type: 'any',
|
||||||
description: format('Foreign key for %s', relationName),
|
description: format('Foreign key for %s', relationName),
|
||||||
required: true,
|
required: true,
|
||||||
http: {source: 'path'}}].concat(accepts),
|
http: {source: 'path'}},
|
||||||
|
].concat(accepts).concat([
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
]),
|
||||||
description: format('Add a related item by id for %s.', relationName),
|
description: format('Add a related item by id for %s.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: {arg: relationName, type: modelThrough.modelName, root: true},
|
returns: {arg: relationName, type: modelThrough.modelName, root: true},
|
||||||
|
@ -598,10 +689,15 @@ module.exports = function(registry) {
|
||||||
define('__unlink__' + relationName, {
|
define('__unlink__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
|
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: [
|
||||||
description: format('Foreign key for %s', relationName),
|
{
|
||||||
required: true,
|
arg: 'fk', type: 'any',
|
||||||
http: {source: 'path'}},
|
description: format('Foreign key for %s', relationName),
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Remove the %s relation to an item by id.', relationName),
|
description: format('Remove the %s relation to an item by id.', relationName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: [],
|
returns: [],
|
||||||
|
@ -613,10 +709,15 @@ module.exports = function(registry) {
|
||||||
define('__exists__' + relationName, {
|
define('__exists__' + relationName, {
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
|
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
|
||||||
accepts: {arg: 'fk', type: 'any',
|
accepts: [
|
||||||
description: format('Foreign key for %s', relationName),
|
{
|
||||||
required: true,
|
arg: 'fk', type: 'any',
|
||||||
http: {source: 'path'}},
|
description: format('Foreign key for %s', relationName),
|
||||||
|
required: true,
|
||||||
|
http: {source: 'path'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Check the existence of %s relation to an item by id.', relationName),
|
description: format('Check the existence of %s relation to an item by id.', relationName),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {arg: 'exists', type: 'boolean', root: true},
|
returns: {arg: 'exists', type: 'boolean', root: true},
|
||||||
|
@ -659,7 +760,10 @@ module.exports = function(registry) {
|
||||||
define('__get__' + scopeName, {
|
define('__get__' + scopeName, {
|
||||||
isStatic: isStatic,
|
isStatic: isStatic,
|
||||||
http: {verb: 'get', path: '/' + pathName},
|
http: {verb: 'get', path: '/' + pathName},
|
||||||
accepts: {arg: 'filter', type: 'object'},
|
accepts: [
|
||||||
|
{arg: 'filter', type: 'object'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Queries %s of %s.', scopeName, this.modelName),
|
description: format('Queries %s of %s.', scopeName, this.modelName),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {arg: scopeName, type: [toModelName], root: true},
|
returns: {arg: scopeName, type: [toModelName], root: true},
|
||||||
|
@ -668,13 +772,16 @@ module.exports = function(registry) {
|
||||||
define('__create__' + scopeName, {
|
define('__create__' + scopeName, {
|
||||||
isStatic: isStatic,
|
isStatic: isStatic,
|
||||||
http: {verb: 'post', path: '/' + pathName},
|
http: {verb: 'post', path: '/' + pathName},
|
||||||
accepts: {
|
accepts: [
|
||||||
arg: 'data',
|
{
|
||||||
type: 'object',
|
arg: 'data',
|
||||||
allowArray: true,
|
type: 'object',
|
||||||
model: toModelName,
|
allowArray: true,
|
||||||
http: {source: 'body'},
|
model: toModelName,
|
||||||
},
|
http: {source: 'body'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Creates a new instance in %s of this model.', scopeName),
|
description: format('Creates a new instance in %s of this model.', scopeName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
returns: {arg: 'data', type: toModelName, root: true},
|
returns: {arg: 'data', type: toModelName, root: true},
|
||||||
|
@ -683,6 +790,16 @@ module.exports = function(registry) {
|
||||||
define('__delete__' + scopeName, {
|
define('__delete__' + scopeName, {
|
||||||
isStatic: isStatic,
|
isStatic: isStatic,
|
||||||
http: {verb: 'delete', path: '/' + pathName},
|
http: {verb: 'delete', path: '/' + pathName},
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'where', type: 'object',
|
||||||
|
// The "where" argument is not exposed in the REST API
|
||||||
|
// but we need to provide a value so that we can pass "options"
|
||||||
|
// as the third argument.
|
||||||
|
http: function(ctx) { return undefined; },
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Deletes all %s of this model.', scopeName),
|
description: format('Deletes all %s of this model.', scopeName),
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
});
|
});
|
||||||
|
@ -690,8 +807,13 @@ module.exports = function(registry) {
|
||||||
define('__count__' + scopeName, {
|
define('__count__' + scopeName, {
|
||||||
isStatic: isStatic,
|
isStatic: isStatic,
|
||||||
http: {verb: 'get', path: '/' + pathName + '/count'},
|
http: {verb: 'get', path: '/' + pathName + '/count'},
|
||||||
accepts: {arg: 'where', type: 'object',
|
accepts: [
|
||||||
description: 'Criteria to match model instances'},
|
{
|
||||||
|
arg: 'where', type: 'object',
|
||||||
|
description: 'Criteria to match model instances',
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
description: format('Counts %s of %s.', scopeName, this.modelName),
|
description: format('Counts %s of %s.', scopeName, this.modelName),
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
returns: {arg: 'count', type: 'number'},
|
returns: {arg: 'count', type: 'number'},
|
||||||
|
@ -873,6 +995,46 @@ module.exports = function(registry) {
|
||||||
|
|
||||||
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
|
Model.ValidationError = require('loopback-datasource-juggler').ValidationError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create "options" value to use when invoking model methods
|
||||||
|
* via strong-remoting (e.g. REST).
|
||||||
|
*
|
||||||
|
* Example
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* MyModel.myMethod = function(options, cb) {
|
||||||
|
* // by default, options contains only one property "accessToken"
|
||||||
|
* var accessToken = options && options.accessToken;
|
||||||
|
* var userId = accessToken && accessToken.userId;
|
||||||
|
* var message = 'Hello ' + (userId ? 'user #' + userId : 'anonymous');
|
||||||
|
* cb(null, message);
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* MyModel.remoteMethod('myMethod', {
|
||||||
|
* accepts: {
|
||||||
|
* arg: 'options',
|
||||||
|
* type: 'object',
|
||||||
|
* // "optionsFromRequest" is a loopback-specific HTTP mapping that
|
||||||
|
* // calls Model's createOptionsFromRemotingContext
|
||||||
|
* // to build the argument value
|
||||||
|
* http: 'optionsFromRequest'
|
||||||
|
* },
|
||||||
|
* returns: {
|
||||||
|
* arg: 'message',
|
||||||
|
* type: 'string'
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} ctx A strong-remoting Context instance
|
||||||
|
* @returns {Object} The value to pass to "options" argument.
|
||||||
|
*/
|
||||||
|
Model.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {
|
||||||
|
accessToken: ctx.req.accessToken,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// setup the initial model
|
// setup the initial model
|
||||||
Model.setup();
|
Model.setup();
|
||||||
|
|
||||||
|
|
|
@ -639,11 +639,14 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'create', {
|
setRemoting(PersistedModel, 'create', {
|
||||||
description: 'Create a new instance of the model and persist it into the data source.',
|
description: 'Create a new instance of the model and persist it into the data source.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {
|
accepts: [
|
||||||
arg: 'data', type: 'object', model: typeName, allowArray: true,
|
{
|
||||||
description: 'Model instance data',
|
arg: 'data', type: 'object', model: typeName, allowArray: true,
|
||||||
http: {source: 'body'},
|
description: 'Model instance data',
|
||||||
},
|
http: {source: 'body'},
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: {verb: 'post', path: '/'},
|
http: {verb: 'post', path: '/'},
|
||||||
});
|
});
|
||||||
|
@ -653,10 +656,13 @@ module.exports = function(registry) {
|
||||||
description: 'Patch an existing model instance or insert a new one ' +
|
description: 'Patch an existing model instance or insert a new one ' +
|
||||||
'into the data source.',
|
'into the data source.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {
|
accepts: [
|
||||||
arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
{
|
||||||
description: 'Model instance data',
|
arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
||||||
},
|
description: 'Model instance data',
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: [{verb: 'patch', path: '/'}],
|
http: [{verb: 'patch', path: '/'}],
|
||||||
};
|
};
|
||||||
|
@ -669,11 +675,14 @@ module.exports = function(registry) {
|
||||||
var replaceOrCreateOptions = {
|
var replaceOrCreateOptions = {
|
||||||
description: 'Replace an existing model instance or insert a new one into the data source.',
|
description: 'Replace an existing model instance or insert a new one into the data source.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {
|
accepts: [
|
||||||
arg: 'data', type: 'object', model: typeName,
|
{
|
||||||
http: {source: 'body'},
|
arg: 'data', type: 'object', model: typeName,
|
||||||
description: 'Model instance data',
|
http: {source: 'body'},
|
||||||
},
|
description: 'Model instance data',
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: [{verb: 'post', path: '/replaceOrCreate'}],
|
http: [{verb: 'post', path: '/replaceOrCreate'}],
|
||||||
};
|
};
|
||||||
|
@ -694,6 +703,7 @@ module.exports = function(registry) {
|
||||||
description: 'Criteria to match model instances'},
|
description: 'Criteria to match model instances'},
|
||||||
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
||||||
description: 'An object of model property name/value pairs'},
|
description: 'An object of model property name/value pairs'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
],
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: {verb: 'post', path: '/upsertWithWhere'},
|
http: {verb: 'post', path: '/upsertWithWhere'},
|
||||||
|
@ -702,7 +712,10 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'exists', {
|
setRemoting(PersistedModel, 'exists', {
|
||||||
description: 'Check whether a model instance exists in the data source.',
|
description: 'Check whether a model instance exists in the data source.',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true},
|
accepts: [
|
||||||
|
{arg: 'id', type: 'any', description: 'Model id', required: true},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'exists', type: 'boolean'},
|
returns: {arg: 'exists', type: 'boolean'},
|
||||||
http: [
|
http: [
|
||||||
{verb: 'get', path: '/:id/exists'},
|
{verb: 'get', path: '/:id/exists'},
|
||||||
|
@ -738,6 +751,7 @@ module.exports = function(registry) {
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
{arg: 'filter', type: 'object',
|
{arg: 'filter', type: 'object',
|
||||||
description: 'Filter defining fields and include'},
|
description: 'Filter defining fields and include'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
],
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: {verb: 'get', path: '/:id'},
|
http: {verb: 'get', path: '/:id'},
|
||||||
|
@ -751,7 +765,8 @@ module.exports = function(registry) {
|
||||||
{arg: 'id', type: 'any', description: 'Model id', required: true,
|
{arg: 'id', type: 'any', description: 'Model id', required: true,
|
||||||
http: {source: 'path'}},
|
http: {source: 'path'}},
|
||||||
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description:
|
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'}, description:
|
||||||
'Model instance data'},
|
'Model instance data'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
],
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: [{verb: 'post', path: '/:id/replace'}],
|
http: [{verb: 'post', path: '/:id/replace'}],
|
||||||
|
@ -766,8 +781,11 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'find', {
|
setRemoting(PersistedModel, 'find', {
|
||||||
description: 'Find all instances of the model matched by filter from the data source.',
|
description: 'Find all instances of the model matched by filter from the data source.',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: {arg: 'filter', type: 'object', description:
|
accepts: [
|
||||||
'Filter defining fields, where, include, order, offset, and limit'},
|
{arg: 'filter', type: 'object', description:
|
||||||
|
'Filter defining fields, where, include, order, offset, and limit'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: [typeName], root: true},
|
returns: {arg: 'data', type: [typeName], root: true},
|
||||||
http: {verb: 'get', path: '/'},
|
http: {verb: 'get', path: '/'},
|
||||||
});
|
});
|
||||||
|
@ -775,8 +793,11 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'findOne', {
|
setRemoting(PersistedModel, 'findOne', {
|
||||||
description: 'Find first instance of the model matched by filter from the data source.',
|
description: 'Find first instance of the model matched by filter from the data source.',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: {arg: 'filter', type: 'object', description:
|
accepts: [
|
||||||
'Filter defining fields, where, include, order, offset, and limit'},
|
{arg: 'filter', type: 'object', description:
|
||||||
|
'Filter defining fields, where, include, order, offset, and limit'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: {verb: 'get', path: '/findOne'},
|
http: {verb: 'get', path: '/findOne'},
|
||||||
rest: {after: convertNullToNotFoundError},
|
rest: {after: convertNullToNotFoundError},
|
||||||
|
@ -785,7 +806,10 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'destroyAll', {
|
setRemoting(PersistedModel, 'destroyAll', {
|
||||||
description: 'Delete all matching records.',
|
description: 'Delete all matching records.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {arg: 'where', type: 'object', description: 'filter.where object'},
|
accepts: [
|
||||||
|
{arg: 'where', type: 'object', description: 'filter.where object'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {
|
returns: {
|
||||||
arg: 'count',
|
arg: 'count',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
@ -805,6 +829,7 @@ module.exports = function(registry) {
|
||||||
description: 'Criteria to match model instances'},
|
description: 'Criteria to match model instances'},
|
||||||
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
{arg: 'data', type: 'object', model: typeName, http: {source: 'body'},
|
||||||
description: 'An object of model property name/value pairs'},
|
description: 'An object of model property name/value pairs'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
],
|
],
|
||||||
returns: {
|
returns: {
|
||||||
arg: 'info',
|
arg: 'info',
|
||||||
|
@ -824,8 +849,11 @@ module.exports = function(registry) {
|
||||||
aliases: ['destroyById', 'removeById'],
|
aliases: ['destroyById', 'removeById'],
|
||||||
description: 'Delete a model instance by {{id}} from the data source.',
|
description: 'Delete a model instance by {{id}} from the data source.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {arg: 'id', type: 'any', description: 'Model id', required: true,
|
accepts: [
|
||||||
http: {source: 'path'}},
|
{arg: 'id', type: 'any', description: 'Model id', required: true,
|
||||||
|
http: {source: 'path'}},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
http: {verb: 'del', path: '/:id'},
|
http: {verb: 'del', path: '/:id'},
|
||||||
returns: {arg: 'count', type: 'object', root: true},
|
returns: {arg: 'count', type: 'object', root: true},
|
||||||
});
|
});
|
||||||
|
@ -833,7 +861,10 @@ module.exports = function(registry) {
|
||||||
setRemoting(PersistedModel, 'count', {
|
setRemoting(PersistedModel, 'count', {
|
||||||
description: 'Count instances of the model matched by where from the data source.',
|
description: 'Count instances of the model matched by where from the data source.',
|
||||||
accessType: 'READ',
|
accessType: 'READ',
|
||||||
accepts: {arg: 'where', type: 'object', description: 'Criteria to match model instances'},
|
accepts: [
|
||||||
|
{arg: 'where', type: 'object', description: 'Criteria to match model instances'},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'count', type: 'number'},
|
returns: {arg: 'count', type: 'number'},
|
||||||
http: {verb: 'get', path: '/count'},
|
http: {verb: 'get', path: '/count'},
|
||||||
});
|
});
|
||||||
|
@ -843,11 +874,14 @@ module.exports = function(registry) {
|
||||||
description: 'Patch attributes for a model instance and persist it into ' +
|
description: 'Patch attributes for a model instance and persist it into ' +
|
||||||
'the data source.',
|
'the data source.',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
accepts: {
|
accepts: [
|
||||||
arg: 'data', type: 'object', model: typeName,
|
{
|
||||||
http: {source: 'body'},
|
arg: 'data', type: 'object', model: typeName,
|
||||||
description: 'An object of model property name/value pairs',
|
http: {source: 'body'},
|
||||||
},
|
description: 'An object of model property name/value pairs',
|
||||||
|
},
|
||||||
|
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
],
|
||||||
returns: {arg: 'data', type: typeName, root: true},
|
returns: {arg: 'data', type: typeName, root: true},
|
||||||
http: [{verb: 'patch', path: '/'}],
|
http: [{verb: 'patch', path: '/'}],
|
||||||
};
|
};
|
||||||
|
|
|
@ -92,7 +92,8 @@
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
"strong-error-handler": "^1.0.1",
|
"strong-error-handler": "^1.0.1",
|
||||||
"strong-task-emitter": "^0.0.6",
|
"strong-task-emitter": "^0.0.6",
|
||||||
"supertest": "^2.0.0"
|
"supertest": "^2.0.0",
|
||||||
|
"supertest-as-promised": "^4.0.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -0,0 +1,411 @@
|
||||||
|
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
|
||||||
|
// Node module: loopback
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var expect = require('chai').expect;
|
||||||
|
var loopback = require('..');
|
||||||
|
var supertest = require('supertest-as-promised')(require('bluebird'));
|
||||||
|
|
||||||
|
describe('OptionsFromRemotingContext', function() {
|
||||||
|
var app, request, accessToken, userId, Product, actualOptions;
|
||||||
|
|
||||||
|
beforeEach(setupAppAndRequest);
|
||||||
|
beforeEach(resetActualOptions);
|
||||||
|
|
||||||
|
context('when making updates via REST', function() {
|
||||||
|
beforeEach(observeOptionsBeforeSave);
|
||||||
|
|
||||||
|
it('injects options to create()', function() {
|
||||||
|
return request.post('/products')
|
||||||
|
.send({name: 'Pen'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to patchOrCreate()', function() {
|
||||||
|
return request.patch('/products')
|
||||||
|
.send({id: 1, name: 'Pen'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to replaceOrCreate()', function() {
|
||||||
|
return request.put('/products')
|
||||||
|
.send({id: 1, name: 'Pen'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to patchOrCreateWithWhere()', function() {
|
||||||
|
return request.post('/products/upsertWithWhere?where[name]=Pen')
|
||||||
|
.send({name: 'Pencil'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to replaceById()', function() {
|
||||||
|
return Product.create({id: 1, name: 'Pen'})
|
||||||
|
.then(function(p) {
|
||||||
|
return request.put('/products/1')
|
||||||
|
.send({name: 'Pencil'})
|
||||||
|
.expect(200);
|
||||||
|
})
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to prototype.patchAttributes()', function() {
|
||||||
|
return Product.create({id: 1, name: 'Pen'})
|
||||||
|
.then(function(p) {
|
||||||
|
return request.patch('/products/1')
|
||||||
|
.send({name: 'Pencil'})
|
||||||
|
.expect(200);
|
||||||
|
})
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to updateAll()', function() {
|
||||||
|
return request.post('/products/update?where[name]=Pen')
|
||||||
|
.send({name: 'Pencil'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when deleting via REST', function() {
|
||||||
|
beforeEach(observeOptionsBeforeDelete);
|
||||||
|
|
||||||
|
it('injects options to deleteById()', function() {
|
||||||
|
return Product.create({id: 1, name: 'Pen'})
|
||||||
|
.then(function(p) {
|
||||||
|
return request.delete('/products/1').expect(200);
|
||||||
|
})
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when querying via REST', function() {
|
||||||
|
beforeEach(observeOptionsOnAccess);
|
||||||
|
beforeEach(givenProductId1);
|
||||||
|
|
||||||
|
it('injects options to find()', function() {
|
||||||
|
return request.get('/products').expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to findById()', function() {
|
||||||
|
return request.get('/products/1').expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to findOne()', function() {
|
||||||
|
return request.get('/products/findOne?where[id]=1').expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to exists()', function() {
|
||||||
|
return request.head('/products/1').expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to count()', function() {
|
||||||
|
return request.get('/products/count').expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('when invoking prototype methods', function() {
|
||||||
|
beforeEach(observeOptionsOnAccess);
|
||||||
|
beforeEach(givenProductId1);
|
||||||
|
|
||||||
|
it('injects options to sharedCtor', function() {
|
||||||
|
Product.prototype.dummy = function(cb) { cb(); };
|
||||||
|
Product.remoteMethod('prototype.dummy', {});
|
||||||
|
return request.post('/products/1/dummy').expect(204)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Catch: because relations methods are defined on "modelFrom",
|
||||||
|
// they will invoke createOptionsFromRemotingContext on "modelFrom" too,
|
||||||
|
// despite the fact that under the hood a method on "modelTo" is called.
|
||||||
|
|
||||||
|
context('hasManyThrough', function() {
|
||||||
|
var Category, ThroughModel;
|
||||||
|
|
||||||
|
beforeEach(givenCategoryHasManyProductsThroughAnotherModel);
|
||||||
|
beforeEach(givenCategoryAndProduct);
|
||||||
|
|
||||||
|
it('injects options to findById', function() {
|
||||||
|
observeOptionsOnAccess(Product);
|
||||||
|
return request.get('/categories/1/products/1').expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to destroyById', function() {
|
||||||
|
observeOptionsBeforeDelete(Product);
|
||||||
|
return request.del('/categories/1/products/1').expect(204)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to updateById', function() {
|
||||||
|
observeOptionsBeforeSave(Product);
|
||||||
|
return request.put('/categories/1/products/1')
|
||||||
|
.send({description: 'a description'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('through-model operations', function() {
|
||||||
|
it('injects options to link', function() {
|
||||||
|
observeOptionsBeforeSave(ThroughModel);
|
||||||
|
return Product.create({id: 2, name: 'Car2'})
|
||||||
|
.then(function() {
|
||||||
|
return request.put('/categories/1/products/rel/2')
|
||||||
|
.send({description: 'a description'})
|
||||||
|
.expect(200);
|
||||||
|
})
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to unlink', function() {
|
||||||
|
observeOptionsBeforeDelete(ThroughModel);
|
||||||
|
return request.del('/categories/1/products/rel/1').expect(204)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to exists', function() {
|
||||||
|
observeOptionsOnAccess(ThroughModel);
|
||||||
|
return request.head('/categories/1/products/rel/1').expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('scope operations', function() {
|
||||||
|
it('injects options to get', function() {
|
||||||
|
observeOptionsOnAccess(Product);
|
||||||
|
return request.get('/categories/1/products').expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to create', function() {
|
||||||
|
observeOptionsBeforeSave(Product);
|
||||||
|
return request.post('/categories/1/products')
|
||||||
|
.send({name: 'Pen'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to delete', function() {
|
||||||
|
observeOptionsBeforeDelete(ThroughModel);
|
||||||
|
return request.del('/categories/1/products').expect(204)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to count', function() {
|
||||||
|
observeOptionsOnAccess(ThroughModel);
|
||||||
|
return request.get('/categories/1/products/count').expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function givenCategoryHasManyProductsThroughAnotherModel() {
|
||||||
|
Category = app.registry.createModel(
|
||||||
|
'Category',
|
||||||
|
{name: String},
|
||||||
|
{forceId: false, replaceOnPUT: true});
|
||||||
|
|
||||||
|
app.model(Category, {dataSource: 'db'});
|
||||||
|
// This is a shortcut for creating CategoryProduct "through" model
|
||||||
|
Category.hasAndBelongsToMany(Product);
|
||||||
|
|
||||||
|
Category.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {injectedFrom: 'Category'};
|
||||||
|
};
|
||||||
|
|
||||||
|
ThroughModel = app.registry.getModel('CategoryProduct');
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenCategoryAndProduct() {
|
||||||
|
return Category.create({id: 1, name: 'First Category'})
|
||||||
|
.then(function(cat) {
|
||||||
|
return cat.products.create({id: 1, name: 'Pen'});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOptionsInjectedFromCategory() {
|
||||||
|
expect(actualOptions).to.have.property('injectedFrom', 'Category');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context('hasOne', function() {
|
||||||
|
var Category;
|
||||||
|
|
||||||
|
beforeEach(givenCategoryHasOneProduct);
|
||||||
|
beforeEach(givenCategoryId1);
|
||||||
|
|
||||||
|
it('injects options to get', function() {
|
||||||
|
observeOptionsOnAccess(Product);
|
||||||
|
return givenProductInCategory1()
|
||||||
|
.then(function() {
|
||||||
|
return request.get('/categories/1/product').expect(200);
|
||||||
|
})
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to create', function() {
|
||||||
|
observeOptionsBeforeSave(Product);
|
||||||
|
return request.post('/categories/1/product')
|
||||||
|
.send({name: 'Pen'})
|
||||||
|
.expect(200)
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to update', function() {
|
||||||
|
return givenProductInCategory1()
|
||||||
|
.then(function() {
|
||||||
|
observeOptionsBeforeSave(Product);
|
||||||
|
return request.put('/categories/1/product')
|
||||||
|
.send({description: 'a description'})
|
||||||
|
.expect(200);
|
||||||
|
})
|
||||||
|
.then(expectInjectedOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects options to destroy', function() {
|
||||||
|
observeOptionsBeforeDelete(Product);
|
||||||
|
return givenProductInCategory1()
|
||||||
|
.then(function() {
|
||||||
|
return request.del('/categories/1/product').expect(204);
|
||||||
|
})
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
function givenCategoryHasOneProduct() {
|
||||||
|
Category = app.registry.createModel(
|
||||||
|
'Category',
|
||||||
|
{name: String},
|
||||||
|
{forceId: false, replaceOnPUT: true});
|
||||||
|
|
||||||
|
app.model(Category, {dataSource: 'db'});
|
||||||
|
Category.hasOne(Product);
|
||||||
|
|
||||||
|
Category.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {injectedFrom: 'Category'};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenCategoryId1() {
|
||||||
|
return Category.create({id: 1, name: 'First Category'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenProductInCategory1() {
|
||||||
|
return Product.create({id: 1, name: 'Pen', categoryId: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOptionsInjectedFromCategory() {
|
||||||
|
expect(actualOptions).to.have.property('injectedFrom', 'Category');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context('belongsTo', function() {
|
||||||
|
var Category;
|
||||||
|
|
||||||
|
beforeEach(givenCategoryBelongsToProduct);
|
||||||
|
|
||||||
|
it('injects options to get', function() {
|
||||||
|
observeOptionsOnAccess(Product);
|
||||||
|
return Product.create({id: 1, name: 'Pen'})
|
||||||
|
.then(function() {
|
||||||
|
return Category.create({id: 1, name: 'a name', productId: 1});
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return request.get('/categories/1/product').expect(200);
|
||||||
|
})
|
||||||
|
.then(expectOptionsInjectedFromCategory);
|
||||||
|
});
|
||||||
|
|
||||||
|
function givenCategoryBelongsToProduct() {
|
||||||
|
Category = app.registry.createModel(
|
||||||
|
'Category',
|
||||||
|
{name: String},
|
||||||
|
{forceId: false, replaceOnPUT: true});
|
||||||
|
|
||||||
|
app.model(Category, {dataSource: 'db'});
|
||||||
|
Category.belongsTo(Product);
|
||||||
|
|
||||||
|
Category.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {injectedFrom: 'Category'};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenCategoryId1() {
|
||||||
|
return Category.create({id: 1, name: 'First Category'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenProductInCategory1() {
|
||||||
|
return Product.create({id: 1, name: 'Pen', categoryId: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectOptionsInjectedFromCategory() {
|
||||||
|
expect(actualOptions).to.have.property('injectedFrom', 'Category');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupAppAndRequest() {
|
||||||
|
app = loopback({localRegistry: true});
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
Product = app.registry.createModel(
|
||||||
|
'Product',
|
||||||
|
{name: String},
|
||||||
|
{forceId: false, replaceOnPUT: true});
|
||||||
|
|
||||||
|
Product.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {injectedFrom: 'Product'};
|
||||||
|
};
|
||||||
|
|
||||||
|
app.model(Product, {dataSource: 'db'});
|
||||||
|
|
||||||
|
app.use(loopback.rest());
|
||||||
|
request = supertest(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetActualOptions() {
|
||||||
|
actualOptions = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeOptionsBeforeSave() {
|
||||||
|
var Model = arguments[0] || Product;
|
||||||
|
Model.observe('before save', function(ctx, next) {
|
||||||
|
actualOptions = ctx.options;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeOptionsBeforeDelete() {
|
||||||
|
var Model = arguments[0] || Product;
|
||||||
|
Model.observe('before delete', function(ctx, next) {
|
||||||
|
actualOptions = ctx.options;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeOptionsOnAccess() {
|
||||||
|
var Model = arguments[0] || Product;
|
||||||
|
Model.observe('access', function(ctx, next) {
|
||||||
|
actualOptions = ctx.options;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenProductId1() {
|
||||||
|
return Product.create({id: 1, name: 'Pen'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectInjectedOptions(name) {
|
||||||
|
expect(actualOptions).to.have.property('injectedFrom');
|
||||||
|
}
|
||||||
|
});
|
|
@ -886,4 +886,108 @@ describe.onServer('Remote Methods', function() {
|
||||||
// fails on time-out when not implemented correctly
|
// fails on time-out when not implemented correctly
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Model.createOptionsFromRemotingContext', function() {
|
||||||
|
var app, TestModel, accessToken, userId, actualOptions;
|
||||||
|
|
||||||
|
before(setupAppAndRequest);
|
||||||
|
before(createUserAndAccessToken);
|
||||||
|
|
||||||
|
it('sets empty options.accessToken for anonymous requests', function(done) {
|
||||||
|
request(app).get('/TestModels/saveOptions')
|
||||||
|
.expect(204, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(actualOptions).to.eql({accessToken: null});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets options.accessToken for authorized requests', function(done) {
|
||||||
|
request(app).get('/TestModels/saveOptions')
|
||||||
|
.set('Authorization', accessToken.id)
|
||||||
|
.expect(204, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(actualOptions).to.have.property('accessToken');
|
||||||
|
expect(actualOptions.accessToken.toObject())
|
||||||
|
.to.eql(accessToken.toObject());
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows "beforeRemote" hooks to contribute options', function(done) {
|
||||||
|
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
|
||||||
|
ctx.args.options.hooked = true;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app).get('/TestModels/saveOptions')
|
||||||
|
.expect(204, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(actualOptions).to.have.property('hooked', true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows apps to add options before remoting hooks', function(done) {
|
||||||
|
TestModel.createOptionsFromRemotingContext = function(ctx) {
|
||||||
|
return {hooks: []};
|
||||||
|
};
|
||||||
|
|
||||||
|
TestModel.beforeRemote('saveOptions', function(ctx, unused, next) {
|
||||||
|
ctx.args.options.hooks.push('beforeRemote');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// In real apps, this code can live in a component or in a boot script
|
||||||
|
app.remotes().phases
|
||||||
|
.addBefore('invoke', 'options-from-request')
|
||||||
|
.use(function(ctx, next) {
|
||||||
|
ctx.args.options.hooks.push('custom');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app).get('/TestModels/saveOptions')
|
||||||
|
.expect(204, function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(actualOptions.hooks).to.eql(['custom', 'beforeRemote']);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupAppAndRequest() {
|
||||||
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
TestModel = app.registry.createModel('TestModel', {base: 'Model'});
|
||||||
|
TestModel.saveOptions = function(options, cb) {
|
||||||
|
actualOptions = options;
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
TestModel.remoteMethod('saveOptions', {
|
||||||
|
accepts: {arg: 'options', type: 'object', http: 'optionsFromRequest'},
|
||||||
|
http: {verb: 'GET', path: '/saveOptions'},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.model(TestModel, {dataSource: null});
|
||||||
|
|
||||||
|
app.enableAuth({dataSource: 'db'});
|
||||||
|
|
||||||
|
app.use(loopback.token());
|
||||||
|
app.use(loopback.rest());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUserAndAccessToken() {
|
||||||
|
var CREDENTIALS = {email: 'context@example.com', password: 'pass'};
|
||||||
|
var User = app.registry.getModel('User');
|
||||||
|
return User.create(CREDENTIALS)
|
||||||
|
.then(function(u) {
|
||||||
|
return User.login(CREDENTIALS);
|
||||||
|
}).then(function(token) {
|
||||||
|
accessToken = token;
|
||||||
|
userId = token.userId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -82,7 +82,7 @@ describe('RemoteConnector', function() {
|
||||||
|
|
||||||
var ServerModel = this.ServerModel;
|
var ServerModel = this.ServerModel;
|
||||||
|
|
||||||
ServerModel.create = function(data, cb) {
|
ServerModel.create = function(data, options, cb) {
|
||||||
calledServerCreate = true;
|
calledServerCreate = true;
|
||||||
data.id = 1;
|
data.id = 1;
|
||||||
cb(null, data);
|
cb(null, data);
|
||||||
|
|
|
@ -280,7 +280,9 @@ function formatMethod(m) {
|
||||||
arr.push([
|
arr.push([
|
||||||
m.name,
|
m.name,
|
||||||
'(',
|
'(',
|
||||||
m.accepts.map(function(a) {
|
m.accepts.filter(function(a) {
|
||||||
|
return !(a.http && typeof a.http === 'function');
|
||||||
|
}).map(function(a) {
|
||||||
return a.arg + ':' + a.type + (a.model ? ':' + a.model : '');
|
return a.arg + ':' + a.type + (a.model ? ':' + a.model : '');
|
||||||
}).join(','),
|
}).join(','),
|
||||||
')',
|
')',
|
||||||
|
|
Loading…
Reference in New Issue