feat: change hasone relation error message

The current hasone error message is not appropriate one.
So adds a better message.
This commit is contained in:
Sujesh T 2019-11-30 16:41:14 +05:30
parent 2db09a3d00
commit 143182fe2b
2 changed files with 1102 additions and 883 deletions

View File

@ -131,7 +131,8 @@ module.exports = function(registry) {
ModelCtor.sharedCtor = function(data, id, options, fn) {
const ModelCtor = this;
const isRemoteInvocationWithOptions = typeof data !== 'object' &&
const isRemoteInvocationWithOptions =
typeof data !== 'object' &&
typeof id === 'object' &&
typeof options === 'function';
if (isRemoteInvocationWithOptions) {
@ -187,15 +188,18 @@ module.exports = function(registry) {
const idDesc = ModelCtor.modelName + ' id';
ModelCtor.sharedCtor.accepts = [
{arg: 'id', type: 'any', required: true, http: {source: 'path'},
description: idDesc},
{
arg: 'id',
type: 'any',
required: true,
http: {source: 'path'},
description: idDesc,
},
// {arg: 'instance', type: 'object', http: {source: 'body'}}
{arg: 'options', type: 'object', http: createOptionsViaModelMethod},
];
ModelCtor.sharedCtor.http = [
{path: '/:id'},
];
ModelCtor.sharedCtor.http = [{path: '/:id'}];
ModelCtor.sharedCtor.returns = {root: true};
@ -203,11 +207,11 @@ module.exports = function(registry) {
extend(remotingOptions, options.remoting || {});
// create a sharedClass
const sharedClass = ModelCtor.sharedClass = new SharedClass(
const sharedClass = (ModelCtor.sharedClass = new SharedClass(
ModelCtor.modelName,
ModelCtor,
remotingOptions,
);
));
// before remote hook
ModelCtor.beforeRemote = function(name, fn) {
@ -248,15 +252,20 @@ module.exports = function(registry) {
};
if ('injectOptionsFromRemoteContext' in options) {
console.warn(g.f(
'%s is using model setting %s which is no longer available.',
typeName, 'injectOptionsFromRemoteContext',
));
console.warn(g.f(
'Please rework your app to use the offical solution for injecting ' +
'"options" argument from request context,\nsee %s',
'http://loopback.io/doc/en/lb3/Using-current-context.html',
));
console.warn(
g.f(
'%s is using model setting %s which is no longer available.',
typeName,
'injectOptionsFromRemoteContext',
),
);
console.warn(
g.f(
'Please rework your app to use the offical solution for injecting ' +
'"options" argument from request context,\nsee %s',
'http://loopback.io/doc/en/lb3/Using-current-context.html',
),
);
}
// resolve relation functions
@ -284,7 +293,8 @@ module.exports = function(registry) {
} else if (
relation.type === 'hasMany' ||
relation.type === 'embedsMany' ||
relation.type === 'referencesMany') {
relation.type === 'referencesMany'
) {
ModelCtor.hasManyRemoting(relationName, relation, define);
}
// Automatically enable nestRemoting if the flag is set to true in the
@ -345,19 +355,22 @@ module.exports = function(registry) {
ctx = {};
}
aclModel.checkAccessForContext({
accessToken: token,
model: this,
property: sharedMethod.name,
method: sharedMethod.name,
sharedMethod: sharedMethod,
modelId: modelId,
accessType: this._getAccessTypeForMethod(sharedMethod),
remotingContext: ctx,
}, function(err, accessRequest) {
if (err) return callback(err);
callback(null, accessRequest.isAllowed());
});
aclModel.checkAccessForContext(
{
accessToken: token,
model: this,
property: sharedMethod.name,
method: sharedMethod.name,
sharedMethod: sharedMethod,
modelId: modelId,
accessType: this._getAccessTypeForMethod(sharedMethod),
remotingContext: ctx,
},
function(err, accessRequest) {
if (err) return callback(err);
callback(null, accessRequest.isAllowed());
},
);
};
/*!
@ -380,12 +393,15 @@ module.exports = function(registry) {
// Check the explicit setting of accessType
if (method.accessType) {
assert(method.accessType === ACL.READ ||
method.accessType === ACL.REPLICATE ||
method.accessType === ACL.WRITE ||
method.accessType === ACL.EXECUTE, 'invalid accessType ' +
method.accessType +
'. It must be "READ", "REPLICATE", "WRITE", or "EXECUTE"');
assert(
method.accessType === ACL.READ ||
method.accessType === ACL.REPLICATE ||
method.accessType === ACL.WRITE ||
method.accessType === ACL.EXECUTE,
'invalid accessType ' +
method.accessType +
'. It must be "READ", "REPLICATE", "WRITE", or "EXECUTE"',
);
return method.accessType;
}
@ -478,8 +494,7 @@ module.exports = function(registry) {
};
function setupOptionsArgs(accepts, modelClass) {
if (!Array.isArray(accepts))
accepts = [accepts];
if (!Array.isArray(accepts)) accepts = [accepts];
return accepts.map(function(arg) {
if (arg.http && arg.http === 'optionsFromRequest') {
@ -500,7 +515,10 @@ module.exports = function(registry) {
*/
const DEFAULT_OPTIONS = {
// Default to `true` so that hidden properties cannot be used in query
prohibitHiddenPropertiesInQuery: ModelCtor._getProhibitHiddenPropertiesInQuery({}, true),
prohibitHiddenPropertiesInQuery: ModelCtor._getProhibitHiddenPropertiesInQuery(
{},
true,
),
// Default to `12` for the max depth of a query object
maxDepthOfQuery: ModelCtor._getMaxDepthOfQuery({}, 12),
// Default to `32` for the max depth of a data object
@ -510,8 +528,10 @@ module.exports = function(registry) {
if (typeof ModelCtor.createOptionsFromRemotingContext !== 'function')
return DEFAULT_OPTIONS;
debug('createOptionsFromRemotingContext for %s', ctx.method.stringName);
return Object.assign(DEFAULT_OPTIONS,
ModelCtor.createOptionsFromRemotingContext(ctx));
return Object.assign(
DEFAULT_OPTIONS,
ModelCtor.createOptionsFromRemotingContext(ctx),
);
}
/**
@ -524,8 +544,10 @@ module.exports = function(registry) {
*/
Model.disableRemoteMethod = function(name, isStatic) {
deprecated('Model.disableRemoteMethod is deprecated. ' +
'Use Model.disableRemoteMethodByName instead.');
deprecated(
'Model.disableRemoteMethod is deprecated. ' +
'Use Model.disableRemoteMethodByName instead.',
);
const key = this.sharedClass.getKeyFromMethodNameAndTarget(name, isStatic);
this.sharedClass.disableMethodByName(key);
this.emit('remoteMethodDisabled', this.sharedClass, key);
@ -547,7 +569,8 @@ module.exports = function(registry) {
let modelName = relation.modelTo && relation.modelTo.modelName;
modelName = modelName || 'PersistedModel';
const fn = this.prototype[relationName];
const pathName = (relation.options.http && relation.options.http.path) || relationName;
const pathName =
(relation.options.http && relation.options.http.path) || relationName;
define('__get__' + relationName, {
isStatic: false,
http: {verb: 'get', path: '/' + pathName},
@ -565,7 +588,9 @@ module.exports = function(registry) {
if (ctx.result !== null) return cb();
const fk = ctx.getArgByName('fk');
const msg = g.f('Unknown "%s" id "%s".', toModelName, fk);
const msg = fk ?
g.f('Unknown "%s" id "%s".', toModelName, fk) :
g.f('No "%s" instance(s) found', toModelName);
const error = new Error(msg);
error.statusCode = error.status = 404;
error.code = 'MODEL_NOT_FOUND';
@ -573,7 +598,8 @@ module.exports = function(registry) {
}
Model.hasOneRemoting = function(relationName, relation, define) {
const pathName = (relation.options.http && relation.options.http.path) || relationName;
const pathName =
(relation.options.http && relation.options.http.path) || relationName;
const toModelName = relation.modelTo.modelName;
define('__get__' + relationName, {
@ -585,7 +611,11 @@ module.exports = function(registry) {
],
description: g.f('Fetches hasOne relation %s.', relationName),
accessType: 'READ',
returns: {arg: relationName, type: relation.modelTo.modelName, root: true},
returns: {
arg: relationName,
type: relation.modelTo.modelName,
root: true,
},
rest: {after: convertNullToNotFoundError.bind(null, toModelName)},
});
@ -594,12 +624,17 @@ module.exports = function(registry) {
http: {verb: 'post', path: '/' + pathName},
accepts: [
{
arg: 'data', type: 'object', model: toModelName,
arg: 'data',
type: 'object',
model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: g.f('Creates a new instance in %s of this model.', relationName),
description: g.f(
'Creates a new instance in %s of this model.',
relationName,
),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true},
});
@ -609,7 +644,9 @@ module.exports = function(registry) {
http: {verb: 'put', path: '/' + pathName},
accepts: [
{
arg: 'data', type: 'object', model: toModelName,
arg: 'data',
type: 'object',
model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
@ -622,16 +659,15 @@ module.exports = function(registry) {
define('__destroy__' + relationName, {
isStatic: false,
http: {verb: 'delete', path: '/' + pathName},
accepts: [
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
accepts: [{arg: 'options', type: 'object', http: 'optionsFromRequest'}],
description: g.f('Deletes %s of this model.', relationName),
accessType: 'WRITE',
});
};
Model.hasManyRemoting = function(relationName, relation, define) {
const pathName = (relation.options.http && relation.options.http.path) || relationName;
const pathName =
(relation.options.http && relation.options.http.path) || relationName;
const toModelName = relation.modelTo.modelName;
const findByIdFunc = this.prototype['__findById__' + relationName];
@ -640,7 +676,8 @@ module.exports = function(registry) {
http: {verb: 'get', path: '/' + pathName + '/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
@ -659,7 +696,8 @@ module.exports = function(registry) {
http: {verb: 'delete', path: '/' + pathName + '/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
@ -676,11 +714,19 @@ module.exports = function(registry) {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/:fk'},
accepts: [
{arg: 'fk', type: 'any',
{
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}},
{arg: 'data', type: 'object', model: toModelName, http: {source: 'body'}},
http: {source: 'path'},
},
{
arg: 'data',
type: 'object',
model: toModelName,
http: {source: 'body'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: g.f('Update a related item by id for %s.', relationName),
@ -695,7 +741,9 @@ module.exports = function(registry) {
if (relation.type === 'hasMany' && relation.modelThrough) {
// Restrict: only hasManyThrough relation can have additional properties
accepts.push({
arg: 'data', type: 'object', model: modelThrough.modelName,
arg: 'data',
type: 'object',
model: modelThrough.modelName,
http: {source: 'body'},
});
}
@ -704,13 +752,19 @@ module.exports = function(registry) {
define('__link__' + relationName, {
isStatic: false,
http: {verb: 'put', path: '/' + pathName + '/rel/:fk'},
accepts: [{arg: 'fk', type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'}},
].concat(accepts).concat([
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
accepts: [
{
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
]
.concat(accepts)
.concat([
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
]),
description: g.f('Add a related item by id for %s.', relationName),
accessType: 'WRITE',
returns: {arg: relationName, type: modelThrough.modelName, root: true},
@ -722,14 +776,18 @@ module.exports = function(registry) {
http: {verb: 'delete', path: '/' + pathName + '/rel/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: g.f('Remove the %s relation to an item by id.', relationName),
description: g.f(
'Remove the %s relation to an item by id.',
relationName,
),
accessType: 'WRITE',
returns: [],
}, removeFunc);
@ -742,14 +800,18 @@ module.exports = function(registry) {
http: {verb: 'head', path: '/' + pathName + '/rel/:fk'},
accepts: [
{
arg: 'fk', type: 'any',
arg: 'fk',
type: 'any',
description: g.f('Foreign key for %s', relationName),
required: true,
http: {source: 'path'},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: g.f('Check the existence of %s relation to an item by id.', relationName),
description: g.f(
'Check the existence of %s relation to an item by id.',
relationName,
),
accessType: 'READ',
returns: {arg: 'exists', type: 'boolean', root: true},
rest: {
@ -774,7 +836,8 @@ module.exports = function(registry) {
Model.scopeRemoting = function(scopeName, scope, define) {
const pathName =
(scope.options && scope.options.http && scope.options.http.path) || scopeName;
(scope.options && scope.options.http && scope.options.http.path) ||
scopeName;
let modelTo = scope.modelTo;
@ -795,7 +858,8 @@ module.exports = function(registry) {
// createOnlyInstance flag in __create__ to indicate loopback-swagger
// code to create a separate model instance for create operation only
const updateOnlyProps = modelTo.getUpdateOnlyProperties ?
modelTo.getUpdateOnlyProperties() : false;
modelTo.getUpdateOnlyProperties() :
false;
const hasUpdateOnlyProps = updateOnlyProps && updateOnlyProps.length > 0;
define('__get__' + scopeName, {
@ -824,7 +888,10 @@ module.exports = function(registry) {
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
description: g.f('Creates a new instance in %s of this model.', scopeName),
description: g.f(
'Creates a new instance in %s of this model.',
scopeName,
),
accessType: 'WRITE',
returns: {arg: 'data', type: toModelName, root: true},
});
@ -834,11 +901,14 @@ module.exports = function(registry) {
http: {verb: 'delete', path: '/' + pathName},
accepts: [
{
arg: 'where', type: 'object',
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; },
http: function(ctx) {
return undefined;
},
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
],
@ -851,7 +921,8 @@ module.exports = function(registry) {
http: {verb: 'get', path: '/' + pathName + '/count'},
accepts: [
{
arg: 'where', type: 'object',
arg: 'where',
type: 'object',
description: 'Criteria to match model instances',
},
{arg: 'options', type: 'object', http: 'optionsFromRequest'},
@ -863,18 +934,18 @@ module.exports = function(registry) {
};
/**
* Enabled deeply-nested queries of related models via REST API.
*
* @param {String} relationName Name of the nested relation.
* @options {Object} [options] It is optional. See below.
* @param {String} pathName The HTTP path (relative to the model) at which your remote method is exposed.
* @param {String} filterMethod The filter name.
* @param {String} paramName The argument name that the remote method accepts.
* @param {String} getterName The getter name.
* @param {Boolean} hooks Whether to inherit before/after hooks.
* @callback {Function} filterCallback The Optional filter function.
* @param {Object} SharedMethod object. See [here](https://apidocs.strongloop.com/strong-remoting/#sharedmethod).
* @param {Object} RelationDefinition object which includes relation `type`, `ModelConstructor` of `modelFrom`, `modelTo`, `keyFrom`, `keyTo` and more relation definitions.
* Enabled deeply-nested queries of related models via REST API.
*
* @param {String} relationName Name of the nested relation.
* @options {Object} [options] It is optional. See below.
* @param {String} pathName The HTTP path (relative to the model) at which your remote method is exposed.
* @param {String} filterMethod The filter name.
* @param {String} paramName The argument name that the remote method accepts.
* @param {String} getterName The getter name.
* @param {Boolean} hooks Whether to inherit before/after hooks.
* @callback {Function} filterCallback The Optional filter function.
* @param {Object} SharedMethod object. See [here](https://apidocs.strongloop.com/strong-remoting/#sharedmethod).
* @param {Object} RelationDefinition object which includes relation `type`, `ModelConstructor` of `modelFrom`, `modelTo`, `keyFrom`, `keyTo` and more relation definitions.
*/
Model.nestRemoting = function(relationName, options, filterCallback) {
@ -895,7 +966,8 @@ module.exports = function(registry) {
const sharedToClass = relation.modelTo.sharedClass;
const toModelName = relation.modelTo.modelName;
const pathName = options.pathName || relation.options.path || relationName;
const pathName =
options.pathName || relation.options.path || relationName;
const paramName = options.paramName || 'nk';
const http = [].concat(sharedToClass.http || [])[0];
@ -905,7 +977,9 @@ module.exports = function(registry) {
httpPath = pathName + '/:' + paramName;
acceptArgs = [
{
arg: paramName, type: 'any', http: {source: 'path'},
arg: paramName,
type: 'any',
http: {source: 'path'},
description: g.f('Foreign key for %s.', relation.name),
required: true,
},
@ -921,18 +995,21 @@ module.exports = function(registry) {
// 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.
const filter = filterCallback || options.filterMethod || function(method, relation) {
const matches = method.name.match(regExp);
if (matches) {
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
}
};
const filter =
filterCallback ||
options.filterMethod ||
function(method, relation) {
const matches = method.name.match(regExp);
if (matches) {
return '__' + matches[1] + '__' + relation.name + '__' + matches[2];
}
};
sharedToClass.methods().forEach(function(method) {
let methodName;
if (!method.isStatic && (methodName = filter(method, relation))) {
const prefix = relation.multiple ? '__findById__' : '__get__';
const getterName = options.getterName || (prefix + relationName);
const getterName = options.getterName || prefix + relationName;
const getterFn = relation.modelFrom.prototype[getterName];
if (typeof getterFn !== 'function') {
@ -966,46 +1043,57 @@ module.exports = function(registry) {
const lastArg = opts.accepts[opts.accepts.length - 1] || {};
const hasOptionsFromContext =
(lastArg.arg || lastArg.name) === 'options' &&
lastArg.type === 'object' && lastArg.http;
lastArg.type === 'object' &&
lastArg.http;
if (relation.multiple) {
sharedClass.defineMethod(methodName, opts, function(fkId) {
const args = Array.prototype.slice.call(arguments, 1);
const cb = args[args.length - 1];
const contextOptions =
hasOptionsFromContext && args[args.length - 2] || {};
this[getterName](fkId, contextOptions, function(err, inst) {
if (err) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
return cb(err);
sharedClass.defineMethod(
methodName,
opts,
function(fkId) {
const args = Array.prototype.slice.call(arguments, 1);
const cb = args[args.length - 1];
const contextOptions =
(hasOptionsFromContext && args[args.length - 2]) || {};
this[getterName](fkId, contextOptions, function(err, inst) {
if (err) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
return cb(err);
}
} else {
cb(err, null);
}
} else {
cb(err, null);
}
});
}, method.isStatic);
});
},
method.isStatic,
);
} else {
sharedClass.defineMethod(methodName, opts, function() {
const args = Array.prototype.slice.call(arguments);
const cb = args[args.length - 1];
const contextOptions =
hasOptionsFromContext && args[args.length - 2] || {};
this[getterName](contextOptions, function(err, inst) {
if (err) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
return cb(err);
sharedClass.defineMethod(
methodName,
opts,
function() {
const args = Array.prototype.slice.call(arguments);
const cb = args[args.length - 1];
const contextOptions =
(hasOptionsFromContext && args[args.length - 2]) || {};
this[getterName](contextOptions, function(err, inst) {
if (err) return cb(err);
if (inst instanceof relation.modelTo) {
try {
nestedFn.apply(inst, args);
} catch (err) {
return cb(err);
}
} else {
cb(err, null);
}
} else {
cb(err, null);
}
});
}, method.isStatic);
});
},
method.isStatic,
);
}
}
});
@ -1025,9 +1113,15 @@ module.exports = function(registry) {
sharedClass.methods().forEach(function(method) {
const delegateTo = method.rest && method.rest.delegateTo;
if (delegateTo && delegateTo.ctor == relation.modelTo) {
const before = method.isStatic ? beforeListeners : beforeListeners['prototype'];
const after = method.isStatic ? afterListeners : afterListeners['prototype'];
const m = method.isStatic ? method.name : 'prototype.' + method.name;
const before = method.isStatic ?
beforeListeners :
beforeListeners['prototype'];
const after = method.isStatic ?
afterListeners :
afterListeners['prototype'];
const m = method.isStatic ?
method.name :
'prototype.' + method.name;
if (before && before[delegateTo.name]) {
self.beforeRemote(m, function(ctx, result, next) {
before[delegateTo.name]._listeners.call(null, ctx, next);
@ -1042,7 +1136,11 @@ module.exports = function(registry) {
});
});
} else {
const msg = g.f('Relation `%s` does not exist for model `%s`', relationName, this.modelName);
const msg = g.f(
'Relation `%s` does not exist for model `%s`',
relationName,
this.modelName,
);
throw new Error(msg);
}
};

File diff suppressed because it is too large Load Diff